Skip to main content

btrfs_cli/qgroup/
show.rs

1use crate::{
2    Format, Runnable,
3    util::{SizeFormat, fmt_size, open_path},
4};
5use anyhow::{Context, Result};
6use btrfs_uapi::quota::{
7    QgroupInfo, QgroupLimitFlags, QgroupStatusFlags, qgroupid_level,
8    qgroupid_subvolid,
9};
10use clap::Parser;
11use std::{os::unix::io::AsFd, path::PathBuf};
12
13const HEADING_COLUMN_SELECTION: &str = "Column selection";
14const HEADING_FILTERING: &str = "Filtering";
15const HEADING_SIZE_UNITS: &str = "Size units";
16
17/// List subvolume quota groups
18#[derive(Parser, Debug)]
19pub struct QgroupShowCommand {
20    /// Path to a mounted btrfs filesystem
21    pub path: PathBuf,
22
23    /// Print parent qgroup id
24    #[clap(short = 'p', long, help_heading = HEADING_COLUMN_SELECTION)]
25    pub print_parent: bool,
26
27    /// Print child qgroup id
28    #[clap(short = 'c', long, help_heading = HEADING_COLUMN_SELECTION)]
29    pub print_child: bool,
30
31    /// Print limit of referenced size
32    #[clap(short = 'r', long, help_heading = HEADING_COLUMN_SELECTION)]
33    pub print_rfer_limit: bool,
34
35    /// Print limit of exclusive size
36    #[clap(short = 'e', long, help_heading = HEADING_COLUMN_SELECTION)]
37    pub print_excl_limit: bool,
38
39    /// List all qgroups impacting path, including ancestral qgroups
40    #[clap(short = 'F', long, help_heading = HEADING_FILTERING)]
41    pub filter_all: bool,
42
43    /// List all qgroups impacting path, excluding ancestral qgroups
44    #[clap(short = 'f', long, help_heading = HEADING_FILTERING)]
45    pub filter_direct: bool,
46
47    /// Show raw numbers in bytes
48    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
49    pub raw: bool,
50
51    /// Show human friendly numbers, base 1024 (default)
52    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
53    pub human_readable: bool,
54
55    /// Use 1024 as a base (IEC units)
56    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
57    pub iec: bool,
58
59    /// Use 1000 as a base (SI units)
60    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
61    pub si: bool,
62
63    /// Show sizes in KiB
64    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
65    pub kbytes: bool,
66
67    /// Show sizes in MiB
68    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
69    pub mbytes: bool,
70
71    /// Show sizes in GiB
72    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
73    pub gbytes: bool,
74
75    /// Show sizes in TiB
76    #[clap(long, help_heading = HEADING_SIZE_UNITS)]
77    pub tbytes: bool,
78
79    /// Sort by a comma-separated list of fields (qgroupid, rfer, excl, max_rfer, max_excl)
80    #[clap(long)]
81    pub sort: Option<SortKeys>,
82
83    /// Force a sync before getting quota information
84    #[clap(long)]
85    pub sync: bool,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89enum SortField {
90    Qgroupid,
91    Rfer,
92    Excl,
93    MaxRfer,
94    MaxExcl,
95}
96
97#[derive(Debug, Clone, Copy)]
98struct SortKey {
99    field: SortField,
100    descending: bool,
101}
102
103impl std::str::FromStr for SortKey {
104    type Err = String;
105
106    fn from_str(s: &str) -> Result<Self, Self::Err> {
107        let (descending, name) = match s.strip_prefix('-') {
108            Some(rest) => (true, rest),
109            None => (false, s),
110        };
111        let field = match name {
112            "qgroupid" => SortField::Qgroupid,
113            "rfer" => SortField::Rfer,
114            "excl" => SortField::Excl,
115            "max_rfer" => SortField::MaxRfer,
116            "max_excl" => SortField::MaxExcl,
117            _ => {
118                return Err(format!(
119                    "unknown sort field '{name}'; expected qgroupid, rfer, excl, max_rfer, or max_excl"
120                ));
121            }
122        };
123        Ok(SortKey { field, descending })
124    }
125}
126
127/// Comma-separated list of sort keys (e.g. "rfer,-excl").
128#[derive(Debug, Clone)]
129pub struct SortKeys(Vec<SortKey>);
130
131impl std::str::FromStr for SortKeys {
132    type Err = String;
133
134    fn from_str(s: &str) -> Result<Self, Self::Err> {
135        let keys: Vec<SortKey> = s
136            .split(',')
137            .map(|part| part.trim().parse())
138            .collect::<Result<_, _>>()?;
139        if keys.is_empty() {
140            return Err("sort field list must not be empty".to_string());
141        }
142        Ok(SortKeys(keys))
143    }
144}
145
146fn fmt_limit(
147    bytes: u64,
148    flags: QgroupLimitFlags,
149    flag_bit: QgroupLimitFlags,
150    mode: &SizeFormat,
151) -> String {
152    if bytes == u64::MAX || !flags.contains(flag_bit) {
153        "none".to_string()
154    } else {
155        fmt_size(bytes, mode)
156    }
157}
158
159fn format_qgroupid(qgroupid: u64) -> String {
160    format!(
161        "{}/{}",
162        qgroupid_level(qgroupid),
163        qgroupid_subvolid(qgroupid)
164    )
165}
166
167impl Runnable for QgroupShowCommand {
168    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
169        // filter_all / filter_direct: not implemented, ignored
170        let _ = self.filter_all;
171        let _ = self.filter_direct;
172
173        let file = open_path(&self.path)?;
174        let fd = file.as_fd();
175
176        if self.sync {
177            btrfs_uapi::filesystem::sync(fd).with_context(|| {
178                format!("failed to sync '{}'", self.path.display())
179            })?;
180        }
181
182        let list = btrfs_uapi::quota::qgroup_list(fd).with_context(|| {
183            format!("failed to list qgroups on '{}'", self.path.display())
184        })?;
185
186        if list.qgroups.is_empty() {
187            return Ok(());
188        }
189
190        if list.status_flags.contains(QgroupStatusFlags::INCONSISTENT) {
191            eprintln!(
192                "WARNING: qgroup data is inconsistent, use 'btrfs quota rescan' to fix"
193            );
194        }
195
196        // Determine display mode
197        let si = self.si;
198        let mode = if self.raw {
199            SizeFormat::Raw
200        } else if self.kbytes {
201            SizeFormat::Fixed(if si { 1000 } else { 1024 })
202        } else if self.mbytes {
203            SizeFormat::Fixed(if si { 1_000_000 } else { 1024 * 1024 })
204        } else if self.gbytes {
205            SizeFormat::Fixed(if si {
206                1_000_000_000
207            } else {
208                1024 * 1024 * 1024
209            })
210        } else if self.tbytes {
211            SizeFormat::Fixed(if si {
212                1_000_000_000_000
213            } else {
214                1024u64.pow(4)
215            })
216        } else if si {
217            SizeFormat::HumanSi
218        } else {
219            SizeFormat::HumanIec
220        };
221
222        // Sort
223        let mut qgroups: Vec<QgroupInfo> = list.qgroups.clone();
224
225        match &self.sort {
226            Some(SortKeys(keys)) => {
227                qgroups.sort_by(|a, b| {
228                    for key in keys {
229                        let ord = match key.field {
230                            SortField::Qgroupid => a.qgroupid.cmp(&b.qgroupid),
231                            SortField::Rfer => a.rfer.cmp(&b.rfer),
232                            SortField::Excl => a.excl.cmp(&b.excl),
233                            SortField::MaxRfer => a.max_rfer.cmp(&b.max_rfer),
234                            SortField::MaxExcl => a.max_excl.cmp(&b.max_excl),
235                        };
236                        let ord =
237                            if key.descending { ord.reverse() } else { ord };
238                        if ord != std::cmp::Ordering::Equal {
239                            return ord;
240                        }
241                    }
242                    std::cmp::Ordering::Equal
243                });
244            }
245            None => {
246                qgroups.sort_by_key(|q| q.qgroupid);
247            }
248        }
249
250        // Build header
251        let mut header =
252            format!("{:<16} {:>12} {:>12}", "qgroupid", "rfer", "excl");
253        if self.print_rfer_limit {
254            header.push_str(&format!(" {:>12}", "max_rfer"));
255        }
256        if self.print_excl_limit {
257            header.push_str(&format!(" {:>12}", "max_excl"));
258        }
259        if self.print_parent {
260            header.push_str(&format!("  {:<20}", "parent"));
261        }
262        if self.print_child {
263            header.push_str(&format!("  {:<20}", "child"));
264        }
265        println!("{}", header);
266
267        for q in &qgroups {
268            let id_str = format_qgroupid(q.qgroupid);
269            let rfer_str = fmt_size(q.rfer, &mode);
270            let excl_str = fmt_size(q.excl, &mode);
271
272            let mut line =
273                format!("{:<16} {:>12} {:>12}", id_str, rfer_str, excl_str);
274
275            if self.print_rfer_limit {
276                let s = fmt_limit(
277                    q.max_rfer,
278                    q.limit_flags,
279                    QgroupLimitFlags::MAX_RFER,
280                    &mode,
281                );
282                line.push_str(&format!(" {:>12}", s));
283            }
284
285            if self.print_excl_limit {
286                let s = fmt_limit(
287                    q.max_excl,
288                    q.limit_flags,
289                    QgroupLimitFlags::MAX_EXCL,
290                    &mode,
291                );
292                line.push_str(&format!(" {:>12}", s));
293            }
294
295            if self.print_parent {
296                let parents: Vec<String> =
297                    q.parents.iter().map(|&id| format_qgroupid(id)).collect();
298                line.push_str(&format!("  {:<20}", parents.join(",")));
299            }
300
301            if self.print_child {
302                let children: Vec<String> =
303                    q.children.iter().map(|&id| format_qgroupid(id)).collect();
304                line.push_str(&format!("  {:<20}", children.join(",")));
305            }
306
307            println!("{}", line);
308        }
309
310        Ok(())
311    }
312}