This commit is contained in:
Dobatymo 2024-04-16 16:10:15 +01:00 committed by GitHub
commit d746cd9ea3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 62 additions and 21 deletions

View File

@ -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)

View File

@ -13,6 +13,22 @@ 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, ERROR_ACCESS_DENIED
# ERROR_FILE_NOT_FOUND: 0x80070002 is automatically handled by Python
winerrormap = {
shellcon.COPYENGINE_E_SHARING_VIOLATION_SRC: ERROR_SHARING_VIOLATION,
shellcon.COPYENGINE_E_ACCESS_DENIED_SRC: ERROR_ACCESS_DENIED,
}
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,20 +63,29 @@ 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)
fileop.DeleteItem(item, sink)
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)
try:
for path in paths:
item = shell.SHCreateItemFromParsingName(path, None, shell.IID_IShellItem)
fileop.DeleteItem(item, sink)
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)
try:
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:
assert len(pysink.errors) == 1, pysink.errors
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()