Skip to main content

btrfs_cli/device/
remove.rs

1use crate::{Format, Runnable};
2use anyhow::{Context, Result};
3use btrfs_uapi::device::{DeviceSpec, device_remove};
4use clap::Parser;
5use std::{ffi::CString, fs::File, os::unix::io::AsFd, path::PathBuf};
6
7/// Remove one or more devices from a mounted filesystem
8///
9/// Each device can be specified as a block device path, a numeric device ID,
10/// the special token "missing" (to remove a device that is no longer present),
11/// or "cancel" (to cancel an in-progress removal).
12///
13/// The operation requires CAP_SYS_ADMIN.
14#[derive(Parser, Debug)]
15pub struct DeviceRemoveCommand {
16    /// One or more devices to remove (path, devid, "missing", or "cancel"),
17    /// followed by the filesystem mount point
18    ///
19    /// Example: btrfs device remove /dev/sdb 3 missing /mnt/data
20    #[clap(required = true, num_args = 2..)]
21    pub args: Vec<String>,
22}
23
24impl Runnable for DeviceRemoveCommand {
25    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
26        // The last argument is the mount point; everything before it is a device spec.
27        // split_last() returns (&last, &[..rest]), so mount_str is first.
28        let (mount_str, specs) = self
29            .args
30            .split_last()
31            .expect("clap ensures at least 2 args");
32
33        let mount = PathBuf::from(mount_str);
34        let file = File::open(&mount)
35            .with_context(|| format!("failed to open '{}'", mount.display()))?;
36        let fd = file.as_fd();
37
38        let mut had_error = false;
39
40        for spec_str in specs {
41            match remove_one(fd, spec_str) {
42                Ok(()) => println!("removed device '{spec_str}'"),
43                Err(e) => {
44                    eprintln!("error removing device '{spec_str}': {e}");
45                    had_error = true;
46                }
47            }
48        }
49
50        if had_error {
51            anyhow::bail!("one or more devices could not be removed");
52        }
53
54        Ok(())
55    }
56}
57
58/// Attempt to remove a single device identified by `spec_str` from the
59/// filesystem open on `fd`.
60///
61/// If `spec_str` parses as a `u64` it is treated as a device ID; otherwise it
62/// is treated as a path (or the special strings `"missing"` / `"cancel"`).
63fn remove_one(fd: std::os::unix::io::BorrowedFd, spec_str: &str) -> Result<()> {
64    if let Ok(devid) = spec_str.parse::<u64>() {
65        device_remove(fd, DeviceSpec::Id(devid))
66            .with_context(|| format!("failed to remove devid {devid}"))?;
67    } else {
68        let cpath = CString::new(spec_str).with_context(|| {
69            format!("device spec contains a null byte: '{spec_str}'")
70        })?;
71        device_remove(fd, DeviceSpec::Path(&cpath))
72            .with_context(|| format!("failed to remove device '{spec_str}'"))?;
73    }
74    Ok(())
75}