1
0
mirror of https://github.com/arsenetar/send2trash.git synced 2026-03-12 02:41:39 +00:00

11 Commits

Author SHA1 Message Date
d52b4f206c Fix CHANGES.rst issue 2021-06-21 22:22:48 -05:00
33171dde82 Update version for 1.7.1 release 2021-06-21 22:13:46 -05:00
077598d2ce Merge pull request #57 from BoboTiG/fix-windows-unc-names-legacy
Windows legacy: fix handling of UNC names
2021-06-21 22:06:06 -05:00
Mickaël Schoentgen
436686bf0f Windows legacy: fix handling of UNC names
The legacy implementation was not handling UNC names properly:

  Traceback (most recent call last):
    File "check.py", line 6, in <module>
      send2trash(str(file))
    File "\...\plat_win_legacy.py", line 79, in send2trash
      paths = [get_short_path_name(path) for path in paths]
    File "\...\plat_win_legacy.py", line 79, in <listcomp>
      paths = [get_short_path_name(path) for path in paths]
    File "\...\plat_win_legacy.py", line 62, in get_short_path_name
      raise WindowsError(err_no, FormatError(err_no), long_name[4:])
  OSError: [Errno 123] La syntaxe du nom de fichier, de répertoire ou de volume est incorrecte.: '\\\\SERVER\\folder\\file.txt'
2021-05-26 17:22:26 +02:00
23ce7b8c16 Bump version 2021-05-14 21:44:21 -05:00
9b0d5796c1 Change conditional for macos pyobjc usage
macOS 11.x will occasionally identify as 10.16, since there was no real
reason to prevent on all supported platforms allow.
2021-05-14 21:40:16 -05:00
c8bcaea1e8 Update version and changelog for release 2021-04-20 17:35:59 -05:00
530e9b4bc6 Add initial pyobjc version for macOS
This is to help with issue #51.  Will not help in the case of python 2 or
older python 3 version < 3.6.
2021-04-13 22:36:10 -05:00
10c7693d11 Minor fixes to tests 2021-03-17 21:51:51 -05:00
356509120b Add some checks to catch test failure
Really just checking that the setup is able to create test files so it
is known they were there then removed.
Windows tests really need verification of
recycle, which is not present.
2021-03-17 20:52:16 -05:00
f9fcdb8d8c Fix legacy windows platform for multibyte unicode
- Add handling to create correctly sized buffer even with multibyte
characters as len() in python does not line up with what
create_unicode_buffer() needs for length.
- Add test for single and multiple files
2021-03-10 21:41:30 -06:00
10 changed files with 207 additions and 61 deletions

View File

@@ -1,5 +1,26 @@
Changes Changes
======= =======
Version 1.7.1 -- 2021/06/21
---------------------------
* Release stable version with changes from last 3 releases
* Fix handling of UNC names (#57)
Version 1.7.0a1 -- 2021/05/14
-----------------------------
* Changed conditional for when to try to use pyobjc version (#51)
Version 1.7.0a0 -- 2021/04/20
-----------------------------
* Add console_script entry point (#50)
* Increased python CI versions (#52, #54)
* Fix minor issue in setup.py (#53)
* Fix issue with windows tests importing modules on non-windows (#55)
* Unit test cleanups, rewrites, and flake8 cleanups
* Windows: Fix legacy windows platform for multi-byte unicode and add tests
* macOS: Add alternative pyobjc version to potentially improve compatibility (#51)
Version 1.6.0b1 -- 2020/06/18 Version 1.6.0b1 -- 2020/06/18
----------------------------- -----------------------------

View File

@@ -4,53 +4,17 @@
# which should be included with this package. The terms are also available at # which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license # http://www.hardcoded.net/licenses/bsd_license
from __future__ import unicode_literals from platform import mac_ver
from sys import version_info
from ctypes import cdll, byref, Structure, c_char, c_char_p # NOTE: version of pyobjc only supports python >= 3.6 and 10.9+
from ctypes.util import find_library macos_ver = tuple(int(part) for part in mac_ver()[0].split("."))
if version_info >= (3, 6) and macos_ver >= (10, 9):
from .compat import binary_type try:
from .plat_osx_pyobjc import send2trash
Foundation = cdll.LoadLibrary(find_library("Foundation")) except ImportError:
CoreServices = cdll.LoadLibrary(find_library("CoreServices")) # Try to fall back to ctypes version, although likely problematic still
from .plat_osx_ctypes import send2trash
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString else:
GetMacOSStatusCommentString.restype = c_char_p # Just use the old version otherwise
FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions from .plat_osx_ctypes import send2trash # noqa: F401
FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync
kFSPathMakeRefDefaultOptions = 0
kFSPathMakeRefDoNotFollowLeafSymlink = 0x01
kFSFileOperationDefaultOptions = 0
kFSFileOperationOverwrite = 0x01
kFSFileOperationSkipSourcePermissionErrors = 0x02
kFSFileOperationDoNotMoveAcrossVolumes = 0x04
kFSFileOperationSkipPreflight = 0x08
class FSRef(Structure):
_fields_ = [("hidden", c_char * 80)]
def check_op_result(op_result):
if op_result:
msg = GetMacOSStatusCommentString(op_result).decode("utf-8")
raise OSError(msg)
def send2trash(paths):
if not isinstance(paths, list):
paths = [paths]
paths = [
path.encode("utf-8") if not isinstance(path, binary_type) else path
for path in paths
]
for path in paths:
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)

View File

@@ -0,0 +1,56 @@
# Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from __future__ import unicode_literals
from ctypes import cdll, byref, Structure, c_char, c_char_p
from ctypes.util import find_library
from .compat import binary_type
Foundation = cdll.LoadLibrary(find_library("Foundation"))
CoreServices = cdll.LoadLibrary(find_library("CoreServices"))
GetMacOSStatusCommentString = Foundation.GetMacOSStatusCommentString
GetMacOSStatusCommentString.restype = c_char_p
FSPathMakeRefWithOptions = CoreServices.FSPathMakeRefWithOptions
FSMoveObjectToTrashSync = CoreServices.FSMoveObjectToTrashSync
kFSPathMakeRefDefaultOptions = 0
kFSPathMakeRefDoNotFollowLeafSymlink = 0x01
kFSFileOperationDefaultOptions = 0
kFSFileOperationOverwrite = 0x01
kFSFileOperationSkipSourcePermissionErrors = 0x02
kFSFileOperationDoNotMoveAcrossVolumes = 0x04
kFSFileOperationSkipPreflight = 0x08
class FSRef(Structure):
_fields_ = [("hidden", c_char * 80)]
def check_op_result(op_result):
if op_result:
msg = GetMacOSStatusCommentString(op_result).decode("utf-8")
raise OSError(msg)
def send2trash(paths):
if not isinstance(paths, list):
paths = [paths]
paths = [
path.encode("utf-8") if not isinstance(path, binary_type) else path
for path in paths
]
for path in paths:
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)

View File

@@ -0,0 +1,29 @@
# Copyright 2017 Virgil Dupras
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
# which should be included with this package. The terms are also available at
# http://www.hardcoded.net/licenses/bsd_license
from Foundation import NSFileManager, NSURL
from .compat import text_type
def check_op_result(op_result):
# First value will be false on failure
if not op_result[0]:
# Error is in third value, localized failure reason matchs ctypes version
raise OSError(op_result[2].localizedFailureReason())
def send2trash(paths):
if not isinstance(paths, list):
paths = [paths]
paths = [
path.decode("utf-8") if not isinstance(path, text_type) else path
for path in paths
]
for path in paths:
file_url = NSURL.fileURLWithPath_(path)
fm = NSFileManager.defaultManager()
op_result = fm.trashItemAtURL_resultingItemURL_error_(file_url, None, None)
check_op_result(op_result)

View File

@@ -51,18 +51,53 @@ FOF_ALLOWUNDO = 64
FOF_NOERRORUI = 1024 FOF_NOERRORUI = 1024
def prefix_and_path(path):
r"""Guess the long-path prefix based on the kind of *path*.
Local paths (C:\folder\file.ext) and UNC names (\\server\folder\file.ext)
are handled.
Return a tuple of the long-path prefix and the prefixed path.
"""
prefix, long_path = "\\\\?\\", path
if not path.startswith(prefix):
if path.startswith("\\\\"):
# Likely a UNC name
prefix = "\\\\?\\UNC"
long_path = prefix + path[1:]
else:
# Likely a local path
long_path = prefix + path
elif path.startswith(prefix + "UNC\\"):
# UNC name with long-path prefix
prefix = "\\\\?\\UNC"
return prefix, long_path
def get_awaited_path_from_prefix(prefix, path):
"""Guess the correct path to pass to the SHFileOperationW() call.
The long-path prefix must be removed, so we should take care of
different long-path prefixes.
"""
if prefix == "\\\\?\\UNC":
# We need to prepend a backslash for UNC names, as it was removed
# in prefix_and_path().
return "\\" + path[len(prefix) :]
return path[len(prefix) :]
def get_short_path_name(long_name): def get_short_path_name(long_name):
if not long_name.startswith("\\\\?\\"): prefix, long_path = prefix_and_path(long_name)
long_name = "\\\\?\\" + long_name buf_size = GetShortPathNameW(long_path, None, 0)
buf_size = GetShortPathNameW(long_name, None, 0)
# FIX: https://github.com/hsoft/send2trash/issues/31 # FIX: https://github.com/hsoft/send2trash/issues/31
# If buffer size is zero, an error has occurred. # If buffer size is zero, an error has occurred.
if not buf_size: if not buf_size:
err_no = GetLastError() err_no = GetLastError()
raise WindowsError(err_no, FormatError(err_no), long_name[4:]) raise WindowsError(err_no, FormatError(err_no), long_path)
output = create_unicode_buffer(buf_size) output = create_unicode_buffer(buf_size)
GetShortPathNameW(long_name, output, buf_size) GetShortPathNameW(long_path, output, buf_size)
return output.value[4:] # Remove '\\?\' for SHFileOperationW return get_awaited_path_from_prefix(prefix, output.value)
def send2trash(paths): def send2trash(paths):
@@ -77,8 +112,6 @@ def send2trash(paths):
paths = [op.abspath(path) if not op.isabs(path) else path for path in paths] paths = [op.abspath(path) if not op.isabs(path) else path for path in paths]
# get short path to handle path length issues # get short path to handle path length issues
paths = [get_short_path_name(path) for path in paths] paths = [get_short_path_name(path) for path in paths]
# convert to a single string of null terminated paths
paths = "\0".join(paths)
fileop = SHFILEOPSTRUCTW() fileop = SHFILEOPSTRUCTW()
fileop.hwnd = 0 fileop.hwnd = 0
fileop.wFunc = FO_DELETE fileop.wFunc = FO_DELETE
@@ -93,7 +126,15 @@ def send2trash(paths):
# NOTE: based on how python allocates memory for these types they should # 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 # 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'. # setting the last two characters to null using buffer[index] = '\0'.
buffer = create_unicode_buffer(paths, len(paths) + 2) # Additional note on another issue here, unicode_buffer expects length in
# bytes essentially, so having multi-byte characters causes issues if just
# passing pythons string length. Instead of dealing with this difference we
# just create a buffer then a new one with an extra null. Since the non-length
# specified version apparently stops after the first null, join with a space first.
buffer = create_unicode_buffer(" ".join(paths))
# convert to a single string of null terminated paths
path_string = "\0".join(paths)
buffer = create_unicode_buffer(path_string, len(buffer) + 1)
fileop.pFrom = LPCWSTR(addressof(buffer)) 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

View File

@@ -24,7 +24,7 @@ with open("README.rst", "rt") as f1, open("CHANGES.rst", "rt") as f2:
setup( setup(
name="Send2Trash", name="Send2Trash",
version="1.6.0b1", version="1.7.1",
author="Andrew Senetar", author="Andrew Senetar",
author_email="arsenetar@voltaicideas.net", author_email="arsenetar@voltaicideas.net",
packages=["send2trash"], packages=["send2trash"],

View File

@@ -32,6 +32,7 @@ def testfile():
dir=op.expanduser("~"), prefix="send2trash_test", delete=False dir=op.expanduser("~"), prefix="send2trash_test", delete=False
) )
file.close() file.close()
assert op.exists(file.name) is True
yield file yield file
# Cleanup trash files on supported platforms # Cleanup trash files on supported platforms
if sys.platform != "win32": if sys.platform != "win32":
@@ -57,6 +58,7 @@ def testfiles():
) )
) )
[file.close() for file in files] [file.close() for file in files]
assert all([op.exists(file.name) for file in files]) is True
yield files yield files
filenames = [op.basename(file.name) for file in files] filenames = [op.basename(file.name) for file in files]
[os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames] [os.remove(op.join(HOMETRASH, "files", filename)) for filename in filenames]
@@ -93,6 +95,7 @@ def testUnicodefile():
name = u"send2trash_tést1" name = u"send2trash_tést1"
file = op.join(op.expanduser(b"~"), name.encode("utf-8")) file = op.join(op.expanduser(b"~"), name.encode("utf-8"))
touch(file) touch(file)
assert op.exists(file) is True
yield file yield file
# Cleanup trash files on supported platforms # Cleanup trash files on supported platforms
if sys.platform != "win32": if sys.platform != "win32":
@@ -158,6 +161,7 @@ def testExtVol():
fileName = "test.txt" fileName = "test.txt"
filePath = op.join(volume.trashTopdir, fileName) filePath = op.join(volume.trashTopdir, fileName)
touch(filePath) touch(filePath)
assert op.exists(filePath) is True
yield volume, fileName, filePath yield volume, fileName, filePath
volume.cleanup() volume.cleanup()

View File

@@ -26,6 +26,7 @@ def _create_tree(path):
@pytest.fixture @pytest.fixture
def testdir(tmp_path): def testdir(tmp_path):
dirname = "\\\\?\\" + str(tmp_path) dirname = "\\\\?\\" + str(tmp_path)
assert op.exists(dirname) is True
yield dirname yield dirname
shutil.rmtree(dirname, ignore_errors=True) shutil.rmtree(dirname, ignore_errors=True)
@@ -34,6 +35,7 @@ def testdir(tmp_path):
def testfile(testdir): def testfile(testdir):
file = op.join(testdir, "testfile.txt") file = op.join(testdir, "testfile.txt")
_create_tree(file) _create_tree(file)
assert op.exists(file) is True
yield file yield file
# Note dir will cleanup the file # Note dir will cleanup the file
@@ -42,6 +44,7 @@ def testfile(testdir):
def testfiles(testdir): def testfiles(testdir):
files = [op.join(testdir, "testfile{}.txt".format(index)) for index in range(10)] files = [op.join(testdir, "testfile{}.txt".format(index)) for index in range(10)]
[_create_tree(file) for file in files] [_create_tree(file) for file in files]
assert all([op.exists(file) for file in files]) is True
yield files yield files
# Note dir will cleanup the files # Note dir will cleanup the files
@@ -66,6 +69,19 @@ def _file_not_found(dir, fcn):
pytest.raises(OSError, fcn, file) pytest.raises(OSError, fcn, file)
def _multi_byte_unicode(dir, fcn):
single_file = op.join(dir, "😇.txt")
_create_tree(single_file)
assert op.exists(single_file) is True
fcn(single_file)
assert op.exists(single_file) is False
files = [op.join(dir, "😇{}.txt".format(index)) for index in range(10)]
[_create_tree(file) for file in files]
assert all([op.exists(file) for file in files]) is True
fcn(files)
assert any([op.exists(file) for file in files]) is False
def test_trash_folder(testdir): def test_trash_folder(testdir):
_trash_folder(testdir, s2t) _trash_folder(testdir, s2t)
@@ -98,6 +114,10 @@ def test_file_not_found_modern(testdir):
_file_not_found(testdir, s2t_modern) _file_not_found(testdir, s2t_modern)
def test_multi_byte_unicode_modern(testdir):
_multi_byte_unicode(testdir, s2t_modern)
def test_trash_folder_legacy(testdir): def test_trash_folder_legacy(testdir):
_trash_folder(testdir, s2t_legacy) _trash_folder(testdir, s2t_legacy)
@@ -114,6 +134,10 @@ def test_file_not_found_legacy(testdir):
_file_not_found(testdir, s2t_legacy) _file_not_found(testdir, s2t_legacy)
def test_multi_byte_unicode_legacy(testdir):
_multi_byte_unicode(testdir, s2t_legacy)
# Long path tests # Long path tests
@pytest.fixture @pytest.fixture
def longdir(tmp_path): def longdir(tmp_path):
@@ -129,6 +153,7 @@ def longfile(longdir):
path = op.join(longdir, name + "{}.txt") path = op.join(longdir, name + "{}.txt")
file = path.format("") file = path.format("")
_create_tree(file) _create_tree(file)
assert op.exists(file) is True
yield file yield file
@@ -138,11 +163,14 @@ def longfiles(longdir):
path = op.join(longdir, name + "{}.txt") path = op.join(longdir, name + "{}.txt")
files = [path.format(index) for index in range(10)] files = [path.format(index) for index in range(10)]
[_create_tree(file) for file in files] [_create_tree(file) for file in files]
assert all([op.exists(file) for file in files]) is True
yield files yield files
# NOTE: both legacy and modern test "pass" on windows, but actually are not moving files to the # NOTE: both legacy and modern test "pass" on windows, however sometimes with the same path
# recycle bin, this was tested on latest windows 10, thought to have worked previously # they do not actually recycle files but delete them. Noticed this when testing with the
# recycle bin open, noticed later tests actually worked, modern version can actually detect
# when this happens but not stop it at this moment, and we need a way to verify it when testing.
def test_trash_long_file_modern(longfile): def test_trash_long_file_modern(longfile):
_trash_file(longfile, s2t_modern) _trash_file(longfile, s2t_modern)

View File

@@ -18,6 +18,8 @@ def file():
dir=op.expanduser("~"), prefix="send2trash_test", delete=False dir=op.expanduser("~"), prefix="send2trash_test", delete=False
) )
file.close() file.close()
# Verify file was actually created
assert op.exists(file.name) is True
yield file.name yield file.name
# Cleanup trash files on supported platforms # Cleanup trash files on supported platforms
if sys.platform != "win32": if sys.platform != "win32":

View File

@@ -7,6 +7,7 @@ deps =
flake8 flake8
pytest pytest
pywin32; sys_platform == 'win32' pywin32; sys_platform == 'win32'
pyobjc-framework-Cocoa; sys_platform == 'darwin'
commands = commands =
flake8 flake8
pytest pytest