Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Windows: fail if not recyclable #89

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion send2trash/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@
try:
from collections.abc import Iterable as iterable_type
except ImportError:
from collections import Iterable as iterable_type
from collections import Iterable as iterable_type # noqa: F401
32 changes: 24 additions & 8 deletions send2trash/win/IFileOperationProgressSink.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
from win32com.server.policy import DesignatedWrapPolicy


class E_Fail(Exception):
pass


class FileOperationProgressSink(DesignatedWrapPolicy):
_com_interfaces_ = [shell.IID_IFileOperationProgressSink]
_public_methods_ = [
Expand All @@ -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)
51 changes: 38 additions & 13 deletions send2trash/win/modern.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@
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):
Expand Down Expand Up @@ -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()