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#[derive(Parser, Debug)]
54#[allow(clippy::struct_excessive_bools, clippy::doc_markdown)]
55pub struct QgroupShowCommand {
56 pub path: PathBuf,
58
59 #[clap(short = 'p', long, help_heading = HEADING_COLUMN_SELECTION)]
61 pub print_parent: bool,
62
63 #[clap(short = 'c', long, help_heading = HEADING_COLUMN_SELECTION)]
65 pub print_child: bool,
66
67 #[clap(short = 'r', long, help_heading = HEADING_COLUMN_SELECTION)]
69 pub print_rfer_limit: bool,
70
71 #[clap(short = 'e', long, help_heading = HEADING_COLUMN_SELECTION)]
73 pub print_excl_limit: bool,
74
75 #[clap(short = 'F', long, help_heading = HEADING_FILTERING)]
77 pub filter_all: bool,
78
79 #[clap(short = 'f', long, help_heading = HEADING_FILTERING)]
81 pub filter_direct: bool,
82
83 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
85 pub raw: bool,
86
87 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
89 pub human_readable: bool,
90
91 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
93 pub iec: bool,
94
95 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
97 pub si: bool,
98
99 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
101 pub kbytes: bool,
102
103 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
105 pub mbytes: bool,
106
107 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
109 pub gbytes: bool,
110
111 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
113 pub tbytes: bool,
114
115 #[clap(long)]
117 pub sort: Option<SortKeys>,
118
119 #[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#[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 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 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 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
393fn 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 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 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}