Skip to main content

btrfs_cli/qgroup/
show.rs

1use crate::{
2    Format, RunContext, Runnable,
3    util::{SizeFormat, fmt_size, open_path, print_json},
4};
5use anyhow::{Context, Result};
6use btrfs_uapi::quota::{
7    QgroupInfo, QgroupLimitFlags, QgroupStatusFlags, qgroupid_level,
8    qgroupid_subvolid,
9};
10use clap::Parser;
11use cols::Cols;
12use serde::Serialize;
13use std::{fmt::Write as _, os::unix::io::AsFd, path::PathBuf};
14
15const HEADING_COLUMN_SELECTION: &str = "Column selection";
16const HEADING_FILTERING: &str = "Filtering";
17const HEADING_SIZE_UNITS: &str = "Size units";
18
19/// List subvolume quota groups
20///
21/// Shows all quota groups (qgroups) and their space accounting. Each
22/// subvolume automatically gets a level-0 qgroup (e.g. 0/256) that
23/// tracks its individual usage. Higher-level qgroups (1/0, 2/0, ...)
24/// can be created to group subvolumes and apply shared limits across
25/// them.
26///
27/// Columns:
28///
29/// qgroupid: the quota group identifier in level/id format. Level 0
30/// corresponds to individual subvolumes. Higher levels are user-created
31/// grouping containers.
32///
33/// rfer (referenced): total bytes of data referenced by this qgroup.
34/// For level-0 qgroups this is the logical size of all extents in the
35/// subvolume. Shared extents (e.g. from snapshots or reflinks) are
36/// counted in full by each qgroup that references them.
37///
38/// excl (exclusive): bytes used exclusively by this qgroup, not shared
39/// with any other qgroup at the same level. This is the space that
40/// would be freed if the subvolume were deleted (assuming no other
41/// references).
42///
43/// max_rfer: the configured limit on referenced bytes. Writes that
44/// would exceed this limit fail with EDQUOT. Shows "none" if no limit
45/// is set.
46///
47/// max_excl: the configured limit on exclusive bytes. Shows "none" if
48/// no limit is set.
49///
50/// In --format modern, the qgroup hierarchy is shown as a tree: higher-
51/// level qgroups appear as parents with their member qgroups nested
52/// below using tree connectors.
53#[derive(Parser, Debug)]
54#[allow(clippy::struct_excessive_bools, clippy::doc_markdown)]
55pub struct QgroupShowCommand {
56    /// Path to a mounted btrfs filesystem
57    pub path: PathBuf,
58
59    /// Print parent qgroup id
60    #[clap(short = 'p', long, help_heading = HEADING_COLUMN_SELECTION)]
61    pub print_parent: bool,
62
63    /// Print child qgroup id
64    #[clap(short = 'c', long, help_heading = HEADING_COLUMN_SELECTION)]
65    pub print_child: bool,
66
67    /// Print limit of referenced size
68    #[clap(short = 'r', long, help_heading = HEADING_COLUMN_SELECTION)]
69    pub print_rfer_limit: bool,
70
71    /// Print limit of exclusive size
72    #[clap(short = 'e', long, help_heading = HEADING_COLUMN_SELECTION)]
73    pub print_excl_limit: bool,
74
75    /// List all qgroups impacting path, including ancestral qgroups
76    #[clap(short = 'F', long, help_heading = HEADING_FILTERING)]
77    pub filter_all: bool,
78
79    /// List all qgroups impacting path, excluding ancestral qgroups
80    #[clap(short = 'f', long, help_heading = HEADING_FILTERING)]
81    pub filter_direct: bool,
82
83    /// Show raw numbers in bytes
84    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
85    pub raw: bool,
86
87    /// Show human friendly numbers, base 1024 (default)
88    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
89    pub human_readable: bool,
90
91    /// Use 1024 as a base (IEC units)
92    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
93    pub iec: bool,
94
95    /// Use 1000 as a base (SI units)
96    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
97    pub si: bool,
98
99    /// Show sizes in KiB
100    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
101    pub kbytes: bool,
102
103    /// Show sizes in MiB
104    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
105    pub mbytes: bool,
106
107    /// Show sizes in GiB
108    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
109    pub gbytes: bool,
110
111    /// Show sizes in TiB
112    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
113    pub tbytes: bool,
114
115    /// Sort by a comma-separated list of fields (qgroupid, rfer, excl, max_rfer, max_excl)
116    #[clap(long)]
117    pub sort: Option<SortKeys>,
118
119    /// Force a sync before getting quota information
120    #[clap(long)]
121    pub sync: bool,
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125enum SortField {
126    Qgroupid,
127    Rfer,
128    Excl,
129    MaxRfer,
130    MaxExcl,
131}
132
133#[derive(Debug, Clone, Copy)]
134struct SortKey {
135    field: SortField,
136    descending: bool,
137}
138
139impl std::str::FromStr for SortKey {
140    type Err = String;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        let (descending, name) = match s.strip_prefix('-') {
144            Some(rest) => (true, rest),
145            None => (false, s),
146        };
147        let field = match name {
148            "qgroupid" => SortField::Qgroupid,
149            "rfer" => SortField::Rfer,
150            "excl" => SortField::Excl,
151            "max_rfer" => SortField::MaxRfer,
152            "max_excl" => SortField::MaxExcl,
153            _ => {
154                return Err(format!(
155                    "unknown sort field '{name}'; expected qgroupid, rfer, excl, max_rfer, or max_excl"
156                ));
157            }
158        };
159        Ok(SortKey { field, descending })
160    }
161}
162
163/// Comma-separated list of sort keys (e.g. "rfer,-excl").
164#[derive(Debug, Clone)]
165pub struct SortKeys(Vec<SortKey>);
166
167impl std::str::FromStr for SortKeys {
168    type Err = String;
169
170    fn from_str(s: &str) -> Result<Self, Self::Err> {
171        let keys: Vec<SortKey> = s
172            .split(',')
173            .map(|part| part.trim().parse())
174            .collect::<Result<_, _>>()?;
175        if keys.is_empty() {
176            return Err("sort field list must not be empty".to_string());
177        }
178        Ok(SortKeys(keys))
179    }
180}
181
182fn fmt_limit(
183    bytes: u64,
184    flags: QgroupLimitFlags,
185    flag_bit: QgroupLimitFlags,
186    mode: &SizeFormat,
187) -> String {
188    if bytes == u64::MAX || !flags.contains(flag_bit) {
189        "none".to_string()
190    } else {
191        fmt_size(bytes, mode)
192    }
193}
194
195fn format_qgroupid(qgroupid: u64) -> String {
196    format!(
197        "{}/{}",
198        qgroupid_level(qgroupid),
199        qgroupid_subvolid(qgroupid)
200    )
201}
202
203#[derive(Serialize)]
204struct QgroupJson {
205    qgroupid: String,
206    rfer: u64,
207    excl: u64,
208    max_rfer: Option<u64>,
209    max_excl: Option<u64>,
210    #[serde(skip_serializing_if = "Vec::is_empty")]
211    parents: Vec<String>,
212    #[serde(skip_serializing_if = "Vec::is_empty")]
213    children: Vec<String>,
214}
215
216impl QgroupJson {
217    fn from_info(q: &QgroupInfo) -> Self {
218        let max_rfer = if q.limit_flags.contains(QgroupLimitFlags::MAX_RFER)
219            && q.max_rfer != u64::MAX
220        {
221            Some(q.max_rfer)
222        } else {
223            None
224        };
225        let max_excl = if q.limit_flags.contains(QgroupLimitFlags::MAX_EXCL)
226            && q.max_excl != u64::MAX
227        {
228            Some(q.max_excl)
229        } else {
230            None
231        };
232        Self {
233            qgroupid: format_qgroupid(q.qgroupid),
234            rfer: q.rfer,
235            excl: q.excl,
236            max_rfer,
237            max_excl,
238            parents: q.parents.iter().map(|&id| format_qgroupid(id)).collect(),
239            children: q
240                .children
241                .iter()
242                .map(|&id| format_qgroupid(id))
243                .collect(),
244        }
245    }
246}
247
248impl Runnable for QgroupShowCommand {
249    fn supported_formats(&self) -> &[Format] {
250        &[Format::Text, Format::Json, Format::Modern]
251    }
252
253    #[allow(clippy::too_many_lines)]
254    fn run(&self, ctx: &RunContext) -> Result<()> {
255        // filter_all / filter_direct: not implemented, ignored
256        let _ = self.filter_all;
257        let _ = self.filter_direct;
258
259        let file = open_path(&self.path)?;
260        let fd = file.as_fd();
261
262        if self.sync {
263            btrfs_uapi::filesystem::sync(fd).with_context(|| {
264                format!("failed to sync '{}'", self.path.display())
265            })?;
266        }
267
268        let list = btrfs_uapi::quota::qgroup_list(fd).with_context(|| {
269            format!("failed to list qgroups on '{}'", self.path.display())
270        })?;
271
272        if list.qgroups.is_empty() {
273            return Ok(());
274        }
275
276        if list.status_flags.contains(QgroupStatusFlags::INCONSISTENT) {
277            eprintln!(
278                "WARNING: qgroup data is inconsistent, use 'btrfs quota rescan' to fix"
279            );
280        }
281
282        // Determine display mode
283        let si = self.si;
284        let mode = if self.raw {
285            SizeFormat::Raw
286        } else if self.kbytes {
287            SizeFormat::Fixed(if si { 1000 } else { 1024 })
288        } else if self.mbytes {
289            SizeFormat::Fixed(if si { 1_000_000 } else { 1024 * 1024 })
290        } else if self.gbytes {
291            SizeFormat::Fixed(if si {
292                1_000_000_000
293            } else {
294                1024 * 1024 * 1024
295            })
296        } else if self.tbytes {
297            SizeFormat::Fixed(if si {
298                1_000_000_000_000
299            } else {
300                1024u64.pow(4)
301            })
302        } else if si {
303            SizeFormat::HumanSi
304        } else {
305            SizeFormat::HumanIec
306        };
307
308        // Sort
309        let mut qgroups: Vec<QgroupInfo> = list.qgroups.clone();
310
311        match &self.sort {
312            Some(SortKeys(keys)) => {
313                qgroups.sort_by(|a, b| {
314                    for key in keys {
315                        let ord = match key.field {
316                            SortField::Qgroupid => a.qgroupid.cmp(&b.qgroupid),
317                            SortField::Rfer => a.rfer.cmp(&b.rfer),
318                            SortField::Excl => a.excl.cmp(&b.excl),
319                            SortField::MaxRfer => a.max_rfer.cmp(&b.max_rfer),
320                            SortField::MaxExcl => a.max_excl.cmp(&b.max_excl),
321                        };
322                        let ord =
323                            if key.descending { ord.reverse() } else { ord };
324                        if ord != std::cmp::Ordering::Equal {
325                            return ord;
326                        }
327                    }
328                    std::cmp::Ordering::Equal
329                });
330            }
331            None => {
332                qgroups.sort_by_key(|q| q.qgroupid);
333            }
334        }
335
336        match ctx.format {
337            Format::Modern => {
338                print_qgroups_modern(self, &qgroups, &mode);
339            }
340            Format::Text => {
341                print_qgroups_text(self, &qgroups, &mode);
342            }
343            Format::Json => {
344                let json: Vec<QgroupJson> =
345                    qgroups.iter().map(QgroupJson::from_info).collect();
346                print_json("qgroup-show", &json)?;
347            }
348        }
349
350        Ok(())
351    }
352}
353
354#[derive(Cols)]
355struct QgroupRow {
356    #[column(tree)]
357    qgroupid: String,
358    #[column(header = "RFER", right)]
359    rfer: String,
360    #[column(header = "EXCL", right)]
361    excl: String,
362    #[column(header = "MAX_RFER", right)]
363    max_rfer: String,
364    #[column(header = "MAX_EXCL", right)]
365    max_excl: String,
366    #[column(children)]
367    children: Vec<Self>,
368}
369
370impl QgroupRow {
371    fn from_info(q: &QgroupInfo, mode: &SizeFormat) -> Self {
372        Self {
373            qgroupid: format_qgroupid(q.qgroupid),
374            rfer: fmt_size(q.rfer, mode),
375            excl: fmt_size(q.excl, mode),
376            max_rfer: fmt_limit(
377                q.max_rfer,
378                q.limit_flags,
379                QgroupLimitFlags::MAX_RFER,
380                mode,
381            ),
382            max_excl: fmt_limit(
383                q.max_excl,
384                q.limit_flags,
385                QgroupLimitFlags::MAX_EXCL,
386                mode,
387            ),
388            children: Vec::new(),
389        }
390    }
391}
392
393/// Recursively remove a row from the map and attach its children.
394fn attach_qgroup_children(
395    id: u64,
396    rows: &mut std::collections::BTreeMap<u64, QgroupRow>,
397    qgroups: &[QgroupInfo],
398) -> Option<QgroupRow> {
399    let mut row = rows.remove(&id)?;
400    if let Some(q) = qgroups.iter().find(|q| q.qgroupid == id) {
401        for &child_id in &q.children {
402            if let Some(child) = attach_qgroup_children(child_id, rows, qgroups)
403            {
404                row.children.push(child);
405            }
406        }
407    }
408    Some(row)
409}
410
411fn print_qgroups_modern(
412    _cmd: &QgroupShowCommand,
413    qgroups: &[QgroupInfo],
414    mode: &SizeFormat,
415) {
416    use std::collections::BTreeMap;
417
418    let mut rows: BTreeMap<u64, QgroupRow> = qgroups
419        .iter()
420        .map(|q| (q.qgroupid, QgroupRow::from_info(q, mode)))
421        .collect();
422
423    // A qgroup is a root if no other qgroup lists it as a child.
424    let mut is_child = std::collections::HashSet::new();
425    for q in qgroups {
426        for &child_id in &q.children {
427            is_child.insert(child_id);
428        }
429    }
430
431    let root_ids: Vec<u64> = qgroups
432        .iter()
433        .filter(|q| !is_child.contains(&q.qgroupid))
434        .map(|q| q.qgroupid)
435        .collect();
436
437    let mut tree: Vec<QgroupRow> = root_ids
438        .iter()
439        .filter_map(|&id| attach_qgroup_children(id, &mut rows, qgroups))
440        .collect();
441
442    // Any orphans (not reachable from roots) go at the top level.
443    for (_, row) in rows {
444        tree.push(row);
445    }
446
447    let mut out = std::io::stdout().lock();
448    let _ = QgroupRow::print_table(&tree, &mut out);
449}
450
451fn print_qgroups_text(
452    cmd: &QgroupShowCommand,
453    qgroups: &[QgroupInfo],
454    mode: &SizeFormat,
455) {
456    let mut header =
457        format!("{:<16} {:>12} {:>12}", "qgroupid", "rfer", "excl");
458    if cmd.print_rfer_limit {
459        let _ = write!(header, " {:>12}", "max_rfer");
460    }
461    if cmd.print_excl_limit {
462        let _ = write!(header, " {:>12}", "max_excl");
463    }
464    if cmd.print_parent {
465        let _ = write!(header, "  {:<20}", "parent");
466    }
467    if cmd.print_child {
468        let _ = write!(header, "  {:<20}", "child");
469    }
470    println!("{header}");
471
472    for q in qgroups {
473        let id_str = format_qgroupid(q.qgroupid);
474        let rfer_str = fmt_size(q.rfer, mode);
475        let excl_str = fmt_size(q.excl, mode);
476
477        let mut line = format!("{id_str:<16} {rfer_str:>12} {excl_str:>12}");
478
479        if cmd.print_rfer_limit {
480            let s = fmt_limit(
481                q.max_rfer,
482                q.limit_flags,
483                QgroupLimitFlags::MAX_RFER,
484                mode,
485            );
486            let _ = write!(line, " {s:>12}");
487        }
488
489        if cmd.print_excl_limit {
490            let s = fmt_limit(
491                q.max_excl,
492                q.limit_flags,
493                QgroupLimitFlags::MAX_EXCL,
494                mode,
495            );
496            let _ = write!(line, " {s:>12}");
497        }
498
499        if cmd.print_parent {
500            let parents: Vec<String> =
501                q.parents.iter().map(|&id| format_qgroupid(id)).collect();
502            let _ = write!(line, "  {:<20}", parents.join(","));
503        }
504
505        if cmd.print_child {
506            let children: Vec<String> =
507                q.children.iter().map(|&id| format_qgroupid(id)).collect();
508            let _ = write!(line, "  {:<20}", children.join(","));
509        }
510
511        println!("{line}");
512    }
513}