use crate::fs_beneath::{device_of, handle_path, open_directory_widening, remove_at, widen_directory};
use std::os::unix::fs::MetadataExt;
use std::ffi::OsString;
use std::fs::File;
use std::path::{Path, PathBuf};
const EMPTY_SUBVOLUME_PLACEHOLDER_INODE: u64 = 2;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Pass {
Check,
Remove,
}
struct Frame {
directory: File,
name: Option<OsString>,
pending: Vec<(OsString, bool)>,
}
fn list(directory: &File, directories_only: bool) -> std::io::Result<Vec<(OsString, bool)>> {
let mut entries: Vec<(OsString, bool)> = Vec::new();
for found in std::fs::read_dir(handle_path(directory))? {
let item: std::fs::DirEntry = found?;
let is_directory: bool = item.file_type()?.is_dir();
if is_directory || !directories_only {
entries.push((item.file_name(), is_directory));
}
}
return Ok(entries);
}
fn shown_path(top: &Path, stack: &[Frame], name: &OsString) -> PathBuf {
let mut path: PathBuf = top.to_path_buf();
for frame in stack {
if let Some(part) = frame.name.as_ref() {
path.push(part);
}
}
path.push(name);
return path;
}
fn boundary_error(top: &Path, found: &Path, what: &str) -> std::io::Error {
return std::io::Error::new(
std::io::ErrorKind::CrossesDevices,
format!(
"{found} is {what} inside {top}, so it holds data that is not part of {top}, and the removal stopped before it \
changed anything below {top}. To go on: move {found} out of {top}, or delete it yourself (for a subvolume \
`btrfs subvolume delete {found}`, for a mount `umount {found}`), then run the removal again; to keep it, \
remove the other entries of {top} by hand.",
found = found.display(),
what = what,
top = top.display()
),
);
}
fn walk(top: &File, shown: &Path, pass: Pass) -> std::io::Result<()> {
let baseline: u64 = device_of(top)?;
let _top_widened = widen_directory(top);
let mut stack: Vec<Frame> = vec![Frame { directory: top.try_clone()?, name: None, pending: list(top, pass == Pass::Check)? }];
while let Some(frame) = stack.last_mut() {
let Some((name, is_directory)) = frame.pending.pop() else {
let Some(done) = stack.pop() else { break };
if pass == Pass::Remove
&& let (Some(name), Some(parent)) = (done.name.as_ref(), stack.last())
{
remove_at(&parent.directory, name, true)
.map_err(|error: std::io::Error| { return std::io::Error::new(error.kind(), format!("{} cannot be removed ({error})", shown_path(shown, &stack, name).display())); })?;
}
continue;
};
if !is_directory {
remove_at(&frame.directory, &name, false)
.map_err(|error: std::io::Error| { return std::io::Error::new(error.kind(), format!("{} cannot be removed ({error})", shown_path(shown, &stack, &name).display())); })?;
continue;
}
let opened: std::io::Result<File> = open_directory_widening(&frame.directory, &name);
let child: File = match opened {
Ok(child) => child,
Err(error) if error.raw_os_error() == Some(libc::EXDEV) => {
return Err(boundary_error(shown, shown_path(shown, &stack, &name).as_path(), "a mount point"));
}
Err(error) => {
return Err(std::io::Error::new(error.kind(), format!("{} cannot be opened without following a link ({error})", shown_path(shown, &stack, &name).display())));
}
};
let child_facts: std::fs::Metadata = child.metadata()?;
if child_facts.dev() != baseline && child_facts.ino() != EMPTY_SUBVOLUME_PLACEHOLDER_INODE {
return Err(boundary_error(shown, shown_path(shown, &stack, &name).as_path(), "a nested subvolume or another filesystem"));
}
let _child_widened = widen_directory(&child);
let pending: Vec<(OsString, bool)> = list(&child, pass == Pass::Check)?;
stack.push(Frame { directory: child, name: Some(name), pending });
}
return Ok(());
}
pub fn verify_below(top: &File, shown: &Path) -> std::io::Result<()> {
return walk(top, shown, Pass::Check);
}
pub fn empty_directory(top: &File, shown: &Path) -> std::io::Result<()> {
return walk(top, shown, Pass::Remove);
}
#[cfg(test)]
#[path = "subvolume_remove_walk_tests.rs"]
mod tests;