1- # Copyright 2013 Hardcoded Software (http://www.hardcoded.net)
1+ # Copyright 2017 Virgil Dupras
22
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
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
55# http://www.hardcoded.net/licenses/bsd_license
66
77# This is a reimplementation of plat_other.py with reference to the
1616
1717from __future__ import unicode_literals
1818
19+ import errno
1920import sys
2021import os
2122import os .path as op
2728 # Python 2
2829 from urllib import quote
2930
30- FILES_DIR = 'files'
31- INFO_DIR = 'info'
32- INFO_SUFFIX = '.trashinfo'
31+ from .compat import text_type , environb
32+ from .exceptions import TrashPermissionError
33+
34+ try :
35+ fsencode = os .fsencode # Python 3
36+ fsdecode = os .fsdecode
37+ except AttributeError :
38+ def fsencode (u ): # Python 2
39+ return u .encode (sys .getfilesystemencoding ())
40+ def fsdecode (b ):
41+ return b .decode (sys .getfilesystemencoding ())
42+ # The Python 3 versions are a bit smarter, handling surrogate escapes,
43+ # but these should work in most cases.
44+
45+ FILES_DIR = b'files'
46+ INFO_DIR = b'info'
47+ INFO_SUFFIX = b'.trashinfo'
3348
3449# Default of ~/.local/share [3]
35- XDG_DATA_HOME = op .expanduser (os .environ .get ('XDG_DATA_HOME' , '~/.local/share' ))
36- HOMETRASH = op .join (XDG_DATA_HOME , 'Trash' )
50+ XDG_DATA_HOME = op .expanduser (environb .get (b'XDG_DATA_HOME' , b'~/.local/share' ))
51+ HOMETRASH_B = op .join (XDG_DATA_HOME , b'Trash' )
52+ HOMETRASH = fsdecode (HOMETRASH_B )
3753
3854uid = os .getuid ()
39- TOPDIR_TRASH = '.Trash'
40- TOPDIR_FALLBACK = '.Trash-' + str (uid )
55+ TOPDIR_TRASH = b '.Trash'
56+ TOPDIR_FALLBACK = b '.Trash-' + text_type (uid ). encode ( 'ascii' )
4157
4258def is_parent (parent , path ):
4359 path = op .realpath (path ) # In case it's a symlink
60+ if isinstance (path , text_type ):
61+ path = fsencode (path )
4462 parent = op .realpath (parent )
63+ if isinstance (parent , text_type ):
64+ parent = fsencode (parent )
4565 return path .startswith (parent )
4666
4767def format_date (date ):
4868 return date .strftime ("%Y-%m-%dT%H:%M:%S" )
4969
5070def info_for (src , topdir ):
51- # ...it MUST not include a ".."" directory, and for files not "under" that
71+ # ...it MUST not include a ".." directory, and for files not "under" that
5272 # directory, absolute pathnames must be used. [2]
5373 if topdir is None or not is_parent (topdir , src ):
5474 src = op .abspath (src )
@@ -75,11 +95,11 @@ def trash_move(src, dst, topdir=None):
7595 destname = filename
7696 while op .exists (op .join (filespath , destname )) or op .exists (op .join (infopath , destname + INFO_SUFFIX )):
7797 counter += 1
78- destname = '%s %s%s' % ( base_name , counter , ext )
79-
98+ destname = base_name + b' ' + text_type ( counter ). encode ( 'ascii' ) + ext
99+
80100 check_create (filespath )
81101 check_create (infopath )
82-
102+
83103 os .rename (src , op .join (filespath , destname ))
84104 f = open (op .join (infopath , destname + INFO_SUFFIX ), 'w' )
85105 f .write (info_for (src , topdir ))
@@ -99,14 +119,14 @@ def find_ext_volume_global_trash(volume_root):
99119 trash_dir = op .join (volume_root , TOPDIR_TRASH )
100120 if not op .exists (trash_dir ):
101121 return None
102-
122+
103123 mode = os .lstat (trash_dir ).st_mode
104124 # vol/.Trash must be a directory, cannot be a symlink, and must have the
105125 # sticky bit set.
106126 if not op .isdir (trash_dir ) or op .islink (trash_dir ) or not (mode & stat .S_ISVTX ):
107127 return None
108128
109- trash_dir = op .join (trash_dir , str (uid ))
129+ trash_dir = op .join (trash_dir , text_type (uid ). encode ( 'ascii' ))
110130 try :
111131 check_create (trash_dir )
112132 except OSError :
@@ -116,9 +136,13 @@ def find_ext_volume_global_trash(volume_root):
116136def find_ext_volume_fallback_trash (volume_root ):
117137 # from [2] Trash directories (1) create a .Trash-$uid dir.
118138 trash_dir = op .join (volume_root , TOPDIR_FALLBACK )
119- # Try to make the directory, if we can't the OSError exception will escape
120- # be thrown out of send2trash.
121- check_create (trash_dir )
139+ # Try to make the directory, if we lack permission, raise TrashPermissionError
140+ try :
141+ check_create (trash_dir )
142+ except OSError as e :
143+ if e .errno == errno .EACCES :
144+ raise TrashPermissionError (e .filename )
145+ raise
122146 return trash_dir
123147
124148def find_ext_volume_trash (volume_root ):
@@ -132,31 +156,37 @@ def get_dev(path):
132156 return os .lstat (path ).st_dev
133157
134158def send2trash (path ):
135- if not isinstance (path , str ):
136- # path = str(path, sys.getfilesystemencoding()) # removed invalid arg passed to str function, shouldn't be used anyway
137- path = str (path )
159+ if isinstance (path , text_type ):
160+ path_b = fsencode (path )
161+ elif isinstance (path , bytes ):
162+ path_b = path
163+ elif hasattr (path , '__fspath__' ):
164+ # Python 3.6 PathLike protocol
165+ return send2trash (path .__fspath__ ())
166+ else :
167+ raise TypeError ('str, bytes or PathLike expected, not %r' % type (path ))
138168
139- if not op .exists (path ):
169+ if not op .exists (path_b ):
140170 raise OSError ("File not found: %s" % path )
141171 # ...should check whether the user has the necessary permissions to delete
142172 # it, before starting the trashing operation itself. [2]
143- if not os .access (path , os .W_OK ):
173+ if not os .access (path_b , os .W_OK ):
144174 raise OSError ("Permission denied: %s" % path )
145175 # if the file to be trashed is on the same device as HOMETRASH we
146176 # want to move it there.
147- path_dev = get_dev (path )
148-
177+ path_dev = get_dev (path_b )
178+
149179 # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
150180 # home directory, and these paths will be created further on if needed.
151- trash_dev = get_dev (op .expanduser ('~' ))
181+ trash_dev = get_dev (op .expanduser (b '~' ))
152182
153183 if path_dev == trash_dev :
154184 topdir = XDG_DATA_HOME
155- dest_trash = HOMETRASH
185+ dest_trash = HOMETRASH_B
156186 else :
157- topdir = find_mount_point (path )
187+ topdir = find_mount_point (path_b )
158188 trash_dev = get_dev (topdir )
159189 if trash_dev != path_dev :
160190 raise OSError ("Couldn't find mount point for %s" % path )
161191 dest_trash = find_ext_volume_trash (topdir )
162- trash_move (path , dest_trash , topdir )
192+ trash_move (path_b , dest_trash , topdir )
0 commit comments