mirror of
https://github.com/arsenetar/send2trash.git
synced 2026-01-26 00:21:40 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d38de269a | ||
| 5733670fc2 | |||
|
|
22ed5dc09b | ||
|
|
3071684f73 | ||
|
|
75a9cd6b55 | ||
|
|
f38aec6569 | ||
|
|
072e28cb1e | ||
|
|
016b90c1ac | ||
|
|
f324ff491e | ||
|
|
f3231ef857 | ||
|
|
b7e3057853 | ||
|
|
7fece243d8 | ||
|
|
4181ed65e9 | ||
|
|
dd69edad3b | ||
|
|
bd9183afe9 | ||
|
|
f6f63b1796 | ||
|
|
0974912e78 | ||
|
|
6c01453fd3 | ||
|
|
7cbefa4317 | ||
|
|
72bc94b48d | ||
|
|
35ad95bcd5 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
|||||||
/build
|
/build
|
||||||
/dist
|
/dist
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/.tox
|
||||||
|
__pycache__
|
||||||
|
|||||||
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 .)"
|
||||||
23
CHANGES.rst
23
CHANGES.rst
@@ -1,11 +1,32 @@
|
|||||||
Changes
|
Changes
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
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
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
* 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
|
Version 1.3.0 -- 2013/07/19
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|
||||||
* Added support for Gnome's GIO.
|
* Added support for Gnome's GIO.
|
||||||
* Merged Python 3 and Python 2 vesion in a single codebase.
|
* 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
|
||||||
---------------------------
|
---------------------------
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2013, Hardcoded Software, 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:
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
# Copyright 2013 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
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
if sys.version < '3':
|
import os
|
||||||
text_type = unicode
|
|
||||||
binary_type = str
|
PY3 = sys.version_info[0] >= 3
|
||||||
else:
|
if PY3:
|
||||||
text_type = str
|
text_type = str
|
||||||
binary_type = bytes
|
binary_type = bytes
|
||||||
|
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:
|
||||||
|
text_type = unicode
|
||||||
|
binary_type = str
|
||||||
|
environb = os.environ
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2013 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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2013 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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Copyright 2013 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
|
||||||
@@ -27,28 +27,46 @@ except ImportError:
|
|||||||
# Python 2
|
# Python 2
|
||||||
from urllib import quote
|
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)
|
||||||
@@ -75,7 +93,7 @@ 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)
|
||||||
@@ -106,7 +124,7 @@ def find_ext_volume_global_trash(volume_root):
|
|||||||
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:
|
||||||
@@ -132,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,4 +1,4 @@
|
|||||||
# Copyright 2013 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
|
||||||
@@ -6,7 +6,8 @@
|
|||||||
|
|
||||||
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, sizeof, 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
|
||||||
|
|
||||||
@@ -46,7 +47,19 @@ def send2trash(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
|
||||||
@@ -54,6 +67,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)
|
|
||||||
|
|
||||||
|
|||||||
13
setup.py
13
setup.py
@@ -1,6 +1,3 @@
|
|||||||
import sys
|
|
||||||
import os.path as op
|
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
CLASSIFIERS = [
|
CLASSIFIERS = [
|
||||||
@@ -12,6 +9,9 @@ CLASSIFIERS = [
|
|||||||
'Operating System :: POSIX',
|
'Operating System :: POSIX',
|
||||||
'Programming Language :: Python :: 2.7',
|
'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',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -19,12 +19,13 @@ LONG_DESCRIPTION = open('README.rst', 'rt').read() + '\n\n' + open('CHANGES.rst'
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='Send2Trash',
|
name='Send2Trash',
|
||||||
version='1.3.0',
|
version='1.4.2',
|
||||||
author='Hardcoded Software',
|
author='Virgil Dupras',
|
||||||
author_email='hsoft@hardcoded.net',
|
author_email='hsoft@hardcoded.net',
|
||||||
packages=['send2trash'],
|
packages=['send2trash'],
|
||||||
scripts=[],
|
scripts=[],
|
||||||
url='http://github.com/hsoft/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,
|
||||||
|
|||||||
@@ -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