4343import shutil as _shutil
4444import errno as _errno
4545from random import Random as _Random
46+ import stat as _stat
4647import sys as _sys
4748import types as _types
4849import weakref as _weakref
@@ -273,15 +274,68 @@ def _dont_follow_symlinks(func, path, *args):
273274 elif not _os .path .islink (path ):
274275 func (path , * args )
275276
276- def _resetperms (path ):
277+ def _resetflags (path ):
277278 try :
278279 chflags = _os .chflags
279280 except AttributeError :
280281 pass
281282 else :
282283 _dont_follow_symlinks (chflags , path , 0 )
284+
285+ def _resetperms (path ):
286+ _resetflags (path )
283287 _dont_follow_symlinks (_os .chmod , path , 0o700 )
284288
289+ # True if TemporaryDirectory._rmtree() can work relative to open directories
290+ # instead of resolving paths again.
291+ _rmtree_use_dir_fd = (
292+ {_os .chmod , _os .unlink , _os .lstat } <= _os .supports_dir_fd
293+ and _os .chmod in _os .supports_fd
294+ )
295+
296+ def _resetperms_fd (dir_fd , path ):
297+ # Same as _resetperms(), but for the directory referred to by dir_fd.
298+ if dir_fd is None :
299+ _resetperms (path )
300+ return
301+ _resetflags (path )
302+ _os .chmod (dir_fd , 0o700 )
303+
304+ try :
305+ _nofollow_mode = _os .O_RDONLY | _os .O_NONBLOCK | _os .O_NOFOLLOW
306+ except AttributeError :
307+ _nofollow_mode = None
308+
309+ def _resetperms_at (name , dir_fd , path ):
310+ # Same as _resetperms(), but name is resolved relative to the directory
311+ # file descriptor dir_fd. path is only used for os.chflags(), which
312+ # doesn't support dir_fd or file descriptors.
313+ if dir_fd is None :
314+ _resetperms (path )
315+ return
316+ _resetflags (path )
317+ if _os .chmod in _os .supports_follow_symlinks :
318+ _os .chmod (name , 0o700 , dir_fd = dir_fd , follow_symlinks = False )
319+ else :
320+ # dir_fd & follow_symlinks is not supported on this platform.
321+ # Try chmod opening the file with O_NOFOLLOW.
322+ if _nofollow_mode is not None :
323+ try :
324+ fd = _os .open (name , _nofollow_mode , dir_fd = dir_fd )
325+ except OSError :
326+ pass
327+ else :
328+ try :
329+ _os .chmod (fd , 0o700 )
330+ finally :
331+ _os .close (fd )
332+ return
333+ # If that did not work, we change by name, which is subject to a race
334+ # condition.
335+ stat = _os .lstat (name , dir_fd = dir_fd )
336+ if not _stat .S_ISLNK (stat .st_mode ):
337+ _os .chmod (name , 0o700 , dir_fd = dir_fd )
338+
285339
286340# User visible interfaces.
287341
@@ -913,24 +967,43 @@ def __init__(self, suffix=None, prefix=None, dir=None,
913967 ignore_errors = self ._ignore_cleanup_errors , delete = self ._delete )
914968
915969 @classmethod
916- def _rmtree (cls , name , ignore_errors = False , repeated = False ):
917- def onexc (func , path , exc ):
970+ def _rmtree (cls , name , ignore_errors = False , repeated = False , dir_fd = None ,
971+ fullname = None ):
972+ if fullname is None :
973+ fullname = name
974+
975+ def onexc (func , path , exc , direntry = None , dir_fd = None ):
918976 # On DragonFly BSD, UF_NOUNLINK removal fails with EISDIR, not EPERM.
919977 if isinstance (exc , (PermissionError , IsADirectoryError )):
920978 if repeated and path == name :
921979 if ignore_errors :
922980 return
923981 raise
924982
983+ # fullpath is path as seen from the working directory
984+ fullpath = fullname + path [len (name ):]
985+ # base is path relative to dir_fd, the directory rmtree()
986+ # reached it through, or the whole path when there is none
987+ if dir_fd is None or not _rmtree_use_dir_fd :
988+ base , dir_fd = path , None
989+ elif direntry is None :
990+ base = path
991+ else :
992+ base = direntry .name
993+
925994 try :
926995 if path != name :
927- _resetperms (_os .path .dirname (path ))
928- _resetperms (path )
996+ # The parent directory of path is the one referred to
997+ # by dir_fd.
998+ _resetperms_fd (dir_fd , _os .path .dirname (fullpath ))
999+ _resetperms_at (base , dir_fd , fullpath )
9291000
9301001 try :
931- _os .unlink (path )
1002+ _os .unlink (base , dir_fd = dir_fd )
9321003 except IsADirectoryError :
933- cls ._rmtree (path , ignore_errors = ignore_errors )
1004+ cls ._rmtree (base , ignore_errors = ignore_errors ,
1005+ repeated = (path == name ),
1006+ dir_fd = dir_fd , fullname = fullpath )
9341007 except PermissionError :
9351008 # The PermissionError handler was originally added for
9361009 # FreeBSD in directories, but it seems that it is raised
@@ -939,21 +1012,27 @@ def onexc(func, path, exc):
9391012 # raise NotADirectoryError and mask the PermissionError.
9401013 # So we must re-raise the current PermissionError if
9411014 # path is not a directory.
942- if not _os .path .isdir (path ) or _os .path .isjunction (path ):
1015+ if (not _os .path .isdir (fullpath )
1016+ or _os .path .isjunction (fullpath )):
9431017 if ignore_errors :
9441018 return
9451019 raise
946- cls ._rmtree (path , ignore_errors = ignore_errors ,
947- repeated = (path == name ))
1020+ cls ._rmtree (base , ignore_errors = ignore_errors ,
1021+ repeated = (path == name ),
1022+ dir_fd = dir_fd , fullname = fullpath )
9481023 except FileNotFoundError :
9491024 pass
1025+ except OSError :
1026+ if ignore_errors :
1027+ return
1028+ raise
9501029 elif isinstance (exc , FileNotFoundError ):
9511030 pass
9521031 else :
9531032 if not ignore_errors :
9541033 raise
9551034
956- _shutil .rmtree (name , onexc = onexc )
1035+ _shutil .rmtree (name , onexc = onexc , dir_fd = dir_fd , _onexc_kwargs = True )
9571036
9581037 @classmethod
9591038 def _cleanup (cls , name , warn_message , ignore_errors = False , delete = True ):
0 commit comments