Skip to main content

btrfs_cli/subvolume/
delete.rs

1use crate::{Format, Runnable};
2use anyhow::{Context, Result, bail};
3use btrfs_uapi::{
4    filesystem::{start_sync, wait_sync},
5    subvolume::{
6        subvolume_delete, subvolume_delete_by_id, subvolume_info,
7        subvolume_list,
8    },
9};
10use clap::Parser;
11use std::{ffi::CString, fs::File, os::unix::io::AsFd, path::PathBuf};
12
13/// Delete one or more subvolumes or snapshots.
14///
15/// Delete subvolumes from the filesystem, specified by path or id. The
16/// corresponding directory is removed instantly but the data blocks are
17/// removed later.
18///
19/// The deletion does not involve full commit by default due to performance
20/// reasons (as a consequence, the subvolume may appear again after a crash).
21/// Use one of the --commit options to wait until the operation is safely
22/// stored on the media.
23#[derive(Parser, Debug)]
24pub struct SubvolumeDeleteCommand {
25    /// Wait for transaction commit at the end of the operation
26    #[clap(short = 'c', long, conflicts_with = "commit_each")]
27    pub commit_after: bool,
28
29    /// Wait for transaction commit after deleting each subvolume
30    #[clap(short = 'C', long, conflicts_with = "commit_after")]
31    pub commit_each: bool,
32
33    /// Delete by subvolume ID instead of path. When used, exactly one
34    /// positional argument is expected: the filesystem path (mount point).
35    #[clap(short = 'i', long, conflicts_with = "recursive")]
36    pub subvolid: Option<u64>,
37
38    /// Delete accessible subvolumes beneath each subvolume recursively.
39    /// This is not atomic and may need root to delete subvolumes not
40    /// accessible by the user.
41    #[clap(short = 'R', long, conflicts_with = "subvolid")]
42    pub recursive: bool,
43
44    /// Subvolume paths to delete, or (with --subvolid) the filesystem path
45    #[clap(required = true)]
46    pub paths: Vec<PathBuf>,
47}
48
49impl Runnable for SubvolumeDeleteCommand {
50    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
51        if self.subvolid.is_some() && self.paths.len() != 1 {
52            bail!(
53                "--subvolid requires exactly one path argument (the filesystem mount point)"
54            );
55        }
56
57        let mut had_error = false;
58        // For --commit-after, we save an fd and sync once at the end.
59        let mut commit_after_fd: Option<File> = None;
60
61        if let Some(subvolid) = self.subvolid {
62            let (ok, fd) = self.delete_by_id(subvolid, &self.paths[0]);
63            had_error |= !ok;
64            if self.commit_after {
65                commit_after_fd = fd;
66            }
67        } else {
68            for path in &self.paths {
69                let (ok, fd) = self.delete_by_path(path);
70                had_error |= !ok;
71                if self.commit_after && fd.is_some() {
72                    commit_after_fd = fd;
73                }
74            }
75        }
76
77        // --commit-after: sync once at the end.
78        if let Some(ref file) = commit_after_fd
79            && let Err(e) = wait_for_commit(file.as_fd())
80        {
81            eprintln!("error: failed to commit: {e:#}");
82            had_error = true;
83        }
84
85        if had_error {
86            bail!("one or more subvolumes could not be deleted");
87        }
88
89        Ok(())
90    }
91}
92
93impl SubvolumeDeleteCommand {
94    /// Delete a subvolume by path. Returns (success, optional fd for commit-after).
95    fn delete_by_path(&self, path: &PathBuf) -> (bool, Option<File>) {
96        let result = (|| -> Result<File> {
97            let parent = path.parent().ok_or_else(|| {
98                anyhow::anyhow!("'{}' has no parent directory", path.display())
99            })?;
100
101            let name_os = path.file_name().ok_or_else(|| {
102                anyhow::anyhow!("'{}' has no file name", path.display())
103            })?;
104
105            let name_str = name_os.to_str().ok_or_else(|| {
106                anyhow::anyhow!("'{}' is not valid UTF-8", path.display())
107            })?;
108
109            let cname = CString::new(name_str).with_context(|| {
110                format!("subvolume name contains a null byte: '{name_str}'")
111            })?;
112
113            let parent_file = File::open(parent).with_context(|| {
114                format!("failed to open '{}'", parent.display())
115            })?;
116            let fd = parent_file.as_fd();
117
118            if self.recursive {
119                self.delete_children(path)?;
120            }
121
122            log::info!("Delete subvolume '{}'", path.display());
123
124            subvolume_delete(fd, &cname).with_context(|| {
125                format!("failed to delete '{}'", path.display())
126            })?;
127
128            println!("Delete subvolume '{}'", path.display());
129
130            if self.commit_each {
131                wait_for_commit(fd).with_context(|| {
132                    format!("failed to commit after '{}'", path.display())
133                })?;
134            }
135
136            Ok(parent_file)
137        })();
138
139        match result {
140            Ok(file) => (true, Some(file)),
141            Err(e) => {
142                eprintln!("error: {e:#}");
143                (false, None)
144            }
145        }
146    }
147
148    /// Delete a subvolume by numeric ID. Returns (success, optional fd for commit-after).
149    fn delete_by_id(
150        &self,
151        subvolid: u64,
152        fs_path: &PathBuf,
153    ) -> (bool, Option<File>) {
154        let result = (|| -> Result<File> {
155            let file = File::open(fs_path).with_context(|| {
156                format!("failed to open '{}'", fs_path.display())
157            })?;
158            let fd = file.as_fd();
159
160            log::info!("Delete subvolume (subvolid={subvolid})");
161
162            subvolume_delete_by_id(fd, subvolid).with_context(|| {
163                format!(
164                    "failed to delete subvolid={subvolid} on '{}'",
165                    fs_path.display()
166                )
167            })?;
168
169            println!("Delete subvolume (subvolid={subvolid})");
170
171            if self.commit_each {
172                wait_for_commit(fd).with_context(|| {
173                    format!("failed to commit on '{}'", fs_path.display())
174                })?;
175            }
176
177            Ok(file)
178        })();
179
180        match result {
181            Ok(file) => (true, Some(file)),
182            Err(e) => {
183                eprintln!("error: {e:#}");
184                (false, None)
185            }
186        }
187    }
188
189    /// Recursively delete all child subvolumes beneath `path`, deepest first.
190    fn delete_children(&self, path: &PathBuf) -> Result<()> {
191        let file = File::open(path)
192            .with_context(|| format!("failed to open '{}'", path.display()))?;
193        let fd = file.as_fd();
194
195        // Get the root ID of this subvolume.
196        let info = subvolume_info(fd).with_context(|| {
197            format!("failed to get subvolume info for '{}'", path.display())
198        })?;
199        let target_id = info.id;
200
201        // List all subvolumes on this filesystem and find those nested under target_id.
202        let all = subvolume_list(fd).with_context(|| {
203            format!("failed to list subvolumes on '{}'", path.display())
204        })?;
205
206        // Build the set of subvolume IDs that are descendants of target_id.
207        // We need post-order traversal (delete children before parents).
208        let mut children: Vec<u64> = Vec::new();
209        let mut frontier = vec![target_id];
210
211        while let Some(parent) = frontier.pop() {
212            for item in &all {
213                if item.parent_id == parent && item.root_id != target_id {
214                    children.push(item.root_id);
215                    frontier.push(item.root_id);
216                }
217            }
218        }
219
220        // Reverse so deepest children are deleted first (post-order).
221        children.reverse();
222
223        for child_id in children {
224            if let Some(item) = all.iter().find(|i| i.root_id == child_id) {
225                if item.name.is_empty() {
226                    log::info!("Delete subvolume (subvolid={child_id})");
227                } else {
228                    log::info!(
229                        "Delete subvolume '{}/{}'",
230                        path.display(),
231                        item.name
232                    );
233                }
234            }
235
236            subvolume_delete_by_id(fd, child_id).with_context(|| {
237                format!(
238                    "failed to delete child subvolid={child_id} under '{}'",
239                    path.display()
240                )
241            })?;
242
243            if self.commit_each {
244                wait_for_commit(fd).with_context(|| {
245                    format!("failed to commit after child subvolid={child_id}")
246                })?;
247            }
248        }
249
250        Ok(())
251    }
252}
253
254/// Initiate a sync and wait for it to complete (start_sync + wait_sync).
255fn wait_for_commit(fd: std::os::unix::io::BorrowedFd) -> Result<()> {
256    let transid = start_sync(fd).context("start_sync failed")?;
257    wait_sync(fd, transid).context("wait_sync failed")?;
258    Ok(())
259}