diff --git a/send2trash/plat_win.py b/send2trash/plat_win.py index 2020748..5e50801 100644 --- a/send2trash/plat_win.py +++ b/send2trash/plat_win.py @@ -7,15 +7,19 @@ from __future__ import unicode_literals from ctypes import (windll, Structure, byref, c_uint, - create_unicode_buffer, sizeof, addressof) + create_unicode_buffer, addressof) from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL import os.path as op from .compat import text_type +kernel32 = windll.kernel32 +GetShortPathNameW = kernel32.GetShortPathNameW + shell32 = windll.shell32 SHFileOperationW = shell32.SHFileOperationW + class SHFILEOPSTRUCTW(Structure): _fields_ = [ ("hwnd", HWND), @@ -28,6 +32,7 @@ class SHFILEOPSTRUCTW(Structure): ("lpszProgressTitle", LPCWSTR), ] + FO_MOVE = 1 FO_COPY = 2 FO_DELETE = 3 @@ -39,11 +44,22 @@ FOF_NOCONFIRMATION = 16 FOF_ALLOWUNDO = 64 FOF_NOERRORUI = 1024 + +def get_short_path_name(long_name): + if not long_name.startswith('\\\\?\\'): + long_name = '\\\\?\\' + long_name + buf_size = GetShortPathNameW(long_name, None, 0) + output = create_unicode_buffer(buf_size) + GetShortPathNameW(long_name, output, buf_size) + return output.value[4:] # Remove '\\?\' for SHFileOperationW + + def send2trash(path): if not isinstance(path, text_type): path = text_type(path, 'mbcs') if not op.isabs(path): path = op.abspath(path) + path = get_short_path_name(path) fileop = SHFILEOPSTRUCTW() fileop.hwnd = 0 fileop.wFunc = FO_DELETE @@ -51,7 +67,7 @@ def send2trash(path): # Starting in python 3.6.3 it is no longer possible to use: # LPCWSTR(path + '\0') directly as embedded null characters are no longer # allowed in strings - # Workaround + # Workaround # - create buffer of c_wchar[] (LPCWSTR is based on this type) # - buffer is two c_wchar characters longer (double null terminator) # - cast the address of the buffer to a LPCWSTR diff --git a/tests/test_plat_win.py b/tests/test_plat_win.py new file mode 100644 index 0000000..b7d1608 --- /dev/null +++ b/tests/test_plat_win.py @@ -0,0 +1,46 @@ +# coding: utf-8 +import os +import sys +import unittest +from os import path as op +from tempfile import gettempdir + +from send2trash import send2trash as s2t + + +@unittest.skipIf(sys.platform != 'win32', 'Windows only') +class TestLongPath(unittest.TestCase): + def setUp(self): + filename = 'A' * 100 + self.dirname = '\\\\?\\' + os.path.join(gettempdir(), filename) + self.file = os.path.join( + self.dirname, + filename, + filename, # From there, the path is not trashable from Explorer + filename, + filename + '.txt') + self._create_tree(self.file) + + def tearDown(self): + try: + os.remove(self.dirname) + except OSError: + pass + + def _create_tree(self, path): + dirname = os.path.dirname(path) + if not os.path.isdir(dirname): + os.makedirs(dirname) + with open(path, 'w') as writer: + writer.write('Looong filename!') + + def test_trash_file(self): + s2t(self.file) + self.assertFalse(op.exists(self.file)) + + @unittest.skipIf( + op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0], + 'Cannot trash long path from other drive') + def test_trash_folder(self): + s2t(self.dirname) + self.assertFalse(op.exists(self.dirname))