use crate::{RunContext, Runnable};
use anyhow::{Context, Result, bail};
use btrfs_uapi::{
filesystem::{start_sync, wait_sync},
subvolume::{
subvolume_delete, subvolume_delete_by_id, subvolume_info,
subvolume_list,
},
};
use clap::Parser;
use std::{ffi::CString, fs::File, os::unix::io::AsFd, path::PathBuf};
#[derive(Parser, Debug)]
pub struct SubvolumeDeleteCommand {
#[clap(short = 'c', long, conflicts_with = "commit_each")]
pub commit_after: bool,
#[clap(short = 'C', long, conflicts_with = "commit_after")]
pub commit_each: bool,
#[clap(short = 'i', long, conflicts_with = "recursive")]
pub subvolid: Option<u64>,
#[clap(short = 'R', long, conflicts_with = "subvolid")]
pub recursive: bool,
#[clap(required = true)]
pub paths: Vec<PathBuf>,
}
impl Runnable for SubvolumeDeleteCommand {
fn supports_dry_run(&self) -> bool {
true
}
fn run(&self, ctx: &RunContext) -> Result<()> {
if self.subvolid.is_some() && self.paths.len() != 1 {
bail!(
"--subvolid requires exactly one path argument (the filesystem mount point)"
);
}
let mut had_error = false;
let mut commit_after_fd: Option<File> = None;
if let Some(subvolid) = self.subvolid {
let (ok, fd) =
self.delete_by_id(subvolid, &self.paths[0], ctx.dry_run);
had_error |= !ok;
if self.commit_after {
commit_after_fd = fd;
}
} else {
for path in &self.paths {
let (ok, fd) = self.delete_by_path(path, ctx.dry_run);
had_error |= !ok;
if self.commit_after && fd.is_some() {
commit_after_fd = fd;
}
}
}
if !ctx.dry_run
&& let Some(ref file) = commit_after_fd
&& let Err(e) = wait_for_commit(file.as_fd())
{
eprintln!("error: failed to commit: {e:#}");
had_error = true;
}
if had_error {
bail!("one or more subvolumes could not be deleted");
}
Ok(())
}
}
impl SubvolumeDeleteCommand {
fn delete_by_path(
&self,
path: &PathBuf,
dry_run: bool,
) -> (bool, Option<File>) {
let result = (|| -> Result<File> {
let parent = path.parent().ok_or_else(|| {
anyhow::anyhow!("'{}' has no parent directory", path.display())
})?;
let name_os = path.file_name().ok_or_else(|| {
anyhow::anyhow!("'{}' has no file name", path.display())
})?;
let name_str = name_os.to_str().ok_or_else(|| {
anyhow::anyhow!("'{}' is not valid UTF-8", path.display())
})?;
let cname = CString::new(name_str).with_context(|| {
format!("subvolume name contains a null byte: '{name_str}'")
})?;
let parent_file = File::open(parent).with_context(|| {
format!("failed to open '{}'", parent.display())
})?;
let fd = parent_file.as_fd();
if self.recursive && !dry_run {
self.delete_children(path)?;
}
println!("Delete subvolume '{}'", path.display());
if dry_run {
return Ok(parent_file);
}
subvolume_delete(fd, &cname).with_context(|| {
format!("failed to delete '{}'", path.display())
})?;
if self.commit_each {
wait_for_commit(fd).with_context(|| {
format!("failed to commit after '{}'", path.display())
})?;
}
Ok(parent_file)
})();
match result {
Ok(file) => (true, Some(file)),
Err(e) => {
eprintln!("error: {e:#}");
(false, None)
}
}
}
fn delete_by_id(
&self,
subvolid: u64,
fs_path: &PathBuf,
dry_run: bool,
) -> (bool, Option<File>) {
let result = (|| -> Result<File> {
let file = File::open(fs_path).with_context(|| {
format!("failed to open '{}'", fs_path.display())
})?;
let fd = file.as_fd();
println!("Delete subvolume (subvolid={subvolid})");
if dry_run {
return Ok(file);
}
subvolume_delete_by_id(fd, subvolid).with_context(|| {
format!(
"failed to delete subvolid={subvolid} on '{}'",
fs_path.display()
)
})?;
if self.commit_each {
wait_for_commit(fd).with_context(|| {
format!("failed to commit on '{}'", fs_path.display())
})?;
}
Ok(file)
})();
match result {
Ok(file) => (true, Some(file)),
Err(e) => {
eprintln!("error: {e:#}");
(false, None)
}
}
}
fn delete_children(&self, path: &PathBuf) -> Result<()> {
let file = File::open(path)
.with_context(|| format!("failed to open '{}'", path.display()))?;
let fd = file.as_fd();
let info = subvolume_info(fd).with_context(|| {
format!("failed to get subvolume info for '{}'", path.display())
})?;
let target_id = info.id;
let all = subvolume_list(fd).with_context(|| {
format!("failed to list subvolumes on '{}'", path.display())
})?;
let mut children: Vec<u64> = Vec::new();
let mut frontier = vec![target_id];
while let Some(parent) = frontier.pop() {
for item in &all {
if item.parent_id == parent && item.root_id != target_id {
children.push(item.root_id);
frontier.push(item.root_id);
}
}
}
children.reverse();
for child_id in children {
if let Some(item) = all.iter().find(|i| i.root_id == child_id) {
if item.name.is_empty() {
log::info!("Delete subvolume (subvolid={child_id})");
} else {
log::info!(
"Delete subvolume '{}/{}'",
path.display(),
item.name
);
}
}
subvolume_delete_by_id(fd, child_id).with_context(|| {
format!(
"failed to delete child subvolid={child_id} under '{}'",
path.display()
)
})?;
if self.commit_each {
wait_for_commit(fd).with_context(|| {
format!("failed to commit after child subvolid={child_id}")
})?;
}
}
Ok(())
}
}
fn wait_for_commit(fd: std::os::unix::io::BorrowedFd) -> Result<()> {
let transid = start_sync(fd).context("start_sync failed")?;
wait_sync(fd, transid).context("wait_sync failed")?;
Ok(())
}