btrfs_cli/subvolume/
delete.rs1use crate::{RunContext, 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 supports_dry_run(&self) -> bool {
51 true
52 }
53
54 fn run(&self, ctx: &RunContext) -> 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) =
67 self.delete_by_id(subvolid, &self.paths[0], ctx.dry_run);
68 had_error |= !ok;
69 if self.commit_after {
70 commit_after_fd = fd;
71 }
72 } else {
73 for path in &self.paths {
74 let (ok, fd) = self.delete_by_path(path, ctx.dry_run);
75 had_error |= !ok;
76 if self.commit_after && fd.is_some() {
77 commit_after_fd = fd;
78 }
79 }
80 }
81
82 if !ctx.dry_run
84 && let Some(ref file) = commit_after_fd
85 && let Err(e) = wait_for_commit(file.as_fd())
86 {
87 eprintln!("error: failed to commit: {e:#}");
88 had_error = true;
89 }
90
91 if had_error {
92 bail!("one or more subvolumes could not be deleted");
93 }
94
95 Ok(())
96 }
97}
98
99impl SubvolumeDeleteCommand {
100 fn delete_by_path(
102 &self,
103 path: &PathBuf,
104 dry_run: bool,
105 ) -> (bool, Option<File>) {
106 let result = (|| -> Result<File> {
107 let parent = path.parent().ok_or_else(|| {
108 anyhow::anyhow!("'{}' has no parent directory", path.display())
109 })?;
110
111 let name_os = path.file_name().ok_or_else(|| {
112 anyhow::anyhow!("'{}' has no file name", path.display())
113 })?;
114
115 let name_str = name_os.to_str().ok_or_else(|| {
116 anyhow::anyhow!("'{}' is not valid UTF-8", path.display())
117 })?;
118
119 let cname = CString::new(name_str).with_context(|| {
120 format!("subvolume name contains a null byte: '{name_str}'")
121 })?;
122
123 let parent_file = File::open(parent).with_context(|| {
124 format!("failed to open '{}'", parent.display())
125 })?;
126 let fd = parent_file.as_fd();
127
128 if self.recursive && !dry_run {
129 self.delete_children(path)?;
130 }
131
132 println!("Delete subvolume '{}'", path.display());
133
134 if dry_run {
135 return Ok(parent_file);
136 }
137
138 subvolume_delete(fd, &cname).with_context(|| {
139 format!("failed to delete '{}'", path.display())
140 })?;
141
142 if self.commit_each {
143 wait_for_commit(fd).with_context(|| {
144 format!("failed to commit after '{}'", path.display())
145 })?;
146 }
147
148 Ok(parent_file)
149 })();
150
151 match result {
152 Ok(file) => (true, Some(file)),
153 Err(e) => {
154 eprintln!("error: {e:#}");
155 (false, None)
156 }
157 }
158 }
159
160 fn delete_by_id(
162 &self,
163 subvolid: u64,
164 fs_path: &PathBuf,
165 dry_run: bool,
166 ) -> (bool, Option<File>) {
167 let result = (|| -> Result<File> {
168 let file = File::open(fs_path).with_context(|| {
169 format!("failed to open '{}'", fs_path.display())
170 })?;
171 let fd = file.as_fd();
172
173 println!("Delete subvolume (subvolid={subvolid})");
174
175 if dry_run {
176 return Ok(file);
177 }
178
179 subvolume_delete_by_id(fd, subvolid).with_context(|| {
180 format!(
181 "failed to delete subvolid={subvolid} on '{}'",
182 fs_path.display()
183 )
184 })?;
185
186 if self.commit_each {
187 wait_for_commit(fd).with_context(|| {
188 format!("failed to commit on '{}'", fs_path.display())
189 })?;
190 }
191
192 Ok(file)
193 })();
194
195 match result {
196 Ok(file) => (true, Some(file)),
197 Err(e) => {
198 eprintln!("error: {e:#}");
199 (false, None)
200 }
201 }
202 }
203
204 fn delete_children(&self, path: &PathBuf) -> Result<()> {
206 let file = File::open(path)
207 .with_context(|| format!("failed to open '{}'", path.display()))?;
208 let fd = file.as_fd();
209
210 let info = subvolume_info(fd).with_context(|| {
212 format!("failed to get subvolume info for '{}'", path.display())
213 })?;
214 let target_id = info.id;
215
216 let all = subvolume_list(fd).with_context(|| {
218 format!("failed to list subvolumes on '{}'", path.display())
219 })?;
220
221 let mut children: Vec<u64> = Vec::new();
224 let mut frontier = vec![target_id];
225
226 while let Some(parent) = frontier.pop() {
227 for item in &all {
228 if item.parent_id == parent && item.root_id != target_id {
229 children.push(item.root_id);
230 frontier.push(item.root_id);
231 }
232 }
233 }
234
235 children.reverse();
237
238 for child_id in children {
239 if let Some(item) = all.iter().find(|i| i.root_id == child_id) {
240 if item.name.is_empty() {
241 log::info!("Delete subvolume (subvolid={child_id})");
242 } else {
243 log::info!(
244 "Delete subvolume '{}/{}'",
245 path.display(),
246 item.name
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}