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