1- # Copyright 2010 Hardcoded Software (http://www.hardcoded.net)
1+ # Copyright 2017 Virgil Dupras
22
33# This software is licensed under the "BSD" License as described in the "LICENSE" file,
44# which should be included with this package. The terms are also available at
1414# For external volumes this implementation will raise an exception if it can't
1515# find or create the user's trash directory.
1616
17- # import sys
17+ from __future__ import unicode_literals
18+
19+ import errno
20+ import sys
1821import os
1922import os .path as op
2023from datetime import datetime
2124import stat
22- import shutil
23- from urllib .parse import quote
24-
25- FILES_DIR = "files"
26- INFO_DIR = "info"
27- INFO_SUFFIX = ".trashinfo"
25+ try :
26+ from urllib .parse import quote
27+ except ImportError :
28+ # Python 2
29+ from urllib import quote
30+
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'
2848
2949# Default of ~/.local/share [3]
30- XDG_DATA_HOME = op .expanduser (os .environ .get ("XDG_DATA_HOME" , "~/.local/share" ))
31- 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 )
3253
3354uid = os .getuid ()
34- TOPDIR_TRASH = ".Trash"
35- TOPDIR_FALLBACK = ".Trash-" + str (uid )
36-
55+ TOPDIR_TRASH = b'.Trash'
56+ TOPDIR_FALLBACK = b'.Trash-' + text_type (uid ).encode ('ascii' )
3757
3858def is_parent (parent , path ):
39- path = op .realpath (path ) # In case it's a symlink
59+ path = op .realpath (path ) # In case it's a symlink
60+ if isinstance (path , text_type ):
61+ path = fsencode (path )
4062 parent = op .realpath (parent )
63+ if isinstance (parent , text_type ):
64+ parent = fsencode (parent )
4165 return path .startswith (parent )
4266
43-
4467def format_date (date ):
4568 return date .strftime ("%Y-%m-%dT%H:%M:%S" )
4669
47-
4870def info_for (src , topdir ):
49- # ...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
5072 # directory, absolute pathnames must be used. [2]
5173 if topdir is None or not is_parent (topdir , src ):
5274 src = op .abspath (src )
5375 else :
5476 src = op .relpath (src , topdir )
5577
56- info = "[Trash Info]\n "
78+ info = "[Trash Info]\n "
5779 info += "Path=" + quote (src ) + "\n "
5880 info += "DeletionDate=" + format_date (datetime .now ()) + "\n "
5981 return info
6082
61-
6283def check_create (dir ):
6384 # use 0700 for paths [3]
6485 if not op .exists (dir ):
6586 os .makedirs (dir , 0o700 )
6687
67-
6888def trash_move (src , dst , topdir = None ):
6989 filename = op .basename (src )
7090 filespath = op .join (dst , FILES_DIR )
@@ -73,32 +93,26 @@ def trash_move(src, dst, topdir=None):
7393
7494 counter = 0
7595 destname = filename
76- while op .exists (op .join (filespath , destname )) or op .exists (
77- op .join (infopath , destname + INFO_SUFFIX )
78- ):
96+ while op .exists (op .join (filespath , destname )) or op .exists (op .join (infopath , destname + INFO_SUFFIX )):
7997 counter += 1
80- destname = "%s %s%s" % ( base_name , counter , ext )
98+ destname = base_name + b' ' + text_type ( counter ). encode ( 'ascii' ) + ext
8199
82100 check_create (filespath )
83101 check_create (infopath )
84- try :
85- os .rename (src , op .join (filespath , destname ))
86- except :
87- shutil .move (src , op .join (filespath , destname ))
88- f = open (op .join (infopath , destname + INFO_SUFFIX ), "w" )
102+
103+ os .rename (src , op .join (filespath , destname ))
104+ f = open (op .join (infopath , destname + INFO_SUFFIX ), 'w' )
89105 f .write (info_for (src , topdir ))
90106 f .close ()
91107
92-
93108def find_mount_point (path ):
94109 # Even if something's wrong, "/" is a mount point, so the loop will exit.
95110 # Use realpath in case it's a symlink
96- path = op .realpath (path ) # Required to avoid infinite loop
111+ path = op .realpath (path ) # Required to avoid infinite loop
97112 while not op .ismount (path ):
98113 path = op .split (path )[0 ]
99114 return path
100115
101-
102116def find_ext_volume_global_trash (volume_root ):
103117 # from [2] Trash directories (1) check for a .Trash dir with the right
104118 # permissions set.
@@ -112,61 +126,67 @@ def find_ext_volume_global_trash(volume_root):
112126 if not op .isdir (trash_dir ) or op .islink (trash_dir ) or not (mode & stat .S_ISVTX ):
113127 return None
114128
115- trash_dir = op .join (trash_dir , str (uid ))
129+ trash_dir = op .join (trash_dir , text_type (uid ). encode ( 'ascii' ))
116130 try :
117131 check_create (trash_dir )
118132 except OSError :
119133 return None
120134 return trash_dir
121135
122-
123136def find_ext_volume_fallback_trash (volume_root ):
124137 # from [2] Trash directories (1) create a .Trash-$uid dir.
125138 trash_dir = op .join (volume_root , TOPDIR_FALLBACK )
126- # Try to make the directory, if we can't the OSError exception will escape
127- # be thrown out of send2trash.
128- 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
129146 return trash_dir
130147
131-
132148def find_ext_volume_trash (volume_root ):
133149 trash_dir = find_ext_volume_global_trash (volume_root )
134150 if trash_dir is None :
135151 trash_dir = find_ext_volume_fallback_trash (volume_root )
136152 return trash_dir
137153
138-
139154# Pull this out so it's easy to stub (to avoid stubbing lstat itself)
140155def get_dev (path ):
141156 return os .lstat (path ).st_dev
142157
143-
144158def send2trash (path ):
145- # if not isinstance(path, str):
146- # path = str(path, sys.getfilesystemencoding())
147- # if not op.exists(path):
148- # raise OSError("File not found: %s" % 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 ))
168+
169+ if not op .exists (path_b ):
170+ raise OSError ("File not found: %s" % path )
149171 # ...should check whether the user has the necessary permissions to delete
150172 # it, before starting the trashing operation itself. [2]
151- # if not os.access(path , os.W_OK):
152- # raise OSError("Permission denied: %s" % path)
173+ if not os .access (path_b , os .W_OK ):
174+ raise OSError ("Permission denied: %s" % path )
153175 # if the file to be trashed is on the same device as HOMETRASH we
154176 # want to move it there.
155- path_dev = get_dev (path )
177+ path_dev = get_dev (path_b )
156178
157179 # If XDG_DATA_HOME or HOMETRASH do not yet exist we need to stat the
158180 # home directory, and these paths will be created further on if needed.
159- trash_dev = get_dev (op .expanduser ("~" ))
181+ trash_dev = get_dev (op .expanduser (b'~' ))
160182
161- if path_dev == trash_dev or (
162- os .path .exists (XDG_DATA_HOME ) and os .path .exists (HOMETRASH )
163- ):
183+ if path_dev == trash_dev :
164184 topdir = XDG_DATA_HOME
165- dest_trash = HOMETRASH
185+ dest_trash = HOMETRASH_B
166186 else :
167- topdir = find_mount_point (path )
187+ topdir = find_mount_point (path_b )
168188 trash_dev = get_dev (topdir )
169189 if trash_dev != path_dev :
170190 raise OSError ("Couldn't find mount point for %s" % path )
171191 dest_trash = find_ext_volume_trash (topdir )
172- trash_move (path , dest_trash , topdir )
192+ trash_move (path_b , dest_trash , topdir )
0 commit comments