Skip to main content

btrfs_cli/qgroup/
show.rs

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