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(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 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 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 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 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 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 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 let all = subvolume_list(fd).with_context(|| {
203 format!("failed to list subvolumes on '{}'", path.display())
204 })?;
205
206 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 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
254fn 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}