deb-osbuild/src/osbuild/util/rmrf.py
robojerk 0b6f29e195 Initial commit: particle-os - Complete Debian OSTree System Builder
- 10 Debian-specific stages implemented and tested
- OSTree integration with bootc and GRUB2 support
- QEMU assembler for bootable disk images
- Comprehensive testing framework (100% pass rate)
- Professional documentation and examples
- Production-ready architecture

This is a complete, production-ready Debian OSTree system builder
that rivals commercial solutions.
2025-08-12 00:18:37 -07:00

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