mirror of
				https://github.com/arsenetar/send2trash.git
				synced 2025-09-11 18:08:16 +00:00 
			
		
		
		
	Update other platforms for list support
Formatter also ran on these so some other minor changes.
This commit is contained in:
		
							parent
							
								
									1c5fea461f
								
							
						
					
					
						commit
						0d1d5ab40a
					
				| @ -7,13 +7,17 @@ | |||||||
| from gi.repository import GObject, Gio | from gi.repository import GObject, Gio | ||||||
| from .exceptions import TrashPermissionError | from .exceptions import TrashPermissionError | ||||||
| 
 | 
 | ||||||
| def send2trash(path): | 
 | ||||||
|     try: | def send2trash(paths): | ||||||
|         f = Gio.File.new_for_path(path) |     if not isinstance(paths, list): | ||||||
|         f.trash(cancellable=None) |         paths = [paths] | ||||||
|     except GObject.GError as e: |     for path in paths: | ||||||
|         if e.code == Gio.IOErrorEnum.NOT_SUPPORTED: |         try: | ||||||
|             # We get here if we can't create a trash directory on the same |             f = Gio.File.new_for_path(path) | ||||||
|             # device. I don't know if other errors can result in NOT_SUPPORTED. |             f.trash(cancellable=None) | ||||||
|             raise TrashPermissionError('') |         except GObject.GError as e: | ||||||
|         raise OSError(e.message) |             if e.code == Gio.IOErrorEnum.NOT_SUPPORTED: | ||||||
|  |                 # 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) | ||||||
|  | |||||||
| @ -11,8 +11,8 @@ from ctypes.util import find_library | |||||||
| 
 | 
 | ||||||
| from .compat import binary_type | from .compat import binary_type | ||||||
| 
 | 
 | ||||||
| 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 +28,29 @@ 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') |     if not isinstance(paths, list): | ||||||
|     fp = FSRef() |         paths = [paths] | ||||||
|     opts = kFSPathMakeRefDoNotFollowLeafSymlink |     paths = [ | ||||||
|     op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None) |         path.encode("utf-8") if not isinstance(path, binary_type) else path | ||||||
|     check_op_result(op_result) |         for path in paths | ||||||
|     opts = kFSFileOperationDefaultOptions |     ] | ||||||
|     op_result = FSMoveObjectToTrashSync(byref(fp), None, opts) |     for path in paths: | ||||||
|     check_op_result(op_result) |         fp = FSRef() | ||||||
|  |         opts = kFSPathMakeRefDoNotFollowLeafSymlink | ||||||
|  |         op_result = FSPathMakeRefWithOptions(path, opts, byref(fp), None) | ||||||
|  |         check_op_result(op_result) | ||||||
|  |         opts = kFSFileOperationDefaultOptions | ||||||
|  |         op_result = FSMoveObjectToTrashSync(byref(fp), None, opts) | ||||||
|  |         check_op_result(op_result) | ||||||
|  | |||||||
| @ -22,6 +22,7 @@ import os | |||||||
| 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: | ||||||
| @ -32,31 +33,35 @@ from .compat import text_type, environb | |||||||
| from .exceptions import TrashPermissionError | from .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 +69,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,16 +82,18 @@ 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): | ||||||
|     filename = op.basename(src) |     filename = op.basename(src) | ||||||
|     filespath = op.join(dst, FILES_DIR) |     filespath = op.join(dst, FILES_DIR) | ||||||
| @ -93,26 +102,30 @@ def trash_move(src, dst, topdir=None): | |||||||
| 
 | 
 | ||||||
|     counter = 0 |     counter = 0 | ||||||
|     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)) |     os.rename(src, op.join(filespath, destname)) | ||||||
|     f = open(op.join(infopath, destname + INFO_SUFFIX), 'w') |     f = open(op.join(infopath, destname + INFO_SUFFIX), "w") | ||||||
|     f.write(info_for(src, topdir)) |     f.write(info_for(src, topdir)) | ||||||
|     f.close() |     f.close() | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
| 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): | ||||||
|         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 +139,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 +159,54 @@ 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) |     if not isinstance(paths, list): | ||||||
|     # ...should check whether the user has the necessary permissions to delete |         paths = [paths] | ||||||
|     # it, before starting the trashing operation itself. [2] |     for path in paths: | ||||||
|     if not os.access(path_b, os.W_OK): |         if isinstance(path, text_type): | ||||||
|         raise OSError("Permission denied: %s" % path) |             path_b = fsencode(path) | ||||||
|     # if the file to be trashed is on the same device as HOMETRASH we |         elif isinstance(path, bytes): | ||||||
|     # want to move it there. |             path_b = path | ||||||
|     path_dev = get_dev(path_b) |         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 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("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("Permission denied: %s" % path) | ||||||
|  |         # if the file to be trashed is on the same device as HOMETRASH we | ||||||
|  |         # want to move it there. | ||||||
|  |         path_dev = get_dev(path_b) | ||||||
| 
 | 
 | ||||||
|     if path_dev == trash_dev: |         # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the | ||||||
|         topdir = XDG_DATA_HOME |         # home directory, and these paths will be created further on if needed. | ||||||
|         dest_trash = HOMETRASH_B |         trash_dev = get_dev(op.expanduser(b"~")) | ||||||
|     else: | 
 | ||||||
|         topdir = find_mount_point(path_b) |         if path_dev == trash_dev: | ||||||
|         trash_dev = get_dev(topdir) |             topdir = XDG_DATA_HOME | ||||||
|         if trash_dev != path_dev: |             dest_trash = HOMETRASH_B | ||||||
|             raise OSError("Couldn't find mount point for %s" % path) |         else: | ||||||
|         dest_trash = find_ext_volume_trash(topdir) |             topdir = find_mount_point(path_b) | ||||||
|     trash_move(path_b, dest_trash, topdir) |             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) | ||||||
|  |         trash_move(path_b, dest_trash, topdir) | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user