btrfs_cli/subvolume/
delete.rs1use 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#[derive(Parser, Debug)]
24pub struct SubvolumeDeleteCommand {
25 #[clap(short = 'c', long, conflicts_with = "commit_each")]
27 pub commit_after: bool,
28
29 #[clap(short = 'C', long, conflicts_with = "commit_after")]
31 pub commit_each: bool,
32
33 #[clap(short = 'i', long, conflicts_with = "recursive")]
36 pub subvolid: Option<u64>,
37
38 #[clap(short = 'R', long, conflicts_with = "subvolid")]
42 pub recursive: bool,
43
44 #[clap(short = 'v', long)]
46 pub verbose: bool,
47
48 #[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 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 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 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 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 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 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 let all = subvolume_list(fd).with_context(|| {
215 format!("failed to list subvolumes on '{}'", path.display())
216 })?;
217
218 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 children.reverse();
234
235 for child_id in children {
236 if self.verbose {
237 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
269fn 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}