ThirdParty/Send2Trash/send2trash/plat_other.py

changeset 5994
cf0b37d2a28d
parent 5624
cdd346d8858b
child 6228
9c3fbf39ec9b
equal deleted inserted replaced
5993:4b2b9be7de14 5994:cf0b37d2a28d
1 # Copyright 2013 Hardcoded Software (http://www.hardcoded.net) 1 # Copyright 2017 Virgil Dupras
2 2
3 # This software is licensed under the "BSD" License as described in the "LICENSE" file, 3 # This software is licensed under the "BSD" License as described in the "LICENSE" file,
4 # which should be included with this package. The terms are also available at 4 # which should be included with this package. The terms are also available at
5 # http://www.hardcoded.net/licenses/bsd_license 5 # http://www.hardcoded.net/licenses/bsd_license
6 6
7 # This is a reimplementation of plat_other.py with reference to the 7 # This is a reimplementation of plat_other.py with reference to the
8 # freedesktop.org trash specification: 8 # freedesktop.org trash specification:
9 # [1] http://www.freedesktop.org/wiki/Specifications/trash-spec 9 # [1] http://www.freedesktop.org/wiki/Specifications/trash-spec
13 # 13 #
14 # For external volumes this implementation will raise an exception if it can't 14 # For external volumes this implementation will raise an exception if it can't
15 # find or create the user's trash directory. 15 # find or create the user's trash directory.
16 16
17 from __future__ import unicode_literals 17 from __future__ import unicode_literals
18 try:
19 str = unicode
20 except NameError:
21 pass
22 18
23 import sys 19 import sys
24 import os 20 import os
25 import os.path as op 21 import os.path as op
26 from datetime import datetime 22 from datetime import datetime
29 from urllib.parse import quote 25 from urllib.parse import quote
30 except ImportError: 26 except ImportError:
31 # Python 2 27 # Python 2
32 from urllib import quote 28 from urllib import quote
33 29
34 FILES_DIR = 'files' 30 from .compat import text_type, environb
35 INFO_DIR = 'info' 31
36 INFO_SUFFIX = '.trashinfo' 32 try:
33 fsencode = os.fsencode # Python 3
34 fsdecode = os.fsdecode
35 except AttributeError:
36 def fsencode(u): # Python 2
37 return u.encode(sys.getfilesystemencoding())
38 def fsdecode(b):
39 return b.decode(sys.getfilesystemencoding())
40 # The Python 3 versions are a bit smarter, handling surrogate escapes,
41 # but these should work in most cases.
42
43 FILES_DIR = b'files'
44 INFO_DIR = b'info'
45 INFO_SUFFIX = b'.trashinfo'
37 46
38 # Default of ~/.local/share [3] 47 # Default of ~/.local/share [3]
39 XDG_DATA_HOME = op.expanduser(os.environ.get('XDG_DATA_HOME', '~/.local/share')) 48 XDG_DATA_HOME = op.expanduser(environb.get(b'XDG_DATA_HOME', b'~/.local/share'))
40 HOMETRASH = op.join(XDG_DATA_HOME, 'Trash') 49 HOMETRASH_B = op.join(XDG_DATA_HOME, b'Trash')
50 HOMETRASH = fsdecode(HOMETRASH_B)
41 51
42 uid = os.getuid() 52 uid = os.getuid()
43 TOPDIR_TRASH = '.Trash' 53 TOPDIR_TRASH = b'.Trash'
44 TOPDIR_FALLBACK = '.Trash-' + str(uid) 54 TOPDIR_FALLBACK = b'.Trash-' + text_type(uid).encode('ascii')
45 55
46 def is_parent(parent, path): 56 def is_parent(parent, path):
47 path = op.realpath(path) # In case it's a symlink 57 path = op.realpath(path) # In case it's a symlink
58 if isinstance(path, text_type):
59 path = fsencode(path)
48 parent = op.realpath(parent) 60 parent = op.realpath(parent)
61 if isinstance(parent, text_type):
62 parent = fsencode(parent)
49 return path.startswith(parent) 63 return path.startswith(parent)
50 64
51 def format_date(date): 65 def format_date(date):
52 return date.strftime("%Y-%m-%dT%H:%M:%S") 66 return date.strftime("%Y-%m-%dT%H:%M:%S")
53 67
54 def info_for(src, topdir): 68 def info_for(src, topdir):
55 # ...it MUST not include a ".."" directory, and for files not "under" that 69 # ...it MUST not include a ".." directory, and for files not "under" that
56 # directory, absolute pathnames must be used. [2] 70 # directory, absolute pathnames must be used. [2]
57 if topdir is None or not is_parent(topdir, src): 71 if topdir is None or not is_parent(topdir, src):
58 src = op.abspath(src) 72 src = op.abspath(src)
59 else: 73 else:
60 src = op.relpath(src, topdir) 74 src = op.relpath(src, topdir)
77 91
78 counter = 0 92 counter = 0
79 destname = filename 93 destname = filename
80 while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)): 94 while op.exists(op.join(filespath, destname)) or op.exists(op.join(infopath, destname + INFO_SUFFIX)):
81 counter += 1 95 counter += 1
82 destname = '%s %s%s' % (base_name, counter, ext) 96 destname = base_name + b' ' + text_type(counter).encode('ascii') + ext
83 97
84 check_create(filespath) 98 check_create(filespath)
85 check_create(infopath) 99 check_create(infopath)
86 100
87 os.rename(src, op.join(filespath, destname)) 101 os.rename(src, op.join(filespath, destname))
88 f = open(op.join(infopath, destname + INFO_SUFFIX), 'w') 102 f = open(op.join(infopath, destname + INFO_SUFFIX), 'w')
89 f.write(info_for(src, topdir)) 103 f.write(info_for(src, topdir))
90 f.close()
91
92 # added by detlev@die-offenbachs.de to update the Trash metadata file
93 metadata = op.join(dst, "metadata")
94 entriesCount = len(os.listdir(filespath))
95 f = open(metadata, 'w')
96 f.write("[Cached]\nSize={0}\n".format(entriesCount))
97 f.close() 104 f.close()
98 105
99 def find_mount_point(path): 106 def find_mount_point(path):
100 # Even if something's wrong, "/" is a mount point, so the loop will exit. 107 # Even if something's wrong, "/" is a mount point, so the loop will exit.
101 # Use realpath in case it's a symlink 108 # Use realpath in case it's a symlink
108 # from [2] Trash directories (1) check for a .Trash dir with the right 115 # from [2] Trash directories (1) check for a .Trash dir with the right
109 # permissions set. 116 # permissions set.
110 trash_dir = op.join(volume_root, TOPDIR_TRASH) 117 trash_dir = op.join(volume_root, TOPDIR_TRASH)
111 if not op.exists(trash_dir): 118 if not op.exists(trash_dir):
112 return None 119 return None
113 120
114 mode = os.lstat(trash_dir).st_mode 121 mode = os.lstat(trash_dir).st_mode
115 # vol/.Trash must be a directory, cannot be a symlink, and must have the 122 # vol/.Trash must be a directory, cannot be a symlink, and must have the
116 # sticky bit set. 123 # sticky bit set.
117 if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX): 124 if not op.isdir(trash_dir) or op.islink(trash_dir) or not (mode & stat.S_ISVTX):
118 return None 125 return None
119 126
120 trash_dir = op.join(trash_dir, str(uid)) 127 trash_dir = op.join(trash_dir, text_type(uid).encode('ascii'))
121 try: 128 try:
122 check_create(trash_dir) 129 check_create(trash_dir)
123 except OSError: 130 except OSError:
124 return None 131 return None
125 return trash_dir 132 return trash_dir
141 # Pull this out so it's easy to stub (to avoid stubbing lstat itself) 148 # Pull this out so it's easy to stub (to avoid stubbing lstat itself)
142 def get_dev(path): 149 def get_dev(path):
143 return os.lstat(path).st_dev 150 return os.lstat(path).st_dev
144 151
145 def send2trash(path): 152 def send2trash(path):
146 if not isinstance(path, str): 153 if isinstance(path, text_type):
147 path = str(path, sys.getfilesystemencoding()) 154 path_b = fsencode(path)
148 if not op.exists(path): 155 elif isinstance(path, bytes):
156 path_b = path
157 elif hasattr(path, '__fspath__'):
158 # Python 3.6 PathLike protocol
159 return send2trash(path.__fspath__())
160 else:
161 raise TypeError('str, bytes or PathLike expected, not %r' % type(path))
162
163 if not op.exists(path_b):
149 raise OSError("File not found: %s" % path) 164 raise OSError("File not found: %s" % path)
150 # ...should check whether the user has the necessary permissions to delete 165 # ...should check whether the user has the necessary permissions to delete
151 # it, before starting the trashing operation itself. [2] 166 # it, before starting the trashing operation itself. [2]
152 if not os.access(path, os.W_OK): 167 if not os.access(path_b, os.W_OK):
153 raise OSError("Permission denied: %s" % path) 168 raise OSError("Permission denied: %s" % path)
154 # if the file to be trashed is on the same device as HOMETRASH we 169 # if the file to be trashed is on the same device as HOMETRASH we
155 # want to move it there. 170 # want to move it there.
156 path_dev = get_dev(path) 171 path_dev = get_dev(path_b)
157 172
158 # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the 173 # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
159 # home directory, and these paths will be created further on if needed. 174 # home directory, and these paths will be created further on if needed.
160 trash_dev = get_dev(op.expanduser('~')) 175 trash_dev = get_dev(op.expanduser(b'~'))
161 176
162 if path_dev == trash_dev: 177 if path_dev == trash_dev:
163 topdir = XDG_DATA_HOME 178 topdir = XDG_DATA_HOME
164 dest_trash = HOMETRASH 179 dest_trash = HOMETRASH_B
165 else: 180 else:
166 topdir = find_mount_point(path) 181 topdir = find_mount_point(path_b)
167 trash_dev = get_dev(topdir) 182 trash_dev = get_dev(topdir)
168 if trash_dev != path_dev: 183 if trash_dev != path_dev:
169 raise OSError("Couldn't find mount point for %s" % path) 184 raise OSError("Couldn't find mount point for %s" % path)
170 dest_trash = find_ext_volume_trash(topdir) 185 dest_trash = find_ext_volume_trash(topdir)
171 trash_move(path, dest_trash, topdir) 186 trash_move(path_b, dest_trash, topdir)

eric ide

mercurial