mirror of
https://github.com/arsenetar/send2trash.git
synced 2026-01-25 16:11:39 +00:00
Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75a9cd6b55 | ||
|
|
f38aec6569 | ||
|
|
072e28cb1e | ||
|
|
016b90c1ac | ||
|
|
f324ff491e | ||
|
|
f3231ef857 | ||
|
|
b7e3057853 | ||
|
|
7fece243d8 | ||
|
|
4181ed65e9 | ||
|
|
dd69edad3b | ||
|
|
bd9183afe9 | ||
|
|
f6f63b1796 | ||
|
|
0974912e78 | ||
|
|
6c01453fd3 | ||
|
|
7cbefa4317 | ||
|
|
72bc94b48d | ||
|
|
35ad95bcd5 | ||
|
|
a568370c6a | ||
|
|
baf125ff61 | ||
|
|
bb8ed834da | ||
|
|
a8dbb1ac63 | ||
|
|
bfd8f6e024 | ||
|
|
8996fb9eac | ||
|
|
f7a6f217ce | ||
|
|
a4936be846 |
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
*.pyc
|
||||||
|
*.egg-info
|
||||||
|
/build
|
||||||
|
/dist
|
||||||
|
.DS_Store
|
||||||
|
/.tox
|
||||||
|
__pycache__
|
||||||
3
.hgtags
3
.hgtags
@@ -1,3 +0,0 @@
|
|||||||
48c2103380f5e7deca49364f44fb31ded9942bb7 1.0.0
|
|
||||||
a7e04d8e47e161daaa1f031a7c1e98c52fa269ac 1.0.1
|
|
||||||
de5f43fcce5e776eb28306951939afb9946cbd3d 1.1.0
|
|
||||||
10
.travis.yml
Normal file
10
.travis.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
language: python
|
||||||
|
python:
|
||||||
|
- "2.7"
|
||||||
|
- "3.4"
|
||||||
|
- "3.5"
|
||||||
|
- "3.6"
|
||||||
|
install:
|
||||||
|
- "pip install tox"
|
||||||
|
script:
|
||||||
|
- "tox -e $(echo py$TRAVIS_PYTHON_VERSION | tr -d .)"
|
||||||
@@ -1,6 +1,23 @@
|
|||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
Version 1.4.0 -- 2017/08/07
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Use ``bytes`` instead of ``str`` for internal path handling in ``plat_other``. (#13)
|
||||||
|
|
||||||
|
Version 1.3.1 -- 2017/07/31
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Throw ``WindowsError`` instead of ``OSError`` in ``plat_win``. (#7)
|
||||||
|
* Fix ``TypeError`` on python 2 in ``plat_other``. (#12)
|
||||||
|
|
||||||
|
Version 1.3.0 -- 2013/07/19
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* Added support for Gnome's GIO.
|
||||||
|
* Merged Python 3 and Python 2 versions in a single codebase.
|
||||||
|
|
||||||
Version 1.2.0 -- 2011/03/16
|
Version 1.2.0 -- 2011/03/16
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
@@ -24,4 +41,4 @@ Version 1.0.1 -- 2010/04/19
|
|||||||
Version 1.0.0 -- 2010/04/07
|
Version 1.0.0 -- 2010/04/07
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
* Initial Release
|
* Initial Release
|
||||||
4
LICENSE
4
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2010, Hardcoded Software Inc., http://www.hardcoded.net
|
Copyright (c) 2017, Virgil Dupras
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
@@ -7,4 +7,4 @@ Redistribution and use in source and binary forms, with or without modification,
|
|||||||
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
|
||||||
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
* Neither the name of Hardcoded Software Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
include CHANGES
|
include CHANGES.rst
|
||||||
25
README
25
README
@@ -1,25 +0,0 @@
|
|||||||
==================================================
|
|
||||||
Send2Trash -- Send files to trash on all platforms
|
|
||||||
==================================================
|
|
||||||
|
|
||||||
This is a Python 3 package. The Python 2 package is at http://pypi.python.org/pypi/Send2Trash .
|
|
||||||
|
|
||||||
Send2Trash is a small package that sends files to the Trash (or Recycle Bin) *natively* and on
|
|
||||||
*all platforms*. On OS X, it uses native ``FSMoveObjectToTrashSync`` Cocoa calls, on Windows, it uses native (and ugly) ``SHFileOperation`` win32 calls. On other platforms, it follows the trash specifications from freedesktop.org.
|
|
||||||
|
|
||||||
``ctypes`` is used to access native libraries, so no compilation is necessary.
|
|
||||||
|
|
||||||
Installation
|
|
||||||
------------
|
|
||||||
|
|
||||||
Download the source from http://hg.hardcoded.net/send2trash and install it with::
|
|
||||||
|
|
||||||
>>> python setup.py install
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
>>> from send2trash import send2trash
|
|
||||||
>>> send2trash('some_file')
|
|
||||||
|
|
||||||
When there's a problem ``OSError`` is raised.
|
|
||||||
36
README.rst
Normal file
36
README.rst
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
==================================================
|
||||||
|
Send2Trash -- Send files to trash on all platforms
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Send2Trash is a small package that sends files to the Trash (or Recycle Bin) *natively* and on
|
||||||
|
*all platforms*. On OS X, it uses native ``FSMoveObjectToTrashSync`` Cocoa calls, on Windows, it
|
||||||
|
uses native (and ugly) ``SHFileOperation`` win32 calls. On other platforms, if `PyGObject`_ and
|
||||||
|
`GIO`_ are available, it will use this. Otherwise, it will fallback to its own implementation
|
||||||
|
of the `trash specifications from freedesktop.org`_.
|
||||||
|
|
||||||
|
``ctypes`` is used to access native libraries, so no compilation is necessary.
|
||||||
|
|
||||||
|
Send2Trash supports Python 2.7 and up (Python 3 is supported).
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
You can download it with pip::
|
||||||
|
|
||||||
|
pip install Send2Trash
|
||||||
|
|
||||||
|
or you can download the source from http://github.com/hsoft/send2trash and install it with::
|
||||||
|
|
||||||
|
>>> python setup.py install
|
||||||
|
|
||||||
|
Usage
|
||||||
|
-----
|
||||||
|
|
||||||
|
>>> from send2trash import send2trash
|
||||||
|
>>> send2trash('some_file')
|
||||||
|
|
||||||
|
When there's a problem ``OSError`` is raised.
|
||||||
|
|
||||||
|
.. _PyGObject: https://wiki.gnome.org/PyGObject
|
||||||
|
.. _GIO: https://developer.gnome.org/gio/
|
||||||
|
.. _trash specifications from freedesktop.org: http://freedesktop.org/wiki/Specifications/trash-spec/
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
|
||||||
|
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# 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
|
# which should be included with this package. The terms are also available at
|
||||||
@@ -11,4 +11,9 @@ if sys.platform == 'darwin':
|
|||||||
elif sys.platform == 'win32':
|
elif sys.platform == 'win32':
|
||||||
from .plat_win import send2trash
|
from .plat_win import send2trash
|
||||||
else:
|
else:
|
||||||
from .plat_other import send2trash
|
try:
|
||||||
|
# If we can use gio, let's use it
|
||||||
|
from .plat_gio import send2trash
|
||||||
|
except ImportError:
|
||||||
|
# Oh well, let's fallback to our own Freedesktop trash implementation
|
||||||
|
from .plat_other import send2trash
|
||||||
|
|||||||
18
send2trash/compat.py
Normal file
18
send2trash/compat.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
PY3 = sys.version_info[0] >= 3
|
||||||
|
if PY3:
|
||||||
|
text_type = str
|
||||||
|
binary_type = bytes
|
||||||
|
environb = os.environb
|
||||||
|
else:
|
||||||
|
text_type = unicode
|
||||||
|
binary_type = str
|
||||||
|
environb = os.environ
|
||||||
14
send2trash/plat_gio.py
Normal file
14
send2trash/plat_gio.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# 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 gi.repository import GObject, Gio
|
||||||
|
|
||||||
|
def send2trash(path):
|
||||||
|
try:
|
||||||
|
f = Gio.File.new_for_path(path)
|
||||||
|
f.trash(cancellable=None)
|
||||||
|
except GObject.GError as e:
|
||||||
|
raise OSError(e.message)
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2017 Virgil Dupras
|
||||||
|
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# 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
|
# 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 ctypes import cdll, byref, Structure, c_char, c_char_p
|
from ctypes import cdll, byref, Structure, c_char, c_char_p
|
||||||
from ctypes.util import find_library
|
from ctypes.util import find_library
|
||||||
|
|
||||||
|
from .compat import binary_type
|
||||||
|
|
||||||
Foundation = cdll.LoadLibrary(find_library('Foundation'))
|
Foundation = cdll.LoadLibrary(find_library('Foundation'))
|
||||||
CoreServices = cdll.LoadLibrary(find_library('CoreServices'))
|
CoreServices = cdll.LoadLibrary(find_library('CoreServices'))
|
||||||
|
|
||||||
@@ -33,7 +37,7 @@ def check_op_result(op_result):
|
|||||||
raise OSError(msg)
|
raise OSError(msg)
|
||||||
|
|
||||||
def send2trash(path):
|
def send2trash(path):
|
||||||
if not isinstance(path, bytes):
|
if not isinstance(path, binary_type):
|
||||||
path = path.encode('utf-8')
|
path = path.encode('utf-8')
|
||||||
fp = FSRef()
|
fp = FSRef()
|
||||||
opts = kFSPathMakeRefDoNotFollowLeafSymlink
|
opts = kFSPathMakeRefDoNotFollowLeafSymlink
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2017 Virgil Dupras
|
||||||
|
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# 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
|
# 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
|
||||||
|
|
||||||
# This is a reimplementation of plat_other.py with reference to the
|
# This is a reimplementation of plat_other.py with reference to the
|
||||||
@@ -14,35 +14,59 @@
|
|||||||
# For external volumes this implementation will raise an exception if it can't
|
# For external volumes this implementation will raise an exception if it can't
|
||||||
# find or create the user's trash directory.
|
# find or create the user's trash directory.
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import os.path as op
|
import os.path as op
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import stat
|
import stat
|
||||||
from urllib.parse import quote
|
try:
|
||||||
|
from urllib.parse import quote
|
||||||
|
except ImportError:
|
||||||
|
# Python 2
|
||||||
|
from urllib import quote
|
||||||
|
|
||||||
FILES_DIR = 'files'
|
from .compat import text_type, environb
|
||||||
INFO_DIR = 'info'
|
|
||||||
INFO_SUFFIX = '.trashinfo'
|
try:
|
||||||
|
fsencode = os.fsencode # Python 3
|
||||||
|
fsdecode = os.fsdecode
|
||||||
|
except AttributeError:
|
||||||
|
def fsencode(u): # Python 2
|
||||||
|
return u.encode(sys.getfilesystemencoding())
|
||||||
|
def fsdecode(b):
|
||||||
|
return b.decode(sys.getfilesystemencoding())
|
||||||
|
# The Python 3 versions are a bit smarter, handling surrogate escapes,
|
||||||
|
# but these should work in most cases.
|
||||||
|
|
||||||
|
FILES_DIR = b'files'
|
||||||
|
INFO_DIR = b'info'
|
||||||
|
INFO_SUFFIX = b'.trashinfo'
|
||||||
|
|
||||||
# Default of ~/.local/share [3]
|
# Default of ~/.local/share [3]
|
||||||
XDG_DATA_HOME = op.expanduser(os.environ.get('XDG_DATA_HOME', '~/.local/share'))
|
XDG_DATA_HOME = op.expanduser(environb.get(b'XDG_DATA_HOME', b'~/.local/share'))
|
||||||
HOMETRASH = op.join(XDG_DATA_HOME, 'Trash')
|
HOMETRASH_B = op.join(XDG_DATA_HOME, b'Trash')
|
||||||
|
HOMETRASH = fsdecode(HOMETRASH_B)
|
||||||
|
|
||||||
uid = os.getuid()
|
uid = os.getuid()
|
||||||
TOPDIR_TRASH = '.Trash'
|
TOPDIR_TRASH = b'.Trash'
|
||||||
TOPDIR_FALLBACK = '.Trash-' + str(uid)
|
TOPDIR_FALLBACK = b'.Trash-' + text_type(uid).encode('ascii')
|
||||||
|
|
||||||
def is_parent(parent, path):
|
def is_parent(parent, path):
|
||||||
path = op.realpath(path) # In case it's a symlink
|
path = op.realpath(path) # In case it's a symlink
|
||||||
|
if isinstance(path, text_type):
|
||||||
|
path = fsencode(path)
|
||||||
parent = op.realpath(parent)
|
parent = op.realpath(parent)
|
||||||
|
if isinstance(parent, text_type):
|
||||||
|
parent = fsencode(parent)
|
||||||
return path.startswith(parent)
|
return path.startswith(parent)
|
||||||
|
|
||||||
def format_date(date):
|
def format_date(date):
|
||||||
return date.strftime("%Y-%m-%dT%H:%M:%S")
|
return date.strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
|
|
||||||
def info_for(src, topdir):
|
def info_for(src, topdir):
|
||||||
# ...it MUST not include a ".."" directory, and for files not "under" that
|
# ...it MUST not include a ".." directory, and for files not "under" that
|
||||||
# directory, absolute pathnames must be used. [2]
|
# directory, absolute pathnames must be used. [2]
|
||||||
if topdir is None or not is_parent(topdir, src):
|
if topdir is None or not is_parent(topdir, src):
|
||||||
src = op.abspath(src)
|
src = op.abspath(src)
|
||||||
@@ -69,11 +93,11 @@ def trash_move(src, dst, topdir=None):
|
|||||||
destname = filename
|
destname = filename
|
||||||
while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
|
while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
|
||||||
counter += 1
|
counter += 1
|
||||||
destname = '%s %s%s' % (base_name, counter, ext)
|
destname = base_name + b' ' + text_type(counter).encode('ascii') + ext
|
||||||
|
|
||||||
check_create(filespath)
|
check_create(filespath)
|
||||||
check_create(infopath)
|
check_create(infopath)
|
||||||
|
|
||||||
os.rename(src, op.join(filespath, destname))
|
os.rename(src, op.join(filespath, destname))
|
||||||
f = open(op.join(infopath, destname + INFO_SUFFIX), 'w')
|
f = open(op.join(infopath, destname + INFO_SUFFIX), 'w')
|
||||||
f.write(info_for(src, topdir))
|
f.write(info_for(src, topdir))
|
||||||
@@ -93,14 +117,14 @@ def find_ext_volume_global_trash(volume_root):
|
|||||||
trash_dir = op.join(volume_root, TOPDIR_TRASH)
|
trash_dir = op.join(volume_root, TOPDIR_TRASH)
|
||||||
if not op.exists(trash_dir):
|
if not op.exists(trash_dir):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
mode = os.lstat(trash_dir).st_mode
|
mode = os.lstat(trash_dir).st_mode
|
||||||
# vol/.Trash must be a directory, cannot be a symlink, and must have the
|
# vol/.Trash must be a directory, cannot be a symlink, and must have the
|
||||||
# sticky bit set.
|
# sticky bit set.
|
||||||
if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
|
if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
trash_dir = op.join(trash_dir, str(uid))
|
trash_dir = op.join(trash_dir, text_type(uid).encode('ascii'))
|
||||||
try:
|
try:
|
||||||
check_create(trash_dir)
|
check_create(trash_dir)
|
||||||
except OSError:
|
except OSError:
|
||||||
@@ -126,29 +150,37 @@ def get_dev(path):
|
|||||||
return os.lstat(path).st_dev
|
return os.lstat(path).st_dev
|
||||||
|
|
||||||
def send2trash(path):
|
def send2trash(path):
|
||||||
if not isinstance(path, str):
|
if isinstance(path, text_type):
|
||||||
path = str(path, sys.getfilesystemencoding())
|
path_b = fsencode(path)
|
||||||
if not op.exists(path):
|
elif isinstance(path, bytes):
|
||||||
|
path_b = path
|
||||||
|
elif hasattr(path, '__fspath__'):
|
||||||
|
# Python 3.6 PathLike protocol
|
||||||
|
return send2trash(path.__fspath__())
|
||||||
|
else:
|
||||||
|
raise TypeError('str, bytes or PathLike expected, not %r' % type(path))
|
||||||
|
|
||||||
|
if not op.exists(path_b):
|
||||||
raise OSError("File not found: %s" % path)
|
raise OSError("File not found: %s" % path)
|
||||||
# ...should check whether the user has the necessary permissions to delete
|
# ...should check whether the user has the necessary permissions to delete
|
||||||
# it, before starting the trashing operation itself. [2]
|
# it, before starting the trashing operation itself. [2]
|
||||||
if not os.access(path, os.W_OK):
|
if not os.access(path_b, os.W_OK):
|
||||||
raise OSError("Permission denied: %s" % path)
|
raise OSError("Permission denied: %s" % path)
|
||||||
# if the file to be trashed is on the same device as HOMETRASH we
|
# if the file to be trashed is on the same device as HOMETRASH we
|
||||||
# want to move it there.
|
# want to move it there.
|
||||||
path_dev = get_dev(path)
|
path_dev = get_dev(path_b)
|
||||||
|
|
||||||
# If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
|
# If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
|
||||||
# home directory, and these paths will be created further on if needed.
|
# home directory, and these paths will be created further on if needed.
|
||||||
trash_dev = get_dev(op.expanduser('~'))
|
trash_dev = get_dev(op.expanduser(b'~'))
|
||||||
|
|
||||||
if path_dev == trash_dev:
|
if path_dev == trash_dev:
|
||||||
topdir = XDG_DATA_HOME
|
topdir = XDG_DATA_HOME
|
||||||
dest_trash = HOMETRASH
|
dest_trash = HOMETRASH_B
|
||||||
else:
|
else:
|
||||||
topdir = find_mount_point(path)
|
topdir = find_mount_point(path_b)
|
||||||
trash_dev = get_dev(topdir)
|
trash_dev = get_dev(topdir)
|
||||||
if trash_dev != path_dev:
|
if trash_dev != path_dev:
|
||||||
raise OSError("Couldn't find mount point for %s" % path)
|
raise OSError("Couldn't find mount point for %s" % path)
|
||||||
dest_trash = find_ext_volume_trash(topdir)
|
dest_trash = find_ext_volume_trash(topdir)
|
||||||
trash_move(path, dest_trash, topdir)
|
trash_move(path_b, dest_trash, topdir)
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
# Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
|
# Copyright 2017 Virgil Dupras
|
||||||
|
|
||||||
# This software is licensed under the "BSD" License as described in the "LICENSE" file,
|
# 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
|
# 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 ctypes import windll, Structure, byref, c_uint
|
from ctypes import windll, Structure, byref, c_uint
|
||||||
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
|
||||||
|
|
||||||
shell32 = windll.shell32
|
shell32 = windll.shell32
|
||||||
SHFileOperationW = shell32.SHFileOperationW
|
SHFileOperationW = shell32.SHFileOperationW
|
||||||
|
|
||||||
@@ -35,8 +39,8 @@ FOF_ALLOWUNDO = 64
|
|||||||
FOF_NOERRORUI = 1024
|
FOF_NOERRORUI = 1024
|
||||||
|
|
||||||
def send2trash(path):
|
def send2trash(path):
|
||||||
if not isinstance(path, str):
|
if not isinstance(path, text_type):
|
||||||
path = str(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)
|
||||||
fileop = SHFILEOPSTRUCTW()
|
fileop = SHFILEOPSTRUCTW()
|
||||||
@@ -50,6 +54,4 @@ def send2trash(path):
|
|||||||
fileop.lpszProgressTitle = None
|
fileop.lpszProgressTitle = None
|
||||||
result = SHFileOperationW(byref(fileop))
|
result = SHFileOperationW(byref(fileop))
|
||||||
if result:
|
if result:
|
||||||
msg = "Couldn't perform operation. Error code: %d" % result
|
raise WindowsError(None, None, path, result)
|
||||||
raise OSError(msg)
|
|
||||||
|
|
||||||
|
|||||||
21
setup.py
21
setup.py
@@ -1,6 +1,3 @@
|
|||||||
import sys
|
|
||||||
import os.path as op
|
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
CLASSIFIERS = [
|
CLASSIFIERS = [
|
||||||
@@ -10,23 +7,27 @@ CLASSIFIERS = [
|
|||||||
'Operating System :: MacOS :: MacOS X',
|
'Operating System :: MacOS :: MacOS X',
|
||||||
'Operating System :: Microsoft :: Windows',
|
'Operating System :: Microsoft :: Windows',
|
||||||
'Operating System :: POSIX',
|
'Operating System :: POSIX',
|
||||||
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
|
'Programming Language :: Python :: 3.4',
|
||||||
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
'Topic :: Desktop Environment :: File Managers',
|
'Topic :: Desktop Environment :: File Managers',
|
||||||
]
|
]
|
||||||
|
|
||||||
LONG_DESCRIPTION = open('README', 'rt').read() + '\n\n' + open('CHANGES', 'rt').read()
|
LONG_DESCRIPTION = open('README.rst', 'rt').read() + '\n\n' + open('CHANGES.rst', 'rt').read()
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Send2Trash3k',
|
name='Send2Trash',
|
||||||
version='1.2.0',
|
version='1.4.0',
|
||||||
author='Hardcoded Software',
|
author='Virgil Dupras',
|
||||||
author_email='hsoft@hardcoded.net',
|
author_email='hsoft@hardcoded.net',
|
||||||
packages=['send2trash'],
|
packages=['send2trash'],
|
||||||
scripts=[],
|
scripts=[],
|
||||||
url='http://hg.hardcoded.net/send2trash/',
|
test_suite='tests',
|
||||||
|
url='https://github.com/hsoft/send2trash',
|
||||||
license='BSD License',
|
license='BSD License',
|
||||||
description='Send file to trash natively under Mac OS X, Windows and Linux.',
|
description='Send file to trash natively under Mac OS X, Windows and Linux.',
|
||||||
long_description=LONG_DESCRIPTION,
|
long_description=LONG_DESCRIPTION,
|
||||||
classifiers=CLASSIFIERS,
|
classifiers=CLASSIFIERS,
|
||||||
zip_safe=False,
|
)
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,131 +0,0 @@
|
|||||||
import unittest
|
|
||||||
import os
|
|
||||||
from os import path as op
|
|
||||||
import send2trash.plat_other
|
|
||||||
from send2trash.plat_other import send2trash as s2t
|
|
||||||
from configparser import ConfigParser
|
|
||||||
from tempfile import mkdtemp, NamedTemporaryFile, mktemp
|
|
||||||
import shutil
|
|
||||||
import stat
|
|
||||||
# Could still use cleaning up. But no longer relies on ramfs.
|
|
||||||
|
|
||||||
def touch(path):
|
|
||||||
with open(path, 'a'):
|
|
||||||
os.utime(path, None)
|
|
||||||
|
|
||||||
class TestHomeTrash(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.file = NamedTemporaryFile(dir=op.expanduser("~"),
|
|
||||||
prefix='send2trash_test', delete=False)
|
|
||||||
|
|
||||||
def test_trash(self):
|
|
||||||
s2t(self.file.name)
|
|
||||||
self.assertFalse(op.exists(self.file.name))
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
hometrash = send2trash.plat_other.HOMETRASH
|
|
||||||
name = op.basename(self.file.name)
|
|
||||||
os.remove(op.join(hometrash, 'files', name))
|
|
||||||
os.remove(op.join(hometrash, 'info', name+'.trashinfo'))
|
|
||||||
|
|
||||||
#
|
|
||||||
# Tests for files on some other volume than the user's home directory.
|
|
||||||
#
|
|
||||||
# What we need to stub:
|
|
||||||
# * plat_other.get_dev (to make sure the file will not be on the home dir dev)
|
|
||||||
# * os.path.ismount (to make our topdir look like a top dir)
|
|
||||||
#
|
|
||||||
class TestExtVol(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
self.trashTopdir = mkdtemp(prefix='s2t')
|
|
||||||
self.fileName = 'test.txt'
|
|
||||||
self.filePath = op.join(self.trashTopdir, self.fileName)
|
|
||||||
touch(self.filePath)
|
|
||||||
|
|
||||||
self.old_ismount = old_ismount = op.ismount
|
|
||||||
self.old_getdev = send2trash.plat_other.get_dev
|
|
||||||
def s_getdev(path):
|
|
||||||
from send2trash.plat_other import is_parent
|
|
||||||
st = os.lstat(path)
|
|
||||||
if is_parent(self.trashTopdir, path):
|
|
||||||
return 'dev'
|
|
||||||
return st
|
|
||||||
def s_ismount(path):
|
|
||||||
if op.realpath(path) == op.realpath(self.trashTopdir):
|
|
||||||
return True
|
|
||||||
return old_ismount(path)
|
|
||||||
|
|
||||||
send2trash.plat_other.os.path.ismount = s_ismount
|
|
||||||
send2trash.plat_other.get_dev = s_getdev
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
send2trash.plat_other.get_dev = self.old_getdev
|
|
||||||
send2trash.plat_other.os.path.ismount = self.old_ismount
|
|
||||||
shutil.rmtree(self.trashTopdir)
|
|
||||||
|
|
||||||
class TestTopdirTrash(TestExtVol):
|
|
||||||
def setUp(self):
|
|
||||||
TestExtVol.setUp(self)
|
|
||||||
# Create a .Trash dir w/ a sticky bit
|
|
||||||
self.trashDir = op.join(self.trashTopdir, '.Trash')
|
|
||||||
os.mkdir(self.trashDir, 0o777|stat.S_ISVTX)
|
|
||||||
|
|
||||||
def test_trash(self):
|
|
||||||
s2t(self.filePath)
|
|
||||||
self.assertFalse(op.exists(self.filePath))
|
|
||||||
self.assertTrue(op.exists(op.join(self.trashDir, str(os.getuid()), 'files', self.fileName)))
|
|
||||||
self.assertTrue(op.exists(op.join(self.trashDir, str(os.getuid()), 'info', self.fileName + '.trashinfo')))
|
|
||||||
# info relative path (if another test is added, with the same fileName/Path,
|
|
||||||
# then it gets renamed etc.)
|
|
||||||
cfg = ConfigParser()
|
|
||||||
cfg.read(op.join(self.trashDir, str(os.getuid()), 'info', self.fileName + '.trashinfo'))
|
|
||||||
self.assertEqual(self.fileName, cfg.get('Trash Info', 'Path', 1))
|
|
||||||
|
|
||||||
# Test .Trash-UID
|
|
||||||
class TestTopdirTrashFallback(TestExtVol):
|
|
||||||
def test_trash(self):
|
|
||||||
touch(self.filePath)
|
|
||||||
s2t(self.filePath)
|
|
||||||
self.assertFalse(op.exists(self.filePath))
|
|
||||||
self.assertTrue(op.exists(op.join(self.trashTopdir, '.Trash-' + str(os.getuid()), 'files', self.fileName)))
|
|
||||||
|
|
||||||
# Test failure
|
|
||||||
class TestTopdirFailure(TestExtVol):
|
|
||||||
def setUp(self):
|
|
||||||
TestExtVol.setUp(self)
|
|
||||||
os.chmod(self.trashTopdir, 0o500) # not writable to induce the exception
|
|
||||||
|
|
||||||
def test_trash(self):
|
|
||||||
with self.assertRaises(OSError):
|
|
||||||
s2t(self.filePath)
|
|
||||||
self.assertTrue(op.exists(self.filePath))
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
os.chmod(self.trashTopdir, 0o700) # writable to allow deletion
|
|
||||||
TestExtVol.tearDown(self)
|
|
||||||
|
|
||||||
# Make sure it will find the mount point properly for a file in a symlinked path
|
|
||||||
class TestSymlink(TestExtVol):
|
|
||||||
def setUp(self):
|
|
||||||
TestExtVol.setUp(self)
|
|
||||||
# Use mktemp (race conditioney but no symlink equivalent)
|
|
||||||
# Since is_parent uses realpath(), and our getdev uses is_parent,
|
|
||||||
# this should work
|
|
||||||
self.slDir = mktemp(prefix='s2t', dir=op.expanduser('~'))
|
|
||||||
|
|
||||||
os.mkdir(op.join(self.trashTopdir, 'subdir'), 0o700)
|
|
||||||
self.filePath = op.join(self.trashTopdir, 'subdir', self.fileName)
|
|
||||||
touch(self.filePath)
|
|
||||||
os.symlink(op.join(self.trashTopdir, 'subdir'), self.slDir)
|
|
||||||
|
|
||||||
def test_trash(self):
|
|
||||||
s2t(op.join(self.slDir, self.fileName))
|
|
||||||
self.assertFalse(op.exists(self.filePath))
|
|
||||||
self.assertTrue(op.exists(op.join(self.trashTopdir, '.Trash-' + str(os.getuid()), 'files', self.fileName)))
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
os.remove(self.slDir)
|
|
||||||
TestExtVol.tearDown(self)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
unittest.main()
|
|
||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
170
tests/test_plat_other.py
Normal file
170
tests/test_plat_other.py
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
# encoding: utf-8
|
||||||
|
import codecs
|
||||||
|
import unittest
|
||||||
|
import os
|
||||||
|
from os import path as op
|
||||||
|
import send2trash.plat_other
|
||||||
|
from send2trash.plat_other import send2trash as s2t
|
||||||
|
from send2trash.compat import PY3
|
||||||
|
from configparser import ConfigParser
|
||||||
|
from tempfile import mkdtemp, NamedTemporaryFile, mktemp
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
|
# Could still use cleaning up. But no longer relies on ramfs.
|
||||||
|
|
||||||
|
HOMETRASH = send2trash.plat_other.HOMETRASH
|
||||||
|
|
||||||
|
def touch(path):
|
||||||
|
with open(path, 'a'):
|
||||||
|
os.utime(path, None)
|
||||||
|
|
||||||
|
class TestHomeTrash(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.file = NamedTemporaryFile(
|
||||||
|
dir=op.expanduser("~"), prefix='send2trash_test', delete=False)
|
||||||
|
|
||||||
|
def test_trash(self):
|
||||||
|
s2t(self.file.name)
|
||||||
|
self.assertFalse(op.exists(self.file.name))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
name = op.basename(self.file.name)
|
||||||
|
os.remove(op.join(HOMETRASH, 'files', name))
|
||||||
|
os.remove(op.join(HOMETRASH, 'info', name+'.trashinfo'))
|
||||||
|
|
||||||
|
|
||||||
|
def _filesys_enc():
|
||||||
|
enc = sys.getfilesystemencoding()
|
||||||
|
# Get canonical name of codec
|
||||||
|
return codecs.lookup(enc).name
|
||||||
|
|
||||||
|
@unittest.skipIf(_filesys_enc() == 'ascii', 'ASCII filesystem')
|
||||||
|
class TestUnicodeTrash(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.name = u'send2trash_tést1'
|
||||||
|
self.file = op.join(op.expanduser(b'~'), self.name.encode('utf-8'))
|
||||||
|
touch(self.file)
|
||||||
|
|
||||||
|
def test_trash_bytes(self):
|
||||||
|
s2t(self.file)
|
||||||
|
assert not op.exists(self.file)
|
||||||
|
|
||||||
|
def test_trash_unicode(self):
|
||||||
|
s2t(self.file.decode(sys.getfilesystemencoding()))
|
||||||
|
assert not op.exists(self.file)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if op.exists(self.file):
|
||||||
|
os.remove(self.file)
|
||||||
|
|
||||||
|
trash_file = op.join(HOMETRASH, 'files', self.name)
|
||||||
|
if op.exists(trash_file):
|
||||||
|
os.remove(trash_file)
|
||||||
|
os.remove(op.join(HOMETRASH, 'info', self.name+'.trashinfo'))
|
||||||
|
|
||||||
|
#
|
||||||
|
# Tests for files on some other volume than the user's home directory.
|
||||||
|
#
|
||||||
|
# What we need to stub:
|
||||||
|
# * plat_other.get_dev (to make sure the file will not be on the home dir dev)
|
||||||
|
# * os.path.ismount (to make our topdir look like a top dir)
|
||||||
|
#
|
||||||
|
class TestExtVol(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.trashTopdir = mkdtemp(prefix='s2t')
|
||||||
|
if PY3:
|
||||||
|
trashTopdir_b = os.fsencode(self.trashTopdir)
|
||||||
|
else:
|
||||||
|
trashTopdir_b = self.trashTopdir
|
||||||
|
self.fileName = 'test.txt'
|
||||||
|
self.filePath = op.join(self.trashTopdir, self.fileName)
|
||||||
|
touch(self.filePath)
|
||||||
|
|
||||||
|
self.old_ismount = old_ismount = op.ismount
|
||||||
|
self.old_getdev = send2trash.plat_other.get_dev
|
||||||
|
def s_getdev(path):
|
||||||
|
from send2trash.plat_other import is_parent
|
||||||
|
st = os.lstat(path)
|
||||||
|
if is_parent(self.trashTopdir, path):
|
||||||
|
return 'dev'
|
||||||
|
return st.st_dev
|
||||||
|
def s_ismount(path):
|
||||||
|
if op.realpath(path) in (op.realpath(self.trashTopdir), op.realpath(trashTopdir_b)):
|
||||||
|
return True
|
||||||
|
return old_ismount(path)
|
||||||
|
|
||||||
|
send2trash.plat_other.os.path.ismount = s_ismount
|
||||||
|
send2trash.plat_other.get_dev = s_getdev
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
send2trash.plat_other.get_dev = self.old_getdev
|
||||||
|
send2trash.plat_other.os.path.ismount = self.old_ismount
|
||||||
|
shutil.rmtree(self.trashTopdir)
|
||||||
|
|
||||||
|
class TestTopdirTrash(TestExtVol):
|
||||||
|
def setUp(self):
|
||||||
|
TestExtVol.setUp(self)
|
||||||
|
# Create a .Trash dir w/ a sticky bit
|
||||||
|
self.trashDir = op.join(self.trashTopdir, '.Trash')
|
||||||
|
os.mkdir(self.trashDir, 0o777|stat.S_ISVTX)
|
||||||
|
|
||||||
|
def test_trash(self):
|
||||||
|
s2t(self.filePath)
|
||||||
|
self.assertFalse(op.exists(self.filePath))
|
||||||
|
self.assertTrue(op.exists(op.join(self.trashDir, str(os.getuid()), 'files', self.fileName)))
|
||||||
|
self.assertTrue(op.exists(op.join(self.trashDir, str(os.getuid()), 'info', self.fileName + '.trashinfo')))
|
||||||
|
# info relative path (if another test is added, with the same fileName/Path,
|
||||||
|
# then it gets renamed etc.)
|
||||||
|
cfg = ConfigParser()
|
||||||
|
cfg.read(op.join(self.trashDir, str(os.getuid()), 'info', self.fileName + '.trashinfo'))
|
||||||
|
self.assertEqual(self.fileName, cfg.get('Trash Info', 'Path', raw=True))
|
||||||
|
|
||||||
|
# Test .Trash-UID
|
||||||
|
class TestTopdirTrashFallback(TestExtVol):
|
||||||
|
def test_trash(self):
|
||||||
|
touch(self.filePath)
|
||||||
|
s2t(self.filePath)
|
||||||
|
self.assertFalse(op.exists(self.filePath))
|
||||||
|
self.assertTrue(op.exists(op.join(self.trashTopdir, '.Trash-' + str(os.getuid()), 'files', self.fileName)))
|
||||||
|
|
||||||
|
# Test failure
|
||||||
|
class TestTopdirFailure(TestExtVol):
|
||||||
|
def setUp(self):
|
||||||
|
TestExtVol.setUp(self)
|
||||||
|
os.chmod(self.trashTopdir, 0o500) # not writable to induce the exception
|
||||||
|
|
||||||
|
def test_trash(self):
|
||||||
|
with self.assertRaises(OSError):
|
||||||
|
s2t(self.filePath)
|
||||||
|
self.assertTrue(op.exists(self.filePath))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.chmod(self.trashTopdir, 0o700) # writable to allow deletion
|
||||||
|
TestExtVol.tearDown(self)
|
||||||
|
|
||||||
|
# Make sure it will find the mount point properly for a file in a symlinked path
|
||||||
|
class TestSymlink(TestExtVol):
|
||||||
|
def setUp(self):
|
||||||
|
TestExtVol.setUp(self)
|
||||||
|
# Use mktemp (race conditioney but no symlink equivalent)
|
||||||
|
# Since is_parent uses realpath(), and our getdev uses is_parent,
|
||||||
|
# this should work
|
||||||
|
self.slDir = mktemp(prefix='s2t', dir=op.expanduser('~'))
|
||||||
|
|
||||||
|
os.mkdir(op.join(self.trashTopdir, 'subdir'), 0o700)
|
||||||
|
self.filePath = op.join(self.trashTopdir, 'subdir', self.fileName)
|
||||||
|
touch(self.filePath)
|
||||||
|
os.symlink(op.join(self.trashTopdir, 'subdir'), self.slDir)
|
||||||
|
|
||||||
|
def test_trash(self):
|
||||||
|
s2t(op.join(self.slDir, self.fileName))
|
||||||
|
self.assertFalse(op.exists(self.filePath))
|
||||||
|
self.assertTrue(op.exists(op.join(self.trashTopdir, '.Trash-' + str(os.getuid()), 'files', self.fileName)))
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
os.remove(self.slDir)
|
||||||
|
TestExtVol.tearDown(self)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user