From 5e9d56bdcbebcea08cd6ca0a0e0bbada9eddfd4d Mon Sep 17 00:00:00 2001 From: Dobatymo Date: Wed, 13 Mar 2024 19:29:19 +0800 Subject: [PATCH] fail if not recyclable --- send2trash/win/IFileOperationProgressSink.py | 32 +++++++++++++++----- send2trash/win/modern.py | 24 +++++++++++++-- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/send2trash/win/IFileOperationProgressSink.py b/send2trash/win/IFileOperationProgressSink.py index c8702df..d1950b9 100644 --- a/send2trash/win/IFileOperationProgressSink.py +++ b/send2trash/win/IFileOperationProgressSink.py @@ -6,6 +6,10 @@ from win32com.shell import shell, shellcon from win32com.server.policy import DesignatedWrapPolicy +class E_Fail(Exception): + pass + + class FileOperationProgressSink(DesignatedWrapPolicy): _com_interfaces_ = [shell.IID_IFileOperationProgressSink] _public_methods_ = [ @@ -29,18 +33,30 @@ class FileOperationProgressSink(DesignatedWrapPolicy): def __init__(self): self._wrap_(self) - self.newItem = None + self.errors = [] def PreDeleteItem(self, flags, item): - # Can detect cases where to stop via flags and condition below, however the operation - # does not actual stop, we can resort to raising an exception as that does stop things - # but that may need some additional considerations before implementing. - return 0 if flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE else 0x80004005 # S_OK, or E_FAIL + # If TSF_DELETE_RECYCLE_IF_POSSIBLE is not set the file would not be moved to trash. + # Usually the code would have to return S_OK or E_FAIL to signal an abort to the file sink, + # however pywin32 doesn't use the return value of these callback methods [1], so we have to resort + # to raising an exception as that does stop things. + # [1] https://github.com/mhammond/pywin32/blob/1d29e4a4f317be9acbef9d5c5c5787269eacb040/com/win32com/src/PyGatewayBase.cpp#L757 + + name = item.GetDisplayName(shellcon.SHGDN_FORPARSING) + will_recycle = flags & shellcon.TSF_DELETE_RECYCLE_IF_POSSIBLE + if not will_recycle: + raise E_Fail(f"File would be deleted permanently: {name}") + + return None # HR cannot be returned here def PostDeleteItem(self, flags, item, hr_delete, newly_created): - if newly_created: - self.newItem = newly_created.GetDisplayName(shellcon.SHGDN_FORPARSING) + if hr_delete < 0: + name = item.GetDisplayName(shellcon.SHGDN_FORPARSING) + self.errors.append((name, hr_delete)) + + return None # HR cannot be returned here def create_sink(): - return pythoncom.WrapObject(FileOperationProgressSink(), shell.IID_IFileOperationProgressSink) + pysink = FileOperationProgressSink() + return pysink, pythoncom.WrapObject(pysink, shell.IID_IFileOperationProgressSink) diff --git a/send2trash/win/modern.py b/send2trash/win/modern.py index 7927a89..b0fa65c 100644 --- a/send2trash/win/modern.py +++ b/send2trash/win/modern.py @@ -13,6 +13,20 @@ import pythoncom import pywintypes from win32com.shell import shell, shellcon from send2trash.win.IFileOperationProgressSink import create_sink +from win32api import FormatMessage +from winerror import ERROR_SHARING_VIOLATION + +winerrormap = { + shellcon.COPYENGINE_E_SHARING_VIOLATION_SRC: ERROR_SHARING_VIOLATION, +} + + +def win_exception(winerror, filename): + # see `PyErr_SetExcFromWindowsErrWithFilenameObjects` + msg = FormatMessage(winerror).rstrip( + "\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f ." + ) + return WindowsError(None, msg, filename, winerror) def send2trash(paths): @@ -47,7 +61,7 @@ def send2trash(paths): # 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 - sink = create_sink() + pysink, sink = create_sink() try: for path in paths: item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem) @@ -55,12 +69,16 @@ def send2trash(paths): result = fileop.PerformOperations() aborted = fileop.GetAnyOperationsAborted() # if non-zero result or aborted throw an exception + assert not pysink.errors, pysink.errors if result or aborted: raise OSError(None, None, paths, result) - except pywintypes.com_error as error: + except pywintypes.com_error: + assert len(pysink.errors) == 1, pysink.errors # convert to standard OS error, allows other code to get a # normal errno - raise OSError(None, error.strerror, path, error.hresult) + path, hr = pysink.errors[0] + hr = winerrormap.get(hr + 2**32, hr) + raise win_exception(hr, path) finally: # Need to make sure we call this once fore every init pythoncom.CoUninitialize()