Merge branch 'feature/UpdateSend2Trash' into dev

This commit is contained in:
JackDandy 2023-02-09 14:26:15 +00:00
commit 9ad837f28b
16 changed files with 550 additions and 197 deletions

View file

@ -4,6 +4,7 @@
* Add Filelock 3.9.0 (ce3e891) * Add Filelock 3.9.0 (ce3e891)
* Remove Lockfile no longer used by Cachecontrol * Remove Lockfile no longer used by Cachecontrol
* Update Msgpack 1.0.0 (fa7d744) to 1.0.4 (b5acfd5) * Update Msgpack 1.0.0 (fa7d744) to 1.0.4 (b5acfd5)
* Update Send2Trash 1.5.0 (66afce7) to 1.8.1b0 (0ef9b32)
* Update SimpleJSON 3.16.1 (ce75e60) to 3.18.1 (c891b95) * Update SimpleJSON 3.16.1 (ce75e60) to 3.18.1 (c891b95)
* Update tmdbsimple 2.6.6 (679e343) to 2.9.1 (9da400a) * Update tmdbsimple 2.6.6 (679e343) to 2.9.1 (9da400a)
* Update torrent_parser 0.3.0 (2a4eecb) to 0.4.0 (23b9e11) * Update torrent_parser 0.3.0 (2a4eecb) to 0.4.0 (23b9e11)

View file

@ -1,21 +1,21 @@
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net) # Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
# This software is licensed under the "BSD" License as described in the "LICENSE" file, # 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 # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
import sys import sys
from .exceptions import TrashPermissionError from send2trash.exceptions import TrashPermissionError # noqa: F401
if sys.platform == 'darwin': if sys.platform == "darwin":
from .plat_osx import send2trash from send2trash.mac import send2trash
elif sys.platform == 'win32': elif sys.platform == "win32":
from .plat_win import send2trash from send2trash.win import send2trash
else: else:
try: try:
# If we can use gio, let's use it # If we can use gio, let's use it
from .plat_gio import send2trash from send2trash.plat_gio import send2trash
except ImportError: except ImportError:
# Oh well, let's fallback to our own Freedesktop trash implementation # Oh well, let's fallback to our own Freedesktop trash implementation
from .plat_other import send2trash from send2trash.plat_other import send2trash # noqa: F401

View file

@ -0,0 +1,33 @@
# encoding: utf-8
# 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 print_function
import sys
from argparse import ArgumentParser
from send2trash import send2trash
def main(args=None):
parser = ArgumentParser(description="Tool to send files to trash")
parser.add_argument("files", nargs="+")
parser.add_argument("-v", "--verbose", action="store_true", help="Print deleted files")
args = parser.parse_args(args)
for filename in args.files:
try:
send2trash(filename)
if args.verbose:
print("Trashed «" + filename + "»")
except OSError as e:
print(str(e), file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View file

@ -15,6 +15,6 @@ if PY3:
# environb will be unset under Windows, but then again we're not supposed to use it. # environb will be unset under Windows, but then again we're not supposed to use it.
environb = os.environb environb = os.environb
else: else:
text_type = unicode text_type = unicode # noqa: F821
binary_type = str binary_type = str
environb = os.environ environb = os.environ

View file

@ -1,11 +1,12 @@
import errno import errno
from .compat import PY3 from send2trash.compat import PY3
if PY3: if PY3:
_permission_error = PermissionError _permission_error = PermissionError # noqa: F821
else: else:
_permission_error = OSError _permission_error = OSError
class TrashPermissionError(_permission_error): class TrashPermissionError(_permission_error):
"""A permission error specific to a trash directory. """A permission error specific to a trash directory.
@ -20,6 +21,6 @@ class TrashPermissionError(_permission_error):
data between partitions, devices, or network drives, so we don't do it as data between partitions, devices, or network drives, so we don't do it as
a fallback. a fallback.
""" """
def __init__(self, filename): def __init__(self, filename):
_permission_error.__init__(self, errno.EACCES, "Permission denied", _permission_error.__init__(self, errno.EACCES, "Permission denied", filename)
filename)

View file

@ -0,0 +1,20 @@
# 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 platform import mac_ver
from sys import version_info
# NOTE: version of pyobjc only supports python >= 3.6 and 10.9+
macos_ver = tuple(int(part) for part in mac_ver()[0].split("."))
if version_info >= (3, 6) and macos_ver >= (10, 9):
try:
from send2trash.mac.modern import send2trash
except ImportError:
# Try to fall back to ctypes version, although likely problematic still
from send2trash.mac.legacy import send2trash
else:
# Just use the old version otherwise
from send2trash.mac.legacy import send2trash # noqa: F401

View file

@ -9,10 +9,11 @@ from __future__ import unicode_literals
from ctypes import cdll, byref, Structure, c_char, c_char_p from ctypes import cdll, byref, Structure, c_char, c_char_p
from ctypes.util import find_library from ctypes.util import find_library
from .compat import binary_type from send2trash.compat import binary_type
from send2trash.util import preprocess_paths
Foundation = cdll.LoadLibrary(find_library('Foundation')) Foundation = cdll.LoadLibrary(find_library("Foundation"))
CoreServices = cdll.LoadLibrary(find_library('CoreServices')) CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
GetMacOSStatusCommentString.restype = c_char_p GetMacOSStatusCommentString.restype = c_char_p
@ -28,21 +29,25 @@ kFSFileOperationSkipSourcePermissionErrors = 0x02
kFSFileOperationDoNotMoveAcrossVolumes = 0x04 kFSFileOperationDoNotMoveAcrossVolumes = 0x04
kFSFileOperationSkipPreflight = 0x08 kFSFileOperationSkipPreflight = 0x08
class FSRef(Structure): class FSRef(Structure):
_fields_ = [('hidden', c_char * 80)] _fields_ = [("hidden", c_char * 80)]
def check_op_result(op_result): def check_op_result(op_result):
if op_result: if op_result:
msg = GetMacOSStatusCommentString(op_result).decode('utf-8') msg = GetMacOSStatusCommentString(op_result).decode("utf-8")
raise OSError(msg) raise OSError(msg)
def send2trash(path):
if not isinstance(path, binary_type): def send2trash(paths):
path = path.encode('utf-8') paths = preprocess_paths(paths)
fp = FSRef() paths = [path.encode("utf-8") if not isinstance(path, binary_type) else path for path in paths]
opts = kFSPathMakeRefDoNotFollowLeafSymlink for path in paths:
op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None) fp = FSRef()
check_op_result(op_result) opts = kFSPathMakeRefDoNotFollowLeafSymlink
opts = kFSFileOperationDefaultOptions op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None)
op_result = FSMoveObjectToTrashSync(byref(fp), None, opts) check_op_result(op_result)
check_op_result(op_result) opts = kFSFileOperationDefaultOptions
op_result = FSMoveObjectToTrashSync(byref(fp), None, opts)
check_op_result(op_result)

View file

@ -0,0 +1,26 @@
# 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 Foundation import NSFileManager, NSURL
from send2trash.compat import text_type
from send2trash.util import preprocess_paths
def check_op_result(op_result):
# First value will be false on failure
if not op_result[0]:
# Error is in third value, localized failure reason matchs ctypes version
raise OSError(op_result[2].localizedFailureReason())
def send2trash(paths):
paths = preprocess_paths(paths)
paths = [path.decode("utf-8") if not isinstance(path, text_type) else path for path in paths]
for path in paths:
file_url = NSURL.fileURLWithPath_(path)
fm = NSFileManager.defaultManager()
op_result = fm.trashItemAtURL_resultingItemURL_error_(file_url, None, None)
check_op_result(op_result)

View file

@ -5,15 +5,19 @@
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from gi.repository import GObject, Gio from gi.repository import GObject, Gio
from .exceptions import TrashPermissionError from send2trash.exceptions import TrashPermissionError
from send2trash.util import preprocess_paths
def send2trash(path):
try: def send2trash(paths):
f = Gio.File.new_for_path(path) paths = preprocess_paths(paths)
f.trash(cancellable=None) for path in paths:
except GObject.GError as e: try:
if e.code == Gio.IOErrorEnum.NOT_SUPPORTED: f = Gio.File.new_for_path(path)
# We get here if we can't create a trash directory on the same f.trash(cancellable=None)
# device. I don't know if other errors can result in NOT_SUPPORTED. except GObject.GError as e:
raise TrashPermissionError('') if e.code == Gio.IOErrorEnum.NOT_SUPPORTED:
raise OSError(e.message) # We get here if we can't create a trash directory on the same
# device. I don't know if other errors can result in NOT_SUPPORTED.
raise TrashPermissionError("")
raise OSError(e.message)

View file

@ -19,44 +19,51 @@ from __future__ import unicode_literals
import errno import errno
import sys import sys
import os import os
import shutil
import os.path as op import os.path as op
from datetime import datetime from datetime import datetime
import stat import stat
try: try:
from urllib.parse import quote from urllib.parse import quote
except ImportError: except ImportError:
# Python 2 # Python 2
from urllib import quote from urllib import quote
from .compat import text_type, environb from send2trash.compat import text_type, environb
from .exceptions import TrashPermissionError from send2trash.util import preprocess_paths
from send2trash.exceptions import TrashPermissionError
try: try:
fsencode = os.fsencode # Python 3 fsencode = os.fsencode # Python 3
fsdecode = os.fsdecode fsdecode = os.fsdecode
except AttributeError: except AttributeError:
def fsencode(u): # Python 2
def fsencode(u): # Python 2
return u.encode(sys.getfilesystemencoding()) return u.encode(sys.getfilesystemencoding())
def fsdecode(b): def fsdecode(b):
return b.decode(sys.getfilesystemencoding()) return b.decode(sys.getfilesystemencoding())
# The Python 3 versions are a bit smarter, handling surrogate escapes, # The Python 3 versions are a bit smarter, handling surrogate escapes,
# but these should work in most cases. # but these should work in most cases.
FILES_DIR = b'files' FILES_DIR = b"files"
INFO_DIR = b'info' INFO_DIR = b"info"
INFO_SUFFIX = b'.trashinfo' INFO_SUFFIX = b".trashinfo"
# Default of ~/.local/share [3] # Default of ~/.local/share [3]
XDG_DATA_HOME = op.expanduser(environb.get(b'XDG_DATA_HOME', b'~/.local/share')) XDG_DATA_HOME = op.expanduser(environb.get(b"XDG_DATA_HOME", b"~/.local/share"))
HOMETRASH_B = op.join(XDG_DATA_HOME, b'Trash') HOMETRASH_B = op.join(XDG_DATA_HOME, b"Trash")
HOMETRASH = fsdecode(HOMETRASH_B) HOMETRASH = fsdecode(HOMETRASH_B)
uid = os.getuid() uid = os.getuid()
TOPDIR_TRASH = b'.Trash' TOPDIR_TRASH = b".Trash"
TOPDIR_FALLBACK = b'.Trash-' + text_type(uid).encode('ascii') TOPDIR_FALLBACK = b".Trash-" + text_type(uid).encode("ascii")
def is_parent(parent, path): def is_parent(parent, path):
path = op.realpath(path) # In case it's a symlink path = op.realpath(path) # In case it's a symlink
if isinstance(path, text_type): if isinstance(path, text_type):
path = fsencode(path) path = fsencode(path)
parent = op.realpath(parent) parent = op.realpath(parent)
@ -64,9 +71,11 @@ def is_parent(parent, path):
parent = fsencode(parent) parent = fsencode(parent)
return path.startswith(parent) return path.startswith(parent)
def format_date(date): def format_date(date):
return date.strftime("%Y-%m-%dT%H:%M:%S") return date.strftime("%Y-%m-%dT%H:%M:%S")
def info_for(src, topdir): def info_for(src, topdir):
# ...it MUST not include a ".." directory, and for files not "under" that # ...it MUST not include a ".." directory, and for files not "under" that
# directory, absolute pathnames must be used. [2] # directory, absolute pathnames must be used. [2]
@ -75,17 +84,19 @@ def info_for(src, topdir):
else: else:
src = op.relpath(src, topdir) src = op.relpath(src, topdir)
info = "[Trash Info]\n" info = "[Trash Info]\n"
info += "Path=" + quote(src) + "\n" info += "Path=" + quote(src) + "\n"
info += "DeletionDate=" + format_date(datetime.now()) + "\n" info += "DeletionDate=" + format_date(datetime.now()) + "\n"
return info return info
def check_create(dir): def check_create(dir):
# use 0700 for paths [3] # use 0700 for paths [3]
if not op.exists(dir): if not op.exists(dir):
os.makedirs(dir, 0o700) os.makedirs(dir, 0o700)
def trash_move(src, dst, topdir=None):
def trash_move(src, dst, topdir=None, cross_dev=False):
filename = op.basename(src) filename = op.basename(src)
filespath = op.join(dst, FILES_DIR) filespath = op.join(dst, FILES_DIR)
infopath = op.join(dst, INFO_DIR) infopath = op.join(dst, INFO_DIR)
@ -95,24 +106,29 @@ def trash_move(src, dst, topdir=None):
destname = filename destname = filename
while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)): while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
counter += 1 counter += 1
destname = base_name + b' ' + text_type(counter).encode('ascii') + ext destname = base_name + b" " + text_type(counter).encode("ascii") + ext
check_create(filespath) check_create(filespath)
check_create(infopath) check_create(infopath)
os.rename(src, op.join(filespath, destname)) with open(op.join(infopath, destname + INFO_SUFFIX), "w") as f:
f = open(op.join(infopath, destname + INFO_SUFFIX), 'w') f.write(info_for(src, topdir))
f.write(info_for(src, topdir)) destpath = op.join(filespath, destname)
f.close() if cross_dev:
shutil.move(src, destpath)
else:
os.rename(src, destpath)
def find_mount_point(path): def find_mount_point(path):
# Even if something's wrong, "/" is a mount point, so the loop will exit. # Even if something's wrong, "/" is a mount point, so the loop will exit.
# Use realpath in case it's a symlink # Use realpath in case it's a symlink
path = op.realpath(path) # Required to avoid infinite loop path = op.realpath(path) # Required to avoid infinite loop
while not op.ismount(path): while not op.ismount(path): # Note ismount() does not always detect mounts
path = op.split(path)[0] path = op.split(path)[0]
return path return path
def find_ext_volume_global_trash(volume_root): def find_ext_volume_global_trash(volume_root):
# from [2] Trash directories (1) check for a .Trash dir with the right # from [2] Trash directories (1) check for a .Trash dir with the right
# permissions set. # permissions set.
@ -126,13 +142,14 @@ def find_ext_volume_global_trash(volume_root):
if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX): if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
return None return None
trash_dir = op.join(trash_dir, text_type(uid).encode('ascii')) trash_dir = op.join(trash_dir, text_type(uid).encode("ascii"))
try: try:
check_create(trash_dir) check_create(trash_dir)
except OSError: except OSError:
return None return None
return trash_dir return trash_dir
def find_ext_volume_fallback_trash(volume_root): def find_ext_volume_fallback_trash(volume_root):
# from [2] Trash directories (1) create a .Trash-$uid dir. # from [2] Trash directories (1) create a .Trash-$uid dir.
trash_dir = op.join(volume_root, TOPDIR_FALLBACK) trash_dir = op.join(volume_root, TOPDIR_FALLBACK)
@ -145,48 +162,57 @@ def find_ext_volume_fallback_trash(volume_root):
raise raise
return trash_dir return trash_dir
def find_ext_volume_trash(volume_root): def find_ext_volume_trash(volume_root):
trash_dir = find_ext_volume_global_trash(volume_root) trash_dir = find_ext_volume_global_trash(volume_root)
if trash_dir is None: if trash_dir is None:
trash_dir = find_ext_volume_fallback_trash(volume_root) trash_dir = find_ext_volume_fallback_trash(volume_root)
return trash_dir return trash_dir
# Pull this out so it's easy to stub (to avoid stubbing lstat itself) # Pull this out so it's easy to stub (to avoid stubbing lstat itself)
def get_dev(path): def get_dev(path):
return os.lstat(path).st_dev return os.lstat(path).st_dev
def send2trash(path):
if isinstance(path, text_type):
path_b = fsencode(path)
elif isinstance(path, bytes):
path_b = path
elif hasattr(path, '__fspath__'):
# Python 3.6 PathLike protocol
return send2trash(path.__fspath__())
else:
raise TypeError('str, bytes or PathLike expected, not %r' % type(path))
if not op.exists(path_b): def send2trash(paths):
raise OSError("File not found: %s" % path) paths = preprocess_paths(paths)
# ...should check whether the user has the necessary permissions to delete for path in paths:
# it, before starting the trashing operation itself. [2] if isinstance(path, text_type):
if not os.access(path_b, os.W_OK): path_b = fsencode(path)
raise OSError("Permission denied: %s" % path) elif isinstance(path, bytes):
# if the file to be trashed is on the same device as HOMETRASH we path_b = path
# want to move it there. else:
path_dev = get_dev(path_b) raise TypeError("str, bytes or PathLike expected, not %r" % type(path))
# If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the if not op.exists(path_b):
# home directory, and these paths will be created further on if needed. raise OSError(errno.ENOENT, "File not found: %s" % path)
trash_dev = get_dev(op.expanduser(b'~')) # ...should check whether the user has the necessary permissions to delete
# it, before starting the trashing operation itself. [2]
if not os.access(path_b, os.W_OK):
raise OSError(errno.EACCES, "Permission denied: %s" % path)
if path_dev == trash_dev: path_dev = get_dev(path_b)
topdir = XDG_DATA_HOME # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
dest_trash = HOMETRASH_B # home directory, and these paths will be created further on if needed.
else: trash_dev = get_dev(op.expanduser(b"~"))
topdir = find_mount_point(path_b)
trash_dev = get_dev(topdir) # if the file to be trashed is on the same device as HOMETRASH we
if trash_dev != path_dev: # want to move it there.
raise OSError("Couldn't find mount point for %s" % path) if path_dev == trash_dev:
dest_trash = find_ext_volume_trash(topdir) topdir = XDG_DATA_HOME
trash_move(path_b, dest_trash, topdir) dest_trash = HOMETRASH_B
else:
topdir = find_mount_point(path_b)
trash_dev = get_dev(topdir)
if trash_dev != path_dev:
raise OSError("Couldn't find mount point for %s" % path)
dest_trash = find_ext_volume_trash(topdir)
try:
trash_move(path_b, dest_trash, topdir)
except OSError as error:
# Cross link errors default back to HOMETRASH
if error.errno == errno.EXDEV:
trash_move(path_b, HOMETRASH_B, XDG_DATA_HOME, cross_dev=True)
else:
raise

View file

@ -1,103 +0,0 @@
# 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
from ctypes import (windll, Structure, byref, c_uint,
create_unicode_buffer, addressof,
GetLastError, FormatError)
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),
("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
def send2trash(path):
if not isinstance(path, text_type):
path = text_type(path, 'mbcs')
if not op.isabs(path):
path = op.abspath(path)
try:
s2t(path)
except (BaseException, Exception):
short_path = get_short_path_name(path)
try:
s2t(short_path)
except (BaseException, Exception):
s2t(short_path.value[4:]) # Remove '\\?\' for SHFileOperationW
def s2t(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'.
buf = create_unicode_buffer(path, len(path)+2)
fileop.pFrom = LPCWSTR(addressof(buf))
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)

14
lib/send2trash/util.py Normal file
View file

@ -0,0 +1,14 @@
# encoding: utf-8
# 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
def preprocess_paths(paths):
if not isinstance(paths, list):
paths = [paths]
# Convert items such as pathlib paths to strings
paths = [path.__fspath__() if hasattr(path, "__fspath__") else path for path in paths]
return paths

View file

@ -0,0 +1,46 @@
# Sample implementation of IFileOperationProgressSink that just prints
# some basic info
import pythoncom
from win32com.shell import shell, shellcon
from win32com.server.policy import DesignatedWrapPolicy
class FileOperationProgressSink(DesignatedWrapPolicy):
_com_interfaces_ = [shell.IID_IFileOperationProgressSink]
_public_methods_ = [
"StartOperations",
"FinishOperations",
"PreRenameItem",
"PostRenameItem",
"PreMoveItem",
"PostMoveItem",
"PreCopyItem",
"PostCopyItem",
"PreDeleteItem",
"PostDeleteItem",
"PreNewItem",
"PostNewItem",
"UpdateProgress",
"ResetTimer",
"PauseTimer",
"ResumeTimer",
]
def __init__(self):
self._wrap_(self)
self.newItem = None
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
def PostDeleteItem(self, flags, item, hr_delete, newly_created):
if newly_created:
self.newItem = newly_created.GetDisplayName(shellcon.SHGDN_FORPARSING)
def create_sink():
return pythoncom.WrapObject(FileOperationProgressSink(), shell.IID_IFileOperationProgressSink)

View file

@ -0,0 +1,20 @@
# 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
from platform import version
# if windows is vista or newer and pywin32 is available use IFileOperation
modern = int(version().split(".", 1)[0]) >= 6
if modern:
try:
# Attempt to use pywin32 to use IFileOperation
from send2trash.win.modern import send2trash
except ImportError:
modern = False
if not modern:
# use SHFileOperation as fallback
from send2trash.win.legacy import send2trash # noqa: F401

View file

@ -0,0 +1,194 @@
# 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 send2trash.compat import text_type
from send2trash.util import preprocess_paths
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 convert_sh_file_opt_result(result):
# map overlapping values from SHFileOpterationW to approximate standard windows errors
# ref https://docs.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-shfileoperationw#return-value
# ref https://docs.microsoft.com/en-us/windows/win32/debug/system-error-codes--0-499-
results = {
0x71: 0x50, # DE_SAMEFILE -> ERROR_FILE_EXISTS
0x72: 0x57, # DE_MANYSRC1DEST -> ERROR_INVALID_PARAMETER
0x73: 0x57, # DE_DIFFDIR -> ERROR_INVALID_PARAMETER
0x74: 0x57, # DE_ROOTDIR -> ERROR_INVALID_PARAMETER
0x75: 0x4C7, # DE_OPCANCELLED -> ERROR_CANCELLED
0x76: 0x57, # DE_DESTSUBTREE -> ERROR_INVALID_PARAMETER
0x78: 0x05, # DE_ACCESSDENIEDSRC -> ERROR_ACCESS_DENIED
0x79: 0x6F, # DE_PATHTOODEEP -> ERROR_BUFFER_OVERFLOW
0x7A: 0x57, # DE_MANYDEST -> ERROR_INVALID_PARAMETER
0x7C: 0xA1, # DE_INVALIDFILES -> ERROR_BAD_PATHNAME
0x7D: 0x57, # DE_DESTSAMETREE -> ERROR_INVALID_PARAMETER
0x7E: 0xB7, # DE_FLDDESTISFILE -> ERROR_ALREADY_EXISTS
0x80: 0xB7, # DE_FILEDESTISFLD -> ERROR_ALREADY_EXISTS
0x81: 0x6F, # DE_FILENAMETOOLONG -> ERROR_BUFFER_OVERFLOW
0x82: 0x13, # DE_DEST_IS_CDROM -> ERROR_WRITE_PROTECT
0x83: 0x13, # DE_DEST_IS_DVD -> ERROR_WRITE_PROTECT
0x84: 0x6F9, # DE_DEST_IS_CDRECORD -> ERROR_UNRECOGNIZED_MEDIA
0x85: 0xDF, # DE_FILE_TOO_LARGE -> ERROR_FILE_TOO_LARGE
0x86: 0x13, # DE_SRC_IS_CDROM -> ERROR_WRITE_PROTECT
0x87: 0x13, # DE_SRC_IS_DVD -> ERROR_WRITE_PROTECT
0x88: 0x6F9, # DE_SRC_IS_CDRECORD -> ERROR_UNRECOGNIZED_MEDIA
0xB7: 0x6F, # DE_ERROR_MAX -> ERROR_BUFFER_OVERFLOW
0x402: 0xA1, # UNKNOWN -> ERROR_BAD_PATHNAME
0x10000: 0x1D, # ERRORONDEST -> ERROR_WRITE_FAULT
0x10074: 0x57, # DE_ROOTDIR | ERRORONDEST -> ERROR_INVALID_PARAMETER
}
if result in results.keys():
return results[result]
else:
return result
def prefix_and_path(path):
r"""Guess the long-path prefix based on the kind of *path*.
Local paths (C:\folder\file.ext) and UNC names (\\server\folder\file.ext)
are handled.
Return a tuple of the long-path prefix and the prefixed path.
"""
prefix, long_path = "\\\\?\\", path
if not path.startswith(prefix):
if path.startswith("\\\\"):
# Likely a UNC name
prefix = "\\\\?\\UNC"
long_path = prefix + path[1:]
else:
# Likely a local path
long_path = prefix + path
elif path.startswith(prefix + "UNC\\"):
# UNC name with long-path prefix
prefix = "\\\\?\\UNC"
return prefix, long_path
def get_awaited_path_from_prefix(prefix, path):
"""Guess the correct path to pass to the SHFileOperationW() call.
The long-path prefix must be removed, so we should take care of
different long-path prefixes.
"""
if prefix == "\\\\?\\UNC":
# We need to prepend a backslash for UNC names, as it was removed
# in prefix_and_path().
return "\\" + path[len(prefix) :]
return path[len(prefix) :]
def get_short_path_name(long_name):
prefix, long_path = prefix_and_path(long_name)
buf_size = GetShortPathNameW(long_path, 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_path)
output = create_unicode_buffer(buf_size)
GetShortPathNameW(long_path, output, buf_size)
return get_awaited_path_from_prefix(prefix, output.value)
def send2trash(paths):
paths = preprocess_paths(paths)
if not paths:
return
# 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
short_paths = [get_short_path_name(path) for path in paths]
try:
s2t(short_paths)
except(BaseException, Exception):
s2t(paths)
def s2t(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'.
# Additional note on another issue here, unicode_buffer expects length in
# bytes essentially, so having multi-byte characters causes issues if just
# passing pythons string length. Instead of dealing with this difference we
# just create a buffer then a new one with an extra null. Since the non-length
# specified version apparently stops after the first null, join with a space first.
buffer = create_unicode_buffer(" ".join(paths))
# convert to a single string of null terminated paths
path_string = "\0".join(paths)
buffer = create_unicode_buffer(path_string, len(buffer) + 1)
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:
error = convert_sh_file_opt_result(result)
raise WindowsError(None, FormatError(error), paths, error)

View file

@ -0,0 +1,66 @@
# 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 send2trash.compat import text_type
from send2trash.util import preprocess_paths
from platform import version
import pythoncom
import pywintypes
from win32com.shell import shell, shellcon
from send2trash.win.IFileOperationProgressSink import create_sink
def send2trash(paths):
paths = preprocess_paths(paths)
if not paths:
return
# 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]
# Need to initialize the com before using
pythoncom.CoInitialize()
# 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 | 0x00080000 # FOFX_ADDUNDORECORD win 8+ # 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
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)
finally:
# Need to make sure we call this once fore every init
pythoncom.CoUninitialize()