The `shutil.rmtree(onerror=...)` kwarg got deprecated with py3.12. We still need to support older version of python all the way back to 3.6 so just ignore this pylint error for a while.
110 lines
3.6 KiB
Python
110 lines
3.6 KiB
Python
"""Recursive File System Removal
|
|
|
|
This module implements `rm -rf` as a python function. Its core is the
|
|
`rmtree()` function, which takes a file-system path and then recursively
|
|
deletes everything it finds on that path, until eventually the path entry
|
|
itself is dropped. This is modeled around `shutil.rmtree()`.
|
|
|
|
This function tries to be as thorough as possible. That is, it tries its best
|
|
to modify permission bits and other flags to make sure directory entries can be
|
|
removed.
|
|
"""
|
|
|
|
|
|
import os
|
|
import shutil
|
|
|
|
import osbuild.util.linux as linux
|
|
|
|
__all__ = [
|
|
"rmtree",
|
|
]
|
|
|
|
|
|
def rmtree(path: str):
|
|
"""Recursively Remove from File System
|
|
|
|
This removes the object at the given path from the file-system. It
|
|
recursively iterates through its content and removes them, before removing
|
|
the object itself.
|
|
|
|
This function is modeled around `shutil.rmtree()`, but extends its
|
|
functionality with a more aggressive approach. It tries much harder to
|
|
unlink file system objects. This includes immutable markers and more.
|
|
|
|
Note that this function can still fail. In particular, missing permissions
|
|
can always prevent this function from succeeding. However, a caller should
|
|
never assume that they can intentionally prevent this function from
|
|
succeeding. In other words, this function might be extended in any way in
|
|
the future, to be more powerful and successful in removing file system
|
|
objects.
|
|
|
|
Parameters
|
|
---------
|
|
path
|
|
A file system path pointing to the object to remove.
|
|
|
|
Raises
|
|
------
|
|
Exception
|
|
This raises the same exceptions as `shutil.rmtree()` (since that
|
|
function is used internally). Consult its documentation for details.
|
|
"""
|
|
|
|
def fixperms(p):
|
|
fd = None
|
|
try:
|
|
|
|
# if we can't open the file, we just return and let the unlink
|
|
# fail (again) with `EPERM`.
|
|
# A notable case of why open would fail is symlinks; since we
|
|
# want the symlink and not the target we pass the `O_NOFOLLOW`
|
|
# flag, but this will result in `ELOOP`, thus we never change
|
|
# symlinks. This should be fine though since "on Linux, the
|
|
# permissions of an ordinary symbolic link are not used in any
|
|
# operations"; see symlinks(7).
|
|
try:
|
|
fd = os.open(p, os.O_RDONLY | os.O_NOFOLLOW)
|
|
except OSError:
|
|
return
|
|
|
|
# The root-only immutable flag prevents files from being unlinked
|
|
# or modified. Clear it, so we can unlink the file-system tree.
|
|
try:
|
|
linux.ioctl_toggle_immutable(fd, False)
|
|
except OSError:
|
|
pass
|
|
|
|
# If we do not have sufficient permissions on a directory, we
|
|
# cannot traverse it, nor unlink its content. Make sure to set
|
|
# sufficient permissions up front.
|
|
try:
|
|
os.fchmod(fd, 0o777)
|
|
except OSError:
|
|
pass
|
|
finally:
|
|
if fd is not None:
|
|
os.close(fd)
|
|
|
|
def unlink(p):
|
|
try:
|
|
os.unlink(p)
|
|
except IsADirectoryError:
|
|
rmtree(p)
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
def on_error(_fn, p, exc_info):
|
|
e = exc_info[0]
|
|
if issubclass(e, FileNotFoundError):
|
|
pass
|
|
elif issubclass(e, PermissionError):
|
|
if p != path:
|
|
fixperms(os.path.dirname(p))
|
|
fixperms(p)
|
|
unlink(p)
|
|
else:
|
|
raise e
|
|
|
|
# "onerror" can be replaced with "onexc" once we move to python 3.12
|
|
shutil.rmtree(path, onerror=on_error) # pylint: disable=deprecated-argument
|