mirror of
https://github.com/arsenetar/send2trash.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7b4b4ad9 | ||
|
|
1dded4f572 | ||
|
|
020d05979d | ||
|
|
6b0bd46036 | ||
|
|
f6897609ba | ||
|
|
7d38de269a | ||
| 5733670fc2 | |||
|
|
22ed5dc09b | ||
|
|
3071684f73 |
16
CHANGES.rst
16
CHANGES.rst
@@ -1,6 +1,22 @@
|
|||||||
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
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Fix incompatibility with Python 3.6 on Windows. (#18)
|
||||||
|
|
||||||
|
Version 1.4.1 -- 2017/08/07
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Fix crash on Windows introduced in v1.4.0. Oops... (#14)
|
||||||
|
|
||||||
Version 1.4.0 -- 2017/08/07
|
Version 1.4.0 -- 2017/08/07
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
include CHANGES.rst
|
include CHANGES.rst LICENSE
|
||||||
|
|||||||
10
README.rst
10
README.rst
@@ -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/
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ PY3 = sys.version_info[0] >= 3
|
|||||||
if PY3:
|
if PY3:
|
||||||
text_type = str
|
text_type = str
|
||||||
binary_type = bytes
|
binary_type = bytes
|
||||||
environb = os.environb
|
if os.supports_bytes_environ:
|
||||||
|
# environb will be unset under Windows, but then again we're not supposed to use it.
|
||||||
|
environb = os.environb
|
||||||
else:
|
else:
|
||||||
text_type = unicode
|
text_type = unicode
|
||||||
binary_type = str
|
binary_type = str
|
||||||
|
|||||||
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
|
# 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)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -6,15 +6,20 @@
|
|||||||
|
|
||||||
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, 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),
|
||||||
@@ -27,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
|
||||||
@@ -38,15 +44,38 @@ 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
|
||||||
fileop.pFrom = LPCWSTR(path + '\0')
|
# 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'.
|
||||||
|
buffer = create_unicode_buffer(path, len(path)+2)
|
||||||
|
fileop.pFrom = LPCWSTR(addressof(buffer))
|
||||||
fileop.pTo = None
|
fileop.pTo = None
|
||||||
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
|
fileop.fFlags = FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT
|
||||||
fileop.fAnyOperationsAborted = 0
|
fileop.fAnyOperationsAborted = 0
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -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.0',
|
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
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