4444import shutil as _shutil
4545import errno as _errno
4646from random import Random as _Random
47+ import stat as _stat
4748import sys as _sys
4849import types as _types
4950import weakref as _weakref
@@ -274,15 +275,68 @@ def _dont_follow_symlinks(func, path, *args):
274275 elif not _os .path .islink (path ):
275276 func (path , * args )
276277
277- def _resetperms (path ):
278+ def _resetflags (path ):
278279 try :
279280 chflags = _os .chflags
280281 except AttributeError :
281282 pass
282283 else :
283284 _dont_follow_symlinks (chflags , path , 0 )
285+
286+ def _resetperms (path ):
287+ _resetflags (path )
284288 _dont_follow_symlinks (_os .chmod , path , 0o700 )
285289
290+ # True if TemporaryDirectory._rmtree() can work relative to open directories
291+ # instead of resolving paths again.
292+ _rmtree_use_dir_fd = (
293+ {_os .chmod , _os .unlink , _os .lstat } <= _os .supports_dir_fd
294+ and _os .chmod in _os .supports_fd
295+ )
296+
297+ def _resetperms_fd (dir_fd , path ):
298+ # Same as _resetperms(), but for the directory referred to by dir_fd.
299+ if dir_fd is None :
300+ _resetperms (path )
301+ return
302+ _resetflags (path )
303+ _os .chmod (dir_fd , 0o700 )
304+
305+ try :
306+ _nofollow_mode = _os .O_RDONLY | _os .O_NONBLOCK | _os .O_NOFOLLOW
307+ except AttributeError :
308+ _nofollow_mode = None
309+
310+ def _resetperms_at (name , dir_fd , path ):
311+ # Same as _resetperms(), but name is resolved relative to the directory
312+ # file descriptor dir_fd. path is only used for os.chflags(), which
313+ # doesn't support dir_fd or file descriptors.
314+ if dir_fd is None :
315+ _resetperms (path )
316+ return
317+ _resetflags (path )
318+ if _os .chmod in _os .supports_follow_symlinks :
319+ _os .chmod (name , 0o700 , dir_fd = dir_fd , follow_symlinks = False )
320+ else :
321+ # dir_fd & follow_symlinks is not supported on this platform.
322+ # Try chmod opening the file with O_NOFOLLOW.
323+ if _nofollow_mode is not None :
324+ try :
325+ fd = _os .open (name , _nofollow_mode , dir_fd = dir_fd )
326+ except OSError :
327+ pass
328+ else :
329+ try :
330+ _os .chmod (fd , 0o700 )
331+ finally :
332+ _os .close (fd )
333+ return
334+ # If that did not work, we change by name, which is subject to a race
335+ # condition.
336+ stat = _os .lstat (name , dir_fd = dir_fd )
337+ if not _stat .S_ISLNK (stat .st_mode ):
338+ _os .chmod (name , 0o700 , dir_fd = dir_fd )
339+
286340
287341# User visible interfaces.
288342
@@ -927,24 +981,43 @@ def __init__(self, suffix=None, prefix=None, dir=None,
927981 ignore_errors = self ._ignore_cleanup_errors , delete = self ._delete )
928982
929983 @classmethod
930- def _rmtree (cls , name , ignore_errors = False , repeated = False ):
931- def onexc (func , path , exc ):
984+ def _rmtree (cls , name , ignore_errors = False , repeated = False , dir_fd = None ,
985+ fullname = None ):
986+ if fullname is None :
987+ fullname = name
988+
989+ def onexc (func , path , exc , direntry = None , dir_fd = None ):
932990 # On DragonFly BSD, UF_NOUNLINK removal fails with EISDIR, not EPERM.
933991 if isinstance (exc , (PermissionError , IsADirectoryError )):
934992 if repeated and path == name :
935993 if ignore_errors :
936994 return
937995 raise
938996
997+ # fullpath is path as seen from the working directory
998+ fullpath = fullname + path [len (name ):]
999+ # base is path relative to dir_fd, the directory rmtree()
1000+ # reached it through, or the whole path when there is none
1001+ if dir_fd is None or not _rmtree_use_dir_fd :
1002+ base , dir_fd = path , None
1003+ elif direntry is None :
1004+ base = path
1005+ else :
1006+ base = direntry .name
1007+
9391008 try :
9401009 if path != name :
941- _resetperms (_os .path .dirname (path ))
942- _resetperms (path )
1010+ # The parent directory of path is the one referred to
1011+ # by dir_fd.
1012+ _resetperms_fd (dir_fd , _os .path .dirname (fullpath ))
1013+ _resetperms_at (base , dir_fd , fullpath )
9431014
9441015 try :
945- _os .unlink (path )
1016+ _os .unlink (base , dir_fd = dir_fd )
9461017 except IsADirectoryError :
947- cls ._rmtree (path , ignore_errors = ignore_errors )
1018+ cls ._rmtree (base , ignore_errors = ignore_errors ,
1019+ repeated = (path == name ),
1020+ dir_fd = dir_fd , fullname = fullpath )
9481021 except PermissionError :
9491022 # The PermissionError handler was originally added for
9501023 # FreeBSD in directories, but it seems that it is raised
@@ -953,21 +1026,27 @@ def onexc(func, path, exc):
9531026 # raise NotADirectoryError and mask the PermissionError.
9541027 # So we must re-raise the current PermissionError if
9551028 # path is not a directory.
956- if not _os .path .isdir (path ) or _os .path .isjunction (path ):
1029+ if (not _os .path .isdir (fullpath )
1030+ or _os .path .isjunction (fullpath )):
9571031 if ignore_errors :
9581032 return
9591033 raise
960- cls ._rmtree (path , ignore_errors = ignore_errors ,
961- repeated = (path == name ))
1034+ cls ._rmtree (base , ignore_errors = ignore_errors ,
1035+ repeated = (path == name ),
1036+ dir_fd = dir_fd , fullname = fullpath )
9621037 except FileNotFoundError :
9631038 pass
1039+ except OSError :
1040+ if ignore_errors :
1041+ return
1042+ raise
9641043 elif isinstance (exc , FileNotFoundError ):
9651044 pass
9661045 else :
9671046 if not ignore_errors :
9681047 raise
9691048
970- _shutil .rmtree (name , onexc = onexc )
1049+ _shutil .rmtree (name , onexc = onexc , dir_fd = dir_fd , _onexc_kwargs = True )
9711050
9721051 @classmethod
9731052 def _cleanup (cls , name , warn_message , ignore_errors = False , delete = True ):
0 commit comments