1
0
mirror of https://github.com/arsenetar/send2trash.git synced 2026-01-25 16:11:39 +00:00

5 Commits
1.4.2 ... 1.5.0

Author SHA1 Message Date
Virgil Dupras
0d7b4b4ad9 v1.5.0 2018-02-16 09:57:27 -05:00
Thomas Kluyver
1dded4f572 Raise TrashPermissionError from gio backend (#22) 2018-02-16 09:30:26 -05:00
Mickaël Schoentgen
020d05979d Windows: Workaround for long paths (#23)
By using the short path version of a file, we can
manage to move long paths to the trash.

Limitations:
1/ If the final short path is longer than what
    `SHFileOperationW` can handle, it will fail
2/ Still not able to trash long path from another
    drive, ie: trying to delete C:\temp\foo.txt
    while the script is running from D:\trash.py
2018-02-16 09:07:05 -05:00
Thomas Kluyver
6b0bd46036 Define TrashPermissionError (#21) 2018-02-06 17:28:47 -05:00
Nicholas Bollweg
f6897609ba Include LICENSE in package (#19) 2018-01-06 08:19:31 -05:00
10 changed files with 122 additions and 8 deletions

View File

@@ -1,6 +1,12 @@
Changes Changes
======= =======
Version 1.5.0 -- 2018/02/16
---------------------------
* More specific error when failing to create XDG fallback trash directory (#20)
* Windows: Workaround for long paths (#23)
Version 1.4.2 -- 2017/11/17 Version 1.4.2 -- 2017/11/17
--------------------------- ---------------------------

View File

@@ -1 +1 @@
include CHANGES.rst include CHANGES.rst LICENSE

View File

@@ -29,7 +29,15 @@ Usage
>>> from send2trash import send2trash >>> from send2trash import send2trash
>>> send2trash('some_file') >>> send2trash('some_file')
When there's a problem ``OSError`` is raised. On Freedesktop platforms (Linux, BSD, etc.), you may not be able to efficiently
trash some files. In these cases, an exception ``send2trash.TrashPermissionError``
is raised, so that the application can handle this case. This inherits from
``PermissionError`` (``OSError`` on Python 2). Specifically, this affects
files on a different device to the user's home directory, where the root of the
device does not have a ``.Trash`` directory, and we don't have permission to
create a ``.Trash-$UID`` directory.
For any other problem, ``OSError`` is raised.
.. _PyGObject: https://wiki.gnome.org/PyGObject .. _PyGObject: https://wiki.gnome.org/PyGObject
.. _GIO: https://developer.gnome.org/gio/ .. _GIO: https://developer.gnome.org/gio/

View File

@@ -6,6 +6,8 @@
import sys import sys
from .exceptions import TrashPermissionError
if sys.platform == 'darwin': if sys.platform == 'darwin':
from .plat_osx import send2trash from .plat_osx import send2trash
elif sys.platform == 'win32': elif sys.platform == 'win32':

25
send2trash/exceptions.py Normal file
View File

@@ -0,0 +1,25 @@
import errno
from .compat import PY3
if PY3:
_permission_error = PermissionError
else:
_permission_error = OSError
class TrashPermissionError(_permission_error):
"""A permission error specific to a trash directory.
Raising this error indicates that permissions prevent us efficiently
trashing a file, although we might still have permission to delete it.
This is *not* used when permissions prevent removing the file itself:
that will be raised as a regular PermissionError (OSError on Python 2).
Application code that catches this may try to simply delete the file,
or prompt the user to decide, or (on Freedesktop platforms), move it to
'home trash' as a fallback. This last option probably involves copying the
data between partitions, devices, or network drives, so we don't do it as
a fallback.
"""
def __init__(self, filename):
_permission_error.__init__(self, errno.EACCES, "Permission denied",
filename)

View File

@@ -5,10 +5,15 @@
# 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
def send2trash(path): def send2trash(path):
try: try:
f = Gio.File.new_for_path(path) f = Gio.File.new_for_path(path)
f.trash(cancellable=None) f.trash(cancellable=None)
except GObject.GError as e: except GObject.GError as e:
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) raise OSError(e.message)

View File

@@ -16,6 +16,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import errno
import sys import sys
import os import os
import os.path as op import os.path as op
@@ -28,6 +29,7 @@ except ImportError:
from urllib import quote from urllib import quote
from .compat import text_type, environb from .compat import text_type, environb
from .exceptions import TrashPermissionError
try: try:
fsencode = os.fsencode # Python 3 fsencode = os.fsencode # Python 3
@@ -134,9 +136,13 @@ def find_ext_volume_global_trash(volume_root):
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)
# Try to make the directory, if we can't the OSError exception will escape # Try to make the directory, if we lack permission, raise TrashPermissionError
# be thrown out of send2trash. try:
check_create(trash_dir) check_create(trash_dir)
except OSError as e:
if e.errno == errno.EACCES:
raise TrashPermissionError(e.filename)
raise
return trash_dir return trash_dir
def find_ext_volume_trash(volume_root): def find_ext_volume_trash(volume_root):

View File

@@ -7,15 +7,19 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from ctypes import (windll, Structure, byref, c_uint, from ctypes import (windll, Structure, byref, c_uint,
create_unicode_buffer, sizeof, addressof) create_unicode_buffer, addressof)
from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL from ctypes.wintypes import HWND, UINT, LPCWSTR, BOOL
import os.path as op import os.path as op
from .compat import text_type from .compat import text_type
kernel32 = windll.kernel32
GetShortPathNameW = kernel32.GetShortPathNameW
shell32 = windll.shell32 shell32 = windll.shell32
SHFileOperationW = shell32.SHFileOperationW SHFileOperationW = shell32.SHFileOperationW
class SHFILEOPSTRUCTW(Structure): class SHFILEOPSTRUCTW(Structure):
_fields_ = [ _fields_ = [
("hwnd", HWND), ("hwnd", HWND),
@@ -28,6 +32,7 @@ class SHFILEOPSTRUCTW(Structure):
("lpszProgressTitle", LPCWSTR), ("lpszProgressTitle", LPCWSTR),
] ]
FO_MOVE = 1 FO_MOVE = 1
FO_COPY = 2 FO_COPY = 2
FO_DELETE = 3 FO_DELETE = 3
@@ -39,11 +44,22 @@ FOF_NOCONFIRMATION = 16
FOF_ALLOWUNDO = 64 FOF_ALLOWUNDO = 64
FOF_NOERRORUI = 1024 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)
output = create_unicode_buffer(buf_size)
GetShortPathNameW(long_name, output, buf_size)
return output.value[4:] # Remove '\\?\' for SHFileOperationW
def send2trash(path): def send2trash(path):
if not isinstance(path, text_type): if not isinstance(path, text_type):
path = text_type(path, 'mbcs') path = text_type(path, 'mbcs')
if not op.isabs(path): if not op.isabs(path):
path = op.abspath(path) path = op.abspath(path)
path = get_short_path_name(path)
fileop = SHFILEOPSTRUCTW() fileop = SHFILEOPSTRUCTW()
fileop.hwnd = 0 fileop.hwnd = 0
fileop.wFunc = FO_DELETE fileop.wFunc = FO_DELETE
@@ -51,7 +67,7 @@ def send2trash(path):
# Starting in python 3.6.3 it is no longer possible to use: # Starting in python 3.6.3 it is no longer possible to use:
# LPCWSTR(path + '\0') directly as embedded null characters are no longer # LPCWSTR(path + '\0') directly as embedded null characters are no longer
# allowed in strings # allowed in strings
# Workaround # Workaround
# - create buffer of c_wchar[] (LPCWSTR is based on this type) # - create buffer of c_wchar[] (LPCWSTR is based on this type)
# - buffer is two c_wchar characters longer (double null terminator) # - buffer is two c_wchar characters longer (double null terminator)
# - cast the address of the buffer to a LPCWSTR # - cast the address of the buffer to a LPCWSTR

View File

@@ -19,7 +19,7 @@ LONG_DESCRIPTION = open('README.rst', 'rt').read() + '\n\n' + open('CHANGES.rst'
setup( setup(
name='Send2Trash', name='Send2Trash',
version='1.4.2', version='1.5.0',
author='Virgil Dupras', author='Virgil Dupras',
author_email='hsoft@hardcoded.net', author_email='hsoft@hardcoded.net',
packages=['send2trash'], packages=['send2trash'],

46
tests/test_plat_win.py Normal file
View File

@@ -0,0 +1,46 @@
# coding: utf-8
import os
import sys
import unittest
from os import path as op
from tempfile import gettempdir
from send2trash import send2trash as s2t
@unittest.skipIf(sys.platform != 'win32', 'Windows only')
class TestLongPath(unittest.TestCase):
def setUp(self):
filename = 'A' * 100
self.dirname = '\\\\?\\' + os.path.join(gettempdir(), filename)
self.file = os.path.join(
self.dirname,
filename,
filename, # From there, the path is not trashable from Explorer
filename,
filename + '.txt')
self._create_tree(self.file)
def tearDown(self):
try:
os.remove(self.dirname)
except OSError:
pass
def _create_tree(self, path):
dirname = os.path.dirname(path)
if not os.path.isdir(dirname):
os.makedirs(dirname)
with open(path, 'w') as writer:
writer.write('Looong filename!')
def test_trash_file(self):
s2t(self.file)
self.assertFalse(op.exists(self.file))
@unittest.skipIf(
op.splitdrive(os.getcwd())[0] != op.splitdrive(gettempdir())[0],
'Cannot trash long path from other drive')
def test_trash_folder(self):
s2t(self.dirname)
self.assertFalse(op.exists(self.dirname))