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    /// Be verbose, print subvolume names as they are deleted
45    #[clap(short = 'v', long)]
46    pub verbose: bool,
47
48    /// Subvolume paths to delete, or (with --subvolid) the filesystem path
49    #[clap(required = true)]
50    pub paths: Vec<PathBuf>,
51}
52
53impl Runnable for SubvolumeDeleteCommand {
54    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
55        if self.subvolid.is_some() && self.paths.len() != 1 {
56            bail!(
57                "--subvolid requires exactly one path argument (the filesystem mount point)"
58            );
59        }
60
61        let mut had_error = false;
62        // For --commit-after, we save an fd and sync once at the end.
63        let mut commit_after_fd: Option<File> = None;
64
65        if let Some(subvolid) = self.subvolid {
66            let (ok, fd) = self.delete_by_id(subvolid, &self.paths[0]);
67            had_error |= !ok;
68            if self.commit_after {
69                commit_after_fd = fd;
70            }
71        } else {
72            for path in &self.paths {
73                let (ok, fd) = self.delete_by_path(path);
74                had_error |= !ok;
75                if self.commit_after && fd.is_some() {
76                    commit_after_fd = fd;
77                }
78            }
79        }
80
81        // --commit-after: sync once at the end.
82        if let Some(ref file) = commit_after_fd {
83            if let Err(e) = wait_for_commit(file.as_fd()) {
84                eprintln!("error: failed to commit: {e:#}");
85                had_error = true;
86            }
87        }
88
89        if had_error {
90            bail!("one or more subvolumes could not be deleted");
91        }
92
93        Ok(())
94    }
95}
96
97impl SubvolumeDeleteCommand {
98    /// Delete a subvolume by path. Returns (success, optional fd for commit-after).
99    fn delete_by_path(&self, path: &PathBuf) -> (bool, Option<File>) {
100        let result = (|| -> Result<File> {
101            let parent = path.parent().ok_or_else(|| {
102                anyhow::anyhow!("'{}' has no parent directory", path.display())
103            })?;
104
105            let name_os = path.file_name().ok_or_else(|| {
106                anyhow::anyhow!("'{}' has no file name", path.display())
107            })?;
108
109            let name_str = name_os.to_str().ok_or_else(|| {
110                anyhow::anyhow!("'{}' is not valid UTF-8", path.display())
111            })?;
112
113            let cname = CString::new(name_str).with_context(|| {
114                format!("subvolume name contains a null byte: '{name_str}'")
115            })?;
116
117            let parent_file = File::open(parent).with_context(|| {
118                format!("failed to open '{}'", parent.display())
119            })?;
120            let fd = parent_file.as_fd();
121
122            if self.recursive {
123                self.delete_children(path)?;
124            }
125
126            if self.verbose {
127                println!("Delete subvolume '{}'", path.display());
128            }
129
130            subvolume_delete(fd, &cname).with_context(|| {
131                format!("failed to delete '{}'", path.display())
132            })?;
133
134            if !self.verbose {
135                println!("Delete subvolume '{}'", path.display());
136            }
137
138            if self.commit_each {
139                wait_for_commit(fd).with_context(|| {
140                    format!("failed to commit after '{}'", path.display())
141                })?;
142            }
143
144            Ok(parent_file)
145        })();
146
147        match result {
148            Ok(file) => (true, Some(file)),
149            Err(e) => {
150                eprintln!("error: {e:#}");
151                (false, None)
152            }
153        }
154    }
155
156    /// Delete a subvolume by numeric ID. Returns (success, optional fd for commit-after).
157    fn delete_by_id(
158        &self,
159        subvolid: u64,
160        fs_path: &PathBuf,
161    ) -> (bool, Option<File>) {
162        let result = (|| -> Result<File> {
163            let file = File::open(fs_path).with_context(|| {
164                format!("failed to open '{}'", fs_path.display())
165            })?;
166            let fd = file.as_fd();
167
168            if self.verbose {
169                println!("Delete subvolume (subvolid={subvolid})");
170            }
171
172            subvolume_delete_by_id(fd, subvolid).with_context(|| {
173                format!(
174                    "failed to delete subvolid={subvolid} on '{}'",
175                    fs_path.display()
176                )
177            })?;
178
179            if !self.verbose {
180                println!("Delete subvolume (subvolid={subvolid})");
181            }
182
183            if self.commit_each {
184                wait_for_commit(fd).with_context(|| {
185                    format!("failed to commit on '{}'", fs_path.display())
186                })?;
187            }
188
189            Ok(file)
190        })();
191
192        match result {
193            Ok(file) => (true, Some(file)),
194            Err(e) => {
195                eprintln!("error: {e:#}");
196                (false, None)
197            }
198        }
199    }
200
201    /// Recursively delete all child subvolumes beneath `path`, deepest first.
202    fn delete_children(&self, path: &PathBuf) -> Result<()> {
203        let file = File::open(path)
204            .with_context(|| format!("failed to open '{}'", path.display()))?;
205        let fd = file.as_fd();
206
207        // Get the root ID of this subvolume.
208        let info = subvolume_info(fd).with_context(|| {
209            format!("failed to get subvolume info for '{}'", path.display())
210        })?;
211        let target_id = info.id;
212
213        // List all subvolumes on this filesystem and find those nested under target_id.
214        let all = subvolume_list(fd).with_context(|| {
215            format!("failed to list subvolumes on '{}'", path.display())
216        })?;
217
218        // Build the set of subvolume IDs that are descendants of target_id.
219        // We need post-order traversal (delete children before parents).
220        let mut children: Vec<u64> = Vec::new();
221        let mut frontier = vec![target_id];
222
223        while let Some(parent) = frontier.pop() {
224            for item in &all {
225                if item.parent_id == parent && item.root_id != target_id {
226                    children.push(item.root_id);
227                    frontier.push(item.root_id);
228                }
229            }
230        }
231
232        // Reverse so deepest children are deleted first (post-order).
233        children.reverse();
234
235        for child_id in children {
236            if self.verbose {
237                // Try to find the name for verbose output.
238                if let Some(item) = all.iter().find(|i| i.root_id == child_id) {
239                    if !item.name.is_empty() {
240                        println!(
241                            "Delete subvolume '{}/{}'",
242                            path.display(),
243                            item.name
244                        );
245                    } else {
246                        println!("Delete subvolume (subvolid={child_id})");
247                    }
248                }
249            }
250
251            subvolume_delete_by_id(fd, child_id).with_context(|| {
252                format!(
253                    "failed to delete child subvolid={child_id} under '{}'",
254                    path.display()
255                )
256            })?;
257
258            if self.commit_each {
259                wait_for_commit(fd).with_context(|| {
260                    format!("failed to commit after child subvolid={child_id}")
261                })?;
262            }
263        }
264
265        Ok(())
266    }
267}
268
269/// Initiate a sync and wait for it to complete (start_sync + wait_sync).
270fn wait_for_commit(fd: std::os::unix::io::BorrowedFd) -> Result<()> {
271    let transid = start_sync(fd).context("start_sync failed")?;
272    wait_sync(fd, transid).context("wait_sync failed")?;
273    Ok(())
274}