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#[derive(Parser, Debug)]
16pub struct QgroupShowCommand {
17 pub path: PathBuf,
19
20 #[clap(short = 'p', long, help_heading = HEADING_COLUMN_SELECTION)]
22 pub print_parent: bool,
23
24 #[clap(short = 'c', long, help_heading = HEADING_COLUMN_SELECTION)]
26 pub print_child: bool,
27
28 #[clap(short = 'r', long, help_heading = HEADING_COLUMN_SELECTION)]
30 pub print_rfer_limit: bool,
31
32 #[clap(short = 'e', long, help_heading = HEADING_COLUMN_SELECTION)]
34 pub print_excl_limit: bool,
35
36 #[clap(short = 'F', long, help_heading = HEADING_FILTERING)]
38 pub filter_all: bool,
39
40 #[clap(short = 'f', long, help_heading = HEADING_FILTERING)]
42 pub filter_direct: bool,
43
44 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
46 pub raw: bool,
47
48 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
50 pub human_readable: bool,
51
52 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
54 pub iec: bool,
55
56 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
58 pub si: bool,
59
60 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
62 pub kbytes: bool,
63
64 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
66 pub mbytes: bool,
67
68 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
70 pub gbytes: bool,
71
72 #[clap(long, help_heading = HEADING_SIZE_UNITS)]
74 pub tbytes: bool,
75
76 #[clap(long)]
78 pub sort: Option<SortKeys>,
79
80 #[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#[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 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 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 (None, true)
231 } else {
232 (None, false)
234 };
235
236 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 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}