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