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#[derive(Parser, Debug)]
19pub struct QgroupShowCommand {
20 pub path: PathBuf,
22
23 #[clap(short = 'p', long, help_heading = HEADING_COLUMN_SELECTION)]
25 pub print_parent: bool,
26
27 #[clap(short = 'c', long, help_heading = HEADING_COLUMN_SELECTION)]
29 pub print_child: bool,
30
31 #[clap(short = 'r', long, help_heading = HEADING_COLUMN_SELECTION)]
33 pub print_rfer_limit: bool,
34
35 #[clap(short = 'e', long, help_heading = HEADING_COLUMN_SELECTION)]
37 pub print_excl_limit: bool,
38
39 #[clap(short = 'F', long, help_heading = HEADING_FILTERING)]
41 pub filter_all: bool,
42
43 #[clap(short = 'f', long, help_heading = HEADING_FILTERING)]
45 pub filter_direct: bool,
46
47 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
49 pub raw: bool,
50
51 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
53 pub human_readable: bool,
54
55 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
57 pub iec: bool,
58
59 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
61 pub si: bool,
62
63 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
65 pub kbytes: bool,
66
67 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
69 pub mbytes: bool,
70
71 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
73 pub gbytes: bool,
74
75 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
77 pub tbytes: bool,
78
79 #[clap(long)]
81 pub sort: Option<SortKeys>,
82
83 #[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#[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 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 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 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 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}