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
=======
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
---------------------------

View File

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

View File

@@ -29,7 +29,15 @@ Usage
>>> from send2trash import send2trash
>>> 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
.. _GIO: https://developer.gnome.org/gio/

View File

@@ -6,6 +6,8 @@
import sys
from .exceptions import TrashPermissionError
if sys.platform == 'darwin':
from .plat_osx import send2trash
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
from gi.repository import GObject, Gio
from .exceptions import TrashPermissionError
def send2trash(path):
try:
f = Gio.File.new_for_path(path)
f.trash(cancellable=None)
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)

View File

@@ -16,6 +16,7 @@
from __future__ import unicode_literals
import errno
import sys
import os
import os.path as op
@@ -28,6 +29,7 @@ except ImportError:
from urllib import quote
from .compat import text_type, environb
from .exceptions import TrashPermissionError
try:
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):
# from [2] Trash directories (1) create a .Trash-$uid dir.
trash_dir = op.join(volume_root, TOPDIR_FALLBACK)
# Try to make the directory, if we can't the OSError exception will escape
# be thrown out of send2trash.
# Try to make the directory, if we lack permission, raise TrashPermissionError
try:
check_create(trash_dir)
except OSError as e:
if e.errno == errno.EACCES:
raise TrashPermissionError(e.filename)
raise
return trash_dir
def find_ext_volume_trash(volume_root):

View File

@@ -7,15 +7,19 @@
from __future__ import unicode_literals
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
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),
@@ -28,6 +32,7 @@ class SHFILEOPSTRUCTW(Structure):
("lpszProgressTitle", LPCWSTR),
]
FO_MOVE = 1
FO_COPY = 2
FO_DELETE = 3
@@ -39,11 +44,22 @@ 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)
output = create_unicode_buffer(buf_size)
GetShortPathNameW(long_name, output, buf_size)
return output.value[4:] # Remove '\\?\' for SHFileOperationW
def send2trash(path):
if not isinstance(path, text_type):
path = text_type(path, 'mbcs')
if not op.isabs(path):
path = op.abspath(path)
path = get_short_path_name(path)
fileop = SHFILEOPSTRUCTW()
fileop.hwnd = 0
fileop.wFunc = FO_DELETE

View File

@@ -19,7 +19,7 @@ LONG_DESCRIPTION = open('README.rst', 'rt').read() + '\n\n' + open('CHANGES.rst'
setup(
name='Send2Trash',
version='1.4.2',
version='1.5.0',
author='Virgil Dupras',
author_email='hsoft@hardcoded.net',
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))