From 1c5fea461f5b40857f0f9a2d7dad718d74e974dd Mon Sep 17 00:00:00 2001 From: Andrew Senetar Date: Fri, 22 May 2020 12:22:32 -0500 Subject: [PATCH] Split plat_win into legacy and modern --- send2trash/plat_win.py | 165 ++-------------------------------- send2trash/plat_win_legacy.py | 105 ++++++++++++++++++++++ send2trash/plat_win_modern.py | 65 ++++++++++++++ 3 files changed, 175 insertions(+), 160 deletions(-) create mode 100644 send2trash/plat_win_legacy.py create mode 100644 send2trash/plat_win_modern.py diff --git a/send2trash/plat_win.py b/send2trash/plat_win.py index d6a2192..a39833c 100644 --- a/send2trash/plat_win.py +++ b/send2trash/plat_win.py @@ -5,171 +5,16 @@ # http://www.hardcoded.net/licenses/bsd_license from __future__ import unicode_literals -import os.path as op -from .compat import text_type from platform import version -legacy = False # if windows is vista or newer and pywin32 is available use IFileOperation if int(version().split(".", 1)[0]) >= 6: try: # Attempt to use pywin32 to use IFileOperation - import pythoncom - import pywintypes - from win32com.shell import shell, shellcon - - def send2trash(path): - if not isinstance(path, list): - path = [path] - # convert data type - path = [ - text_type(item, "mbcs") if not isinstance(item, text_type) else item - for item in path - ] - # convert to full paths - path = [op.abspath(item) if not op.isabs(item) else item for item in path] - # remove the leading \\?\ if present - path = [item[4:] if item.startswith("\\\\?\\") else item for item in path] - # create instance of file operation object - fileop = pythoncom.CoCreateInstance( - shell.CLSID_FileOperation, - None, - pythoncom.CLSCTX_ALL, - shell.IID_IFileOperation, - ) - # default flags to use - flags = ( - shellcon.FOF_NOCONFIRMATION - | shellcon.FOF_NOERRORUI - | shellcon.FOF_SILENT - | shellcon.FOFX_EARLYFAILURE - ) - # determine rest of the flags based on OS version - # use newer recommended flags if available - if int(version().split(".", 1)[0]) >= 8: - flags |= ( - 0x20000000 # FOFX_ADDUNDORECORD win 8+ - | 0x00080000 # FOFX_RECYCLEONDELETE win 8+ - ) - else: - flags |= shellcon.FOF_ALLOWUNDO - # set the flags - fileop.SetOperationFlags(flags) - # actually try to perform the operation, this section may throw a - # pywintypes.com_error which does not seem to create as nice of an - # error as OSError so wrapping with try to convert - try: - for itemPath in path: - item = shell.SHCreateItemFromParsingName( - itemPath, None, shell.IID_IShellItem - ) - fileop.DeleteItem(item) - result = fileop.PerformOperations() - aborted = fileop.GetAnyOperationsAborted() - # if non-zero result or aborted throw an exception - if result or aborted: - raise OSError(None, None, path, result) - except pywintypes.com_error as error: - # convert to standard OS error, allows other code to get a - # normal errno - raise OSError(None, error.strerror, path, error.hresult) - + from .plat_win_modern import send2trash except ImportError: - legacy = True + # use SHFileOperation as fallback + from .plat_win_legacy import send2trash else: - legacy = True - -# use SHFileOperation as fallback -if legacy: - from ctypes import ( - windll, - Structure, - byref, - c_uint, - create_unicode_buffer, - addressof, - GetLastError, - FormatError, - ) - from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL - - kernel32 = windll.kernel32 - GetShortPathNameW = kernel32.GetShortPathNameW - - shell32 = windll.shell32 - SHFileOperationW = shell32.SHFileOperationW - - class SHFILEOPSTRUCTW(Structure): - _fields_ = [ - ("hwnd", HWND), - ("wFunc", UINT), - ("pFrom", LPCWSTR), - ("pTo", LPCWSTR), - ("fFlags", c_uint), - ("fAnyOperationsAborted", BOOL), - ("hNameMappings", c_uint), - ("lpszProgressTitle", LPCWSTR), - ] - - FO_MOVE = 1 - FO_COPY = 2 - FO_DELETE = 3 - FO_RENAME = 4 - - FOF_MULTIDESTFILES = 1 - FOF_SILENT = 4 - 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) - # FIX: https://github.com/hsoft/send2trash/issues/31 - # If buffer size is zero, an error has occurred. - if not buf_size: - err_no = GetLastError() - raise WindowsError(err_no, FormatError(err_no), long_name[4:]) - 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, list): - path = [path] - # convert data type - path = [ - text_type(item, "mbcs") if not isinstance(item, text_type) else item - for item in path - ] - # convert to full paths - path = [op.abspath(item) if not op.isabs(item) else item for item in path] - # get short path to handle path length issues - path = [get_short_path_name(item) for item in path] - # convert to a single string of null terminated paths - path = "\0".join(path) - fileop = SHFILEOPSTRUCTW() - fileop.hwnd = 0 - fileop.wFunc = FO_DELETE - # FIX: https://github.com/hsoft/send2trash/issues/17 - # 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 - # - 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 - # NOTE: based on how python allocates memory for these types they should - # always be zero, if this is ever not true we can go back to explicitly - # setting the last two characters to null using buffer[index] = '\0'. - buffer = create_unicode_buffer(path, len(path) + 2) - fileop.pFrom = LPCWSTR(addressof(buffer)) - fileop.pTo = None - fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT - fileop.fAnyOperationsAborted = 0 - fileop.hNameMappings = 0 - fileop.lpszProgressTitle = None - result = SHFileOperationW(byref(fileop)) - if result: - raise WindowsError(result, FormatError(result), path) + # use SHFileOperation as fallback + from .plat_win_legacy import send2trash diff --git a/send2trash/plat_win_legacy.py b/send2trash/plat_win_legacy.py new file mode 100644 index 0000000..e3c404b --- /dev/null +++ b/send2trash/plat_win_legacy.py @@ -0,0 +1,105 @@ +# Copyright 2017 Virgil Dupras + +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/bsd_license + +from __future__ import unicode_literals +import os.path as op +from .compat import text_type +from ctypes import ( + windll, + Structure, + byref, + c_uint, + create_unicode_buffer, + addressof, + GetLastError, + FormatError, +) +from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL + +kernel32 = windll.kernel32 +GetShortPathNameW = kernel32.GetShortPathNameW + +shell32 = windll.shell32 +SHFileOperationW = shell32.SHFileOperationW + + +class SHFILEOPSTRUCTW(Structure): + _fields_ = [ + ("hwnd", HWND), + ("wFunc", UINT), + ("pFrom", LPCWSTR), + ("pTo", LPCWSTR), + ("fFlags", c_uint), + ("fAnyOperationsAborted", BOOL), + ("hNameMappings", c_uint), + ("lpszProgressTitle", LPCWSTR), + ] + + +FO_MOVE = 1 +FO_COPY = 2 +FO_DELETE = 3 +FO_RENAME = 4 + +FOF_MULTIDESTFILES = 1 +FOF_SILENT = 4 +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) + # FIX: https://github.com/hsoft/send2trash/issues/31 + # If buffer size is zero, an error has occurred. + if not buf_size: + err_no = GetLastError() + raise WindowsError(err_no, FormatError(err_no), long_name[4:]) + output = create_unicode_buffer(buf_size) + GetShortPathNameW(long_name, output, buf_size) + return output.value[4:] # Remove '\\?\' for SHFileOperationW + + +def send2trash(paths): + if not isinstance(paths, list): + paths = [paths] + # convert data type + paths = [ + text_type(path, "mbcs") if not isinstance(path, text_type) else path + for path in paths + ] + # convert to full paths + paths = [op.abspath(path) if not op.isabs(path) else path for path in paths] + # get short path to handle path length issues + paths = [get_short_path_name(path) for path in paths] + # convert to a single string of null terminated paths + paths = "\0".join(paths) + fileop = SHFILEOPSTRUCTW() + fileop.hwnd = 0 + fileop.wFunc = FO_DELETE + # FIX: https://github.com/hsoft/send2trash/issues/17 + # 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 + # - 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 + # NOTE: based on how python allocates memory for these types they should + # always be zero, if this is ever not true we can go back to explicitly + # setting the last two characters to null using buffer[index] = '\0'. + buffer = create_unicode_buffer(paths, len(paths) + 2) + fileop.pFrom = LPCWSTR(addressof(buffer)) + fileop.pTo = None + fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT + fileop.fAnyOperationsAborted = 0 + fileop.hNameMappings = 0 + fileop.lpszProgressTitle = None + result = SHFileOperationW(byref(fileop)) + if result: + raise WindowsError(result, FormatError(result), paths) diff --git a/send2trash/plat_win_modern.py b/send2trash/plat_win_modern.py new file mode 100644 index 0000000..54914f8 --- /dev/null +++ b/send2trash/plat_win_modern.py @@ -0,0 +1,65 @@ +# Copyright 2017 Virgil Dupras + +# This software is licensed under the "BSD" License as described in the "LICENSE" file, +# which should be included with this package. The terms are also available at +# http://www.hardcoded.net/licenses/bsd_license + +from __future__ import unicode_literals +import os.path as op +from .compat import text_type +from platform import version +import pythoncom +import pywintypes +from win32com.shell import shell, shellcon + + +def send2trash(paths): + if not isinstance(paths, list): + paths = [paths] + # convert data type + paths = [ + text_type(path, "mbcs") if not isinstance(path, text_type) else path + for path in paths + ] + # convert to full paths + paths = [op.abspath(path) if not op.isabs(path) else path for path in paths] + # remove the leading \\?\ if present + paths = [path[4:] if path.startswith("\\\\?\\") else path for path in paths] + # create instance of file operation object + fileop = pythoncom.CoCreateInstance( + shell.CLSID_FileOperation, None, pythoncom.CLSCTX_ALL, shell.IID_IFileOperation, + ) + # default flags to use + flags = ( + shellcon.FOF_NOCONFIRMATION + | shellcon.FOF_NOERRORUI + | shellcon.FOF_SILENT + | shellcon.FOFX_EARLYFAILURE + ) + # determine rest of the flags based on OS version + # use newer recommended flags if available + if int(version().split(".", 1)[0]) >= 8: + flags |= ( + 0x20000000 # FOFX_ADDUNDORECORD win 8+ + | 0x00080000 # FOFX_RECYCLEONDELETE win 8+ + ) + else: + flags |= shellcon.FOF_ALLOWUNDO + # set the flags + fileop.SetOperationFlags(flags) + # actually try to perform the operation, this section may throw a + # pywintypes.com_error which does not seem to create as nice of an + # error as OSError so wrapping with try to convert + try: + for path in paths: + item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem) + fileop.DeleteItem(item) + result = fileop.PerformOperations() + aborted = fileop.GetAnyOperationsAborted() + # if non-zero result or aborted throw an exception + if result or aborted: + raise OSError(None, None, paths, result) + except pywintypes.com_error as error: + # convert to standard OS error, allows other code to get a + # normal errno + raise OSError(None, error.strerror, path, error.hresult)