bpo-46245: Add optional parameter dir_fd in shutil.rmtree() (GH-30365)

This commit is contained in:
Serhiy Storchaka 2022-03-09 14:29:33 +02:00 committed by GitHub
parent 5eb03b1b51
commit 02fbaf4887
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 49 additions and 7 deletions

View File

@ -286,7 +286,7 @@ Directory and files operations
.. versionadded:: 3.8
The *dirs_exist_ok* parameter.
.. function:: rmtree(path, ignore_errors=False, onerror=None)
.. function:: rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None)
.. index:: single: directory; deleting
@ -296,6 +296,9 @@ Directory and files operations
handled by calling a handler specified by *onerror* or, if that is omitted,
they raise an exception.
This function can support :ref:`paths relative to directory descriptors
<dir_fd>`.
.. note::
On platforms that support the necessary fd-based functions a symlink
@ -315,7 +318,7 @@ Directory and files operations
*excinfo*, will be the exception information returned by
:func:`sys.exc_info`. Exceptions raised by *onerror* will not be caught.
.. audit-event:: shutil.rmtree path shutil.rmtree
.. audit-event:: shutil.rmtree path,dir_fd shutil.rmtree
.. versionchanged:: 3.3
Added a symlink attack resistant version that is used automatically
@ -325,6 +328,9 @@ Directory and files operations
On Windows, will no longer delete the contents of a directory junction
before removing the junction.
.. versionchanged:: 3.11
The *dir_fd* parameter.
.. attribute:: rmtree.avoids_symlink_attacks
Indicates whether the current platform and implementation provides a

View File

@ -283,6 +283,13 @@ os
(Contributed by Dong-hee Na in :issue:`44611`.)
shutil
------
* Add optional parameter *dir_fd* in :func:`shutil.rmtree`.
(Contributed by Serhiy Storchaka in :issue:`46245`.)
socket
------

View File

@ -684,9 +684,14 @@ _use_fd_functions = ({os.open, os.stat, os.unlink, os.rmdir} <=
os.scandir in os.supports_fd and
os.stat in os.supports_follow_symlinks)
def rmtree(path, ignore_errors=False, onerror=None):
def rmtree(path, ignore_errors=False, onerror=None, *, dir_fd=None):
"""Recursively delete a directory tree.
If dir_fd is not None, it should be a file descriptor open to a directory;
path will then be relative to that directory.
dir_fd may not be implemented on your platform.
If it is unavailable, using it will raise a NotImplementedError.
If ignore_errors is set, errors are ignored; otherwise, if onerror
is set, it is called to handle the error with arguments (func,
path, exc_info) where func is platform and implementation dependent;
@ -695,7 +700,7 @@ def rmtree(path, ignore_errors=False, onerror=None):
is false and onerror is None, an exception is raised.
"""
sys.audit("shutil.rmtree", path)
sys.audit("shutil.rmtree", path, dir_fd)
if ignore_errors:
def onerror(*args):
pass
@ -709,12 +714,12 @@ def rmtree(path, ignore_errors=False, onerror=None):
# Note: To guard against symlink races, we use the standard
# lstat()/open()/fstat() trick.
try:
orig_st = os.lstat(path)
orig_st = os.lstat(path, dir_fd=dir_fd)
except Exception:
onerror(os.lstat, path, sys.exc_info())
return
try:
fd = os.open(path, os.O_RDONLY)
fd = os.open(path, os.O_RDONLY, dir_fd=dir_fd)
fd_closed = False
except Exception:
onerror(os.open, path, sys.exc_info())
@ -725,7 +730,7 @@ def rmtree(path, ignore_errors=False, onerror=None):
try:
os.close(fd)
fd_closed = True
os.rmdir(path)
os.rmdir(path, dir_fd=dir_fd)
except OSError:
onerror(os.rmdir, path, sys.exc_info())
else:
@ -738,6 +743,8 @@ def rmtree(path, ignore_errors=False, onerror=None):
if not fd_closed:
os.close(fd)
else:
if dir_fd is not None:
raise NotImplementedError("dir_fd unavailable on this platform")
try:
if _rmtree_islink(path):
# symlinks to directories are forbidden, see bug #1669

View File

@ -405,6 +405,27 @@ class TestRmTree(BaseTest, unittest.TestCase):
self.assertFalse(shutil._use_fd_functions)
self.assertFalse(shutil.rmtree.avoids_symlink_attacks)
@unittest.skipUnless(shutil._use_fd_functions, "dir_fd is not supported")
def test_rmtree_with_dir_fd(self):
tmp_dir = self.mkdtemp()
victim = 'killme'
fullname = os.path.join(tmp_dir, victim)
dir_fd = os.open(tmp_dir, os.O_RDONLY)
self.addCleanup(os.close, dir_fd)
os.mkdir(fullname)
os.mkdir(os.path.join(fullname, 'subdir'))
write_file(os.path.join(fullname, 'subdir', 'somefile'), 'foo')
self.assertTrue(os.path.exists(fullname))
shutil.rmtree(victim, dir_fd=dir_fd)
self.assertFalse(os.path.exists(fullname))
@unittest.skipIf(shutil._use_fd_functions, "dir_fd is supported")
def test_rmtree_with_dir_fd_unsupported(self):
tmp_dir = self.mkdtemp()
with self.assertRaises(NotImplementedError):
shutil.rmtree(tmp_dir, dir_fd=0)
self.assertTrue(os.path.exists(tmp_dir))
def test_rmtree_dont_delete_file(self):
# When called on a file instead of a directory, don't delete it.
handle, path = tempfile.mkstemp(dir=self.mkdtemp())

View File

@ -0,0 +1 @@
Add optional parameter *dir_fd* in :func:`shutil.rmtree`.