mirror of
https://github.com/arsenetar/send2trash.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7b4b4ad9 | ||
|
|
1dded4f572 | ||
|
|
020d05979d | ||
|
|
6b0bd46036 | ||
|
|
f6897609ba |
@@ -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
|
||||
---------------------------
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
include CHANGES.rst
|
||||
include CHANGES.rst LICENSE
|
||||
|
||||
10
README.rst
10
README.rst
@@ -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/
|
||||
|
||||
@@ -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
25
send2trash/exceptions.py
Normal 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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
check_create(trash_dir)
|
||||
# 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):
|
||||
|
||||
@@ -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
|
||||
@@ -51,7 +67,7 @@ def send2trash(path):
|
||||
# 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
|
||||
# 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
|
||||
|
||||
2
setup.py
2
setup.py
@@ -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
46
tests/test_plat_win.py
Normal 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))
|
||||
Reference in New Issue
Block a user