1use crate::{
2 Format, RunContext, Runnable,
3 util::{open_path, print_json},
4};
5use anyhow::{Context, Result};
6use btrfs_uapi::subvolume::{
7 SubvolumeFlags, SubvolumeListItem, subvolume_list,
8};
9use clap::Parser;
10use cols::Cols;
11use serde::Serialize;
12use std::{
13 cmp::Ordering, collections::BTreeMap, fmt::Write as _, os::unix::io::AsFd,
14 path::PathBuf, str::FromStr,
15};
16
17const HEADING_PATH_FILTERING: &str = "Path filtering";
18const HEADING_FIELD_SELECTION: &str = "Field selection";
19const HEADING_TYPE_FILTERING: &str = "Type filtering";
20const HEADING_SORTING: &str = "Sorting";
21
22#[derive(Parser, Debug)]
29#[allow(clippy::struct_excessive_bools)]
30pub struct SubvolumeListCommand {
31 #[clap(short = 'o', long, help_heading = HEADING_PATH_FILTERING)]
33 only_below: bool,
34
35 #[clap(short = 'a', long, help_heading = HEADING_PATH_FILTERING)]
38 all: bool,
39
40 #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
42 parent: bool,
43
44 #[clap(short = 'c', long, help_heading = HEADING_FIELD_SELECTION)]
46 ogeneration: bool,
47
48 #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
51 generation: bool,
52
53 #[clap(short, long, help_heading = HEADING_FIELD_SELECTION)]
55 uuid: bool,
56
57 #[clap(short = 'Q', long, help_heading = HEADING_FIELD_SELECTION)]
59 parent_uuid: bool,
60
61 #[clap(short = 'R', long, help_heading = HEADING_FIELD_SELECTION)]
63 received_uuid: bool,
64
65 #[clap(short = 's', long, help_heading = HEADING_TYPE_FILTERING)]
67 snapshots_only: bool,
68
69 #[clap(short = 'r', long, help_heading = HEADING_TYPE_FILTERING)]
71 readonly: bool,
72
73 #[clap(short = 'd', long, help_heading = HEADING_TYPE_FILTERING)]
75 deleted: bool,
76
77 #[clap(short = 't', long, help_heading = "Other")]
79 table: bool,
80
81 #[clap(short = 'G', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
83 gen_filter: Option<GenFilter>,
84
85 #[clap(short = 'C', long, value_name = "[+|-]VALUE", allow_hyphen_values = true, help_heading = HEADING_SORTING)]
87 ogen_filter: Option<GenFilter>,
88
89 #[clap(
94 long,
95 value_name = "KEYS",
96 value_delimiter = ',',
97 allow_hyphen_values = true,
98 help_heading = HEADING_SORTING,
99 )]
100 sort: Vec<SortKey>,
101
102 path: PathBuf,
104}
105
106#[derive(Serialize)]
107struct SubvolListJson {
108 id: u64,
109 generation: u64,
110 ogeneration: u64,
111 parent: u64,
112 top_level: u64,
113 path: String,
114 uuid: String,
115 parent_uuid: String,
116 received_uuid: String,
117 readonly: bool,
118}
119
120impl SubvolListJson {
121 fn from_item(item: &SubvolumeListItem) -> Self {
122 Self {
123 id: item.root_id,
124 generation: item.generation,
125 ogeneration: item.otransid,
126 parent: item.parent_id,
127 top_level: item.parent_id,
128 path: if item.name.is_empty() {
129 "<unknown>".to_string()
130 } else {
131 item.name.clone()
132 },
133 uuid: fmt_uuid(&item.uuid),
134 parent_uuid: fmt_uuid(&item.parent_uuid),
135 received_uuid: fmt_uuid(&item.received_uuid),
136 readonly: item.flags.contains(SubvolumeFlags::RDONLY),
137 }
138 }
139}
140
141impl Runnable for SubvolumeListCommand {
142 fn supported_formats(&self) -> &[Format] {
143 &[Format::Text, Format::Json, Format::Modern]
144 }
145
146 fn run(&self, ctx: &RunContext) -> Result<()> {
147 let file = open_path(&self.path)?;
148
149 let mut items = subvolume_list(file.as_fd()).with_context(|| {
150 format!("failed to list subvolumes for '{}'", self.path.display())
151 })?;
152
153 let top_id = btrfs_uapi::inode::lookup_path_rootid(file.as_fd())
154 .with_context(|| "failed to get root id for path")?;
155
156 if self.deleted {
161 items.retain(|item| item.parent_id == 0);
162 } else if !self.all {
163 items.retain(|item| item.parent_id != 0);
164 }
165 if self.readonly {
166 items.retain(|item| item.flags.contains(SubvolumeFlags::RDONLY));
167 }
168 if self.snapshots_only {
169 items.retain(|item| !item.parent_uuid.is_nil());
170 }
171 if let Some(ref f) = self.gen_filter {
172 items.retain(|item| f.matches(item.generation));
173 }
174 if let Some(ref f) = self.ogen_filter {
175 items.retain(|item| f.matches(item.otransid));
176 }
177 if self.only_below {
178 items.retain(|item| item.parent_id == top_id);
182 }
183
184 if self.all {
187 for item in &mut items {
188 if item.parent_id != 0
189 && item.parent_id != top_id
190 && !item.name.is_empty()
191 {
192 item.name = format!("<FS_TREE>/{}", item.name);
193 }
194 }
195 }
196
197 if self.sort.is_empty() {
199 items.sort_by_key(|item| item.root_id);
200 } else {
201 items.sort_by(|a, b| {
202 for key in &self.sort {
203 let ord = key.compare(a, b);
204 if ord != Ordering::Equal {
205 return ord;
206 }
207 }
208 Ordering::Equal
209 });
210 }
211
212 match ctx.format {
213 Format::Modern => self.print_modern(&items),
214 Format::Text => {
215 if self.table {
216 self.print_table(&items);
217 } else {
218 self.print_default(&items);
219 }
220 }
221 Format::Json => {
222 let json: Vec<SubvolListJson> =
223 items.iter().map(SubvolListJson::from_item).collect();
224 print_json("subvolume-list", &json)?;
225 }
226 }
227
228 Ok(())
229 }
230}
231
232impl SubvolumeListCommand {
233 fn print_default(&self, items: &[SubvolumeListItem]) {
234 for item in items {
235 let name = if item.name.is_empty() {
236 "<unknown>"
237 } else {
238 &item.name
239 };
240
241 let mut line =
245 format!("ID {} gen {}", item.root_id, item.generation);
246
247 if self.ogeneration {
248 let _ = write!(line, " cgen {}", item.otransid);
249 }
250
251 let _ = write!(line, " top level {}", item.parent_id);
252
253 if self.parent {
254 let _ = write!(line, " parent {}", item.parent_id);
255 }
256
257 let _ = write!(line, " path {name}");
258
259 if self.uuid {
260 let _ = write!(line, " uuid {}", fmt_uuid(&item.uuid));
261 }
262
263 if self.parent_uuid {
264 let _ = write!(
265 line,
266 " parent_uuid {}",
267 fmt_uuid(&item.parent_uuid)
268 );
269 }
270
271 if self.received_uuid {
272 let _ = write!(
273 line,
274 " received_uuid {}",
275 fmt_uuid(&item.received_uuid)
276 );
277 }
278
279 println!("{line}");
280 }
281 }
282
283 fn print_table(&self, items: &[SubvolumeListItem]) {
284 let mut headers: Vec<&str> = vec!["ID", "gen"];
286 if self.ogeneration {
287 headers.push("cgen");
288 }
289 headers.push("top level");
290 if self.parent {
291 headers.push("parent");
292 }
293 headers.push("path");
294 if self.uuid {
295 headers.push("uuid");
296 }
297 if self.parent_uuid {
298 headers.push("parent_uuid");
299 }
300 if self.received_uuid {
301 headers.push("received_uuid");
302 }
303
304 println!("{}", headers.join("\t"));
306
307 let sep: Vec<String> =
309 headers.iter().map(|h| "-".repeat(h.len())).collect();
310 println!("{}", sep.join("\t"));
311
312 for item in items {
314 let name = if item.name.is_empty() {
315 "<unknown>"
316 } else {
317 &item.name
318 };
319
320 let mut cols: Vec<String> =
321 vec![item.root_id.to_string(), item.generation.to_string()];
322 if self.ogeneration {
323 cols.push(item.otransid.to_string());
324 }
325 cols.push(item.parent_id.to_string());
326 if self.parent {
327 cols.push(item.parent_id.to_string());
328 }
329 cols.push(name.to_string());
330 if self.uuid {
331 cols.push(fmt_uuid(&item.uuid));
332 }
333 if self.parent_uuid {
334 cols.push(fmt_uuid(&item.parent_uuid));
335 }
336 if self.received_uuid {
337 cols.push(fmt_uuid(&item.received_uuid));
338 }
339
340 println!("{}", cols.join("\t"));
341 }
342 }
343}
344
345#[derive(Cols)]
346struct SubvolRow {
347 #[column(right)]
348 id: u64,
349 #[column(header = "GEN", right)]
350 generation: u64,
351 #[column(header = "CGEN", right)]
352 cgen: u64,
353 #[column(right)]
354 parent: u64,
355 #[column(tree)]
356 path: String,
357 uuid: String,
358 parent_uuid: String,
359 received_uuid: String,
360 #[column(children)]
361 children: Vec<Self>,
362}
363
364impl SubvolRow {
365 fn from_item(item: &SubvolumeListItem) -> Self {
366 let name = if item.name.is_empty() {
367 "<unknown>".to_string()
368 } else {
369 item.name.clone()
370 };
371 Self {
372 id: item.root_id,
373 generation: item.generation,
374 cgen: item.otransid,
375 parent: item.parent_id,
376 path: name,
377 uuid: fmt_uuid(&item.uuid),
378 parent_uuid: fmt_uuid(&item.parent_uuid),
379 received_uuid: fmt_uuid(&item.received_uuid),
380 children: Vec::new(),
381 }
382 }
383}
384
385fn attach_children(
387 id: u64,
388 rows: &mut BTreeMap<u64, SubvolRow>,
389 children_map: &BTreeMap<u64, Vec<u64>>,
390) -> Option<SubvolRow> {
391 let mut row = rows.remove(&id)?;
392 if let Some(child_ids) = children_map.get(&id) {
393 for &child_id in child_ids {
394 if let Some(child) = attach_children(child_id, rows, children_map) {
395 row.children.push(child);
396 }
397 }
398 }
399 Some(row)
400}
401
402fn build_tree(items: &[SubvolumeListItem]) -> Vec<SubvolRow> {
408 let mut rows: BTreeMap<u64, SubvolRow> = items
409 .iter()
410 .map(|i| (i.root_id, SubvolRow::from_item(i)))
411 .collect();
412
413 let mut children_map: BTreeMap<u64, Vec<u64>> = BTreeMap::new();
414 let mut roots = Vec::new();
415
416 for item in items {
420 if rows.contains_key(&item.parent_id) {
421 children_map
422 .entry(item.parent_id)
423 .or_default()
424 .push(item.root_id);
425 } else {
426 roots.push(item.root_id);
427 }
428 }
429
430 let mut result: Vec<SubvolRow> = roots
431 .iter()
432 .filter_map(|&id| attach_children(id, &mut rows, &children_map))
433 .collect();
434
435 for (_, row) in rows {
437 result.push(row);
438 }
439
440 result
441}
442
443impl SubvolumeListCommand {
444 fn print_modern(&self, items: &[SubvolumeListItem]) {
445 let tree = build_tree(items);
446
447 let mut headers =
448 vec![SubvolRowHeader::Id, SubvolRowHeader::Generation];
449 if self.ogeneration {
450 headers.push(SubvolRowHeader::Cgen);
451 }
452 headers.push(SubvolRowHeader::Parent);
453 headers.push(SubvolRowHeader::Path);
454 if self.uuid {
455 headers.push(SubvolRowHeader::Uuid);
456 }
457 if self.parent_uuid {
458 headers.push(SubvolRowHeader::ParentUuid);
459 }
460 if self.received_uuid {
461 headers.push(SubvolRowHeader::ReceivedUuid);
462 }
463
464 let table = SubvolRow::to_table_with(&tree, &headers);
465 let mut out = std::io::stdout().lock();
466 let _ = cols::print_table(&table, &mut out);
467 }
468}
469
470fn fmt_uuid(u: &uuid::Uuid) -> String {
471 if u.is_nil() {
472 "-".to_string()
473 } else {
474 u.hyphenated().to_string()
475 }
476}
477
478#[derive(Debug, Clone)]
480pub enum GenFilter {
481 Exact(u64),
482 AtLeast(u64),
483 AtMost(u64),
484}
485
486impl GenFilter {
487 fn matches(&self, value: u64) -> bool {
488 match self {
489 GenFilter::Exact(v) => value == *v,
490 GenFilter::AtLeast(v) => value >= *v,
491 GenFilter::AtMost(v) => value <= *v,
492 }
493 }
494}
495
496impl FromStr for GenFilter {
497 type Err = String;
498
499 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
500 if let Some(rest) = s.strip_prefix('+') {
501 let v: u64 = rest
502 .parse()
503 .map_err(|_| format!("invalid number: '{rest}'"))?;
504 Ok(GenFilter::AtLeast(v))
505 } else if let Some(rest) = s.strip_prefix('-') {
506 let v: u64 = rest
507 .parse()
508 .map_err(|_| format!("invalid number: '{rest}'"))?;
509 Ok(GenFilter::AtMost(v))
510 } else {
511 let v: u64 =
512 s.parse().map_err(|_| format!("invalid number: '{s}'"))?;
513 Ok(GenFilter::Exact(v))
514 }
515 }
516}
517
518#[derive(Debug, Clone)]
520pub struct SortKey {
521 field: SortField,
522 descending: bool,
523}
524
525#[derive(Debug, Clone)]
526enum SortField {
527 Gen,
528 Ogen,
529 Rootid,
530 Path,
531}
532
533impl SortKey {
534 fn compare(
535 &self,
536 a: &SubvolumeListItem,
537 b: &SubvolumeListItem,
538 ) -> Ordering {
539 let ord = match self.field {
540 SortField::Gen => a.generation.cmp(&b.generation),
541 SortField::Ogen => a.otransid.cmp(&b.otransid),
542 SortField::Rootid => a.root_id.cmp(&b.root_id),
543 SortField::Path => a.name.cmp(&b.name),
544 };
545 if self.descending { ord.reverse() } else { ord }
546 }
547}
548
549impl FromStr for SortKey {
550 type Err = String;
551
552 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
553 let (descending, field_str) = if let Some(rest) = s.strip_prefix('-') {
554 (true, rest)
555 } else if let Some(rest) = s.strip_prefix('+') {
556 (false, rest)
557 } else {
558 (false, s)
559 };
560
561 let field = match field_str {
562 "gen" => SortField::Gen,
563 "ogen" => SortField::Ogen,
564 "rootid" => SortField::Rootid,
565 "path" => SortField::Path,
566 _ => {
567 return Err(format!(
568 "unknown sort key: '{field_str}' (expected gen, ogen, rootid, or path)"
569 ));
570 }
571 };
572
573 Ok(SortKey { field, descending })
574 }
575}