git_policy_cli/subvolume/subvolume_remove.rs
1//! What:
2//! Remove a directory tree, making each directory the user owns writable on the way down, and never going into a nested
3//! subvolume or a mount point.
4//! Why:
5//! The migration preserves permission bits, so a checkout can hold a mode-555 directory with files in it. Unlinking a
6//! file needs write permission on its directory, so `std::fs::remove_dir_all` stops at the first such directory and
7//! both the deletion of the old tree and the emptying of a subvolume fail partway. Every directory is opened below its
8//! parent's descriptor without following links and given the owner's read, write and search bits through its
9//! descriptor before the tree is removed; a directory the user does not own keeps its mode and the removal reports the
10//! operating system's refusal. A nested subvolume or a mount point inside the tree is not part of it (to the kernel it is
11//! a directory, and `remove_dir_all` would delete what it holds), so the removal refuses before it deletes anything
12//! (`subvolume_remove_walk.rs`).
13//!
14//! In TS you'd write (pseudocode):
15//! ```ts
16//! await rm(path, { recursive: true, force: true }); // after chmod u+rwx on every directory, never into a nested subvolume
17//! ```
18
19use crate::fs_beneath::{open_directory_widening, remove_at};
20use crate::subvolume_remove_walk::{empty_directory, verify_below};
21use std::path::Path;
22
23/// What:
24/// Remove `path`, whatever it is: a file or link is unlinked, a directory is emptied and removed.
25/// Why:
26/// Links are never followed; see the module description for the permissions and the boundaries. The empty directory
27/// that remains is removed by name from the parent that was opened first, which succeeds only if it is still empty.
28///
29/// In TS you'd write (pseudocode):
30/// ```ts
31/// await removeTree(path);
32/// ```
33pub fn remove_tree(path: &Path) -> std::io::Result<()> {
34 let metadata: std::fs::Metadata = std::fs::symlink_metadata(path)?;
35 if !metadata.is_dir() {
36 return std::fs::remove_file(path);
37 }
38 let name: &std::ffi::OsStr = path.file_name().ok_or_else(|| { return std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("{} has no last component to remove", path.display())); })?;
39 let parent_path: &Path = path.parent().filter(|parent: &&Path| { return !parent.as_os_str().is_empty(); }).unwrap_or_else(|| { return Path::new("."); });
40 let parent: std::fs::File = std::fs::File::open(parent_path)?;
41 let top: std::fs::File = open_directory_widening(&parent, name).map_err(|error: std::io::Error| {
42 if error.raw_os_error() == Some(libc::EXDEV) {
43 return std::io::Error::new(
44 std::io::ErrorKind::CrossesDevices,
45 format!("{} is a mount point, so its content is not part of any tree to remove; unmount it first (`umount {}`)", path.display(), path.display()),
46 );
47 }
48 return error;
49 })?;
50 verify_below(&top, path)?;
51 empty_directory(&top, path)?;
52 drop(top);
53 return remove_at(&parent, name, true);
54}
55
56/// Removal controls stay out of the release executable.
57#[cfg(test)]
58#[path = "subvolume_remove_tests.rs"]
59mod tests;