1use super::UnitMode;
2use crate::{
3 Format, RunContext, Runnable,
4 util::{SizeFormat, fmt_size},
5};
6use anyhow::{Context, Result};
7use btrfs_uapi::{
8 chunk::device_chunk_allocations,
9 device::device_info_all,
10 filesystem::filesystem_info,
11 space::{BlockGroupFlags, SpaceInfo, space_info},
12};
13use clap::Parser;
14use cols::Cols;
15use std::{
16 collections::{HashMap, HashSet},
17 fs::File,
18 os::unix::io::AsFd,
19 path::PathBuf,
20};
21
22#[derive(Parser, Debug)]
24pub struct FilesystemUsageCommand {
25 #[clap(flatten)]
26 pub units: UnitMode,
27
28 #[clap(short = 'H', long)]
30 pub human_si: bool,
31
32 #[clap(short = 'T', long)]
34 pub tabular: bool,
35
36 #[clap(required = true)]
38 pub paths: Vec<PathBuf>,
39}
40
41fn profile_ncopies(flags: BlockGroupFlags) -> u64 {
44 if flags.contains(BlockGroupFlags::RAID1C4) {
45 4
46 } else if flags.contains(BlockGroupFlags::RAID1C3) {
47 3
48 } else if flags.contains(BlockGroupFlags::RAID1)
49 || flags.contains(BlockGroupFlags::DUP)
50 || flags.contains(BlockGroupFlags::RAID10)
51 {
52 2
53 } else {
54 u64::from(
55 !(flags.contains(BlockGroupFlags::RAID5)
56 || flags.contains(BlockGroupFlags::RAID6)),
57 )
58 }
59}
60
61fn has_multiple_profiles(spaces: &[SpaceInfo]) -> bool {
62 let profile_mask = BlockGroupFlags::RAID0
63 | BlockGroupFlags::RAID1
64 | BlockGroupFlags::DUP
65 | BlockGroupFlags::RAID10
66 | BlockGroupFlags::RAID5
67 | BlockGroupFlags::RAID6
68 | BlockGroupFlags::RAID1C3
69 | BlockGroupFlags::RAID1C4
70 | BlockGroupFlags::SINGLE;
71
72 let profiles_for = |type_flag: BlockGroupFlags| {
73 spaces
74 .iter()
75 .filter(|s| {
76 s.flags.contains(type_flag)
77 && !s.flags.contains(BlockGroupFlags::GLOBAL_RSV)
78 })
79 .map(|s| s.flags & profile_mask)
80 .collect::<HashSet<_>>()
81 };
82
83 profiles_for(BlockGroupFlags::DATA).len() > 1
84 || profiles_for(BlockGroupFlags::METADATA).len() > 1
85 || profiles_for(BlockGroupFlags::SYSTEM).len() > 1
86}
87
88impl Runnable for FilesystemUsageCommand {
89 fn run(&self, ctx: &RunContext) -> Result<()> {
90 let mut mode = self.units.resolve();
91 if self.human_si {
92 mode = SizeFormat::HumanSi;
93 }
94 for (i, path) in self.paths.iter().enumerate() {
95 if i > 0 {
96 println!();
97 }
98 match ctx.format {
99 Format::Modern => print_usage_modern(path, &mode)?,
100 Format::Text | Format::Json => {
101 print_usage(path, self.tabular, &mode)?;
102 }
103 }
104 }
105 Ok(())
106 }
107}
108
109const MIN_UNALLOCATED_THRESH: u64 = 16 * 1024 * 1024;
110
111struct OverallStats {
113 r_total_size: u64,
114 r_total_chunks: u64,
115 r_total_unused: u64,
116 r_total_missing: u64,
117 r_total_used: u64,
118 data_ratio: f64,
119 meta_ratio: f64,
120 free_estimated: u64,
121 free_min: u64,
122 free_statfs: u64,
123 l_global_reserve: u64,
124 l_global_reserve_used: u64,
125 multiple: bool,
126}
127
128#[allow(
129 clippy::cast_precision_loss,
130 clippy::cast_possible_truncation,
131 clippy::cast_sign_loss
132)]
133fn compute_overall(
134 path: &std::path::Path,
135 devices: &[btrfs_uapi::device::DeviceInfo],
136 spaces: &[SpaceInfo],
137) -> OverallStats {
138 let mut r_data_chunks: u64 = 0;
139 let mut r_data_used: u64 = 0;
140 let mut l_data_chunks: u64 = 0;
141 let mut r_meta_chunks: u64 = 0;
142 let mut r_meta_used: u64 = 0;
143 let mut l_meta_chunks: u64 = 0;
144 let mut r_sys_chunks: u64 = 0;
145 let mut r_sys_used: u64 = 0;
146 let mut l_global_reserve: u64 = 0;
147 let mut l_global_reserve_used: u64 = 0;
148 let mut max_ncopies: u64 = 1;
149
150 for s in spaces {
151 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
152 l_global_reserve = s.total_bytes;
153 l_global_reserve_used = s.used_bytes;
154 continue;
155 }
156 let ncopies = profile_ncopies(s.flags);
157 if ncopies > max_ncopies {
158 max_ncopies = ncopies;
159 }
160 if s.flags.contains(BlockGroupFlags::DATA) {
161 r_data_chunks += s.total_bytes * ncopies;
162 r_data_used += s.used_bytes * ncopies;
163 l_data_chunks += s.total_bytes;
164 }
165 if s.flags.contains(BlockGroupFlags::METADATA) {
166 r_meta_chunks += s.total_bytes * ncopies;
167 r_meta_used += s.used_bytes * ncopies;
168 l_meta_chunks += s.total_bytes;
169 }
170 if s.flags.contains(BlockGroupFlags::SYSTEM) {
171 r_sys_chunks += s.total_bytes * ncopies;
172 r_sys_used += s.used_bytes * ncopies;
173 }
174 }
175
176 let r_total_size: u64 = devices.iter().map(|d| d.total_bytes).sum();
177 let r_total_chunks = r_data_chunks + r_meta_chunks + r_sys_chunks;
178 let r_total_used = r_data_used + r_meta_used + r_sys_used;
179 let r_total_unused = r_total_size.saturating_sub(r_total_chunks);
180
181 let r_total_missing: u64 = devices
182 .iter()
183 .filter(|d| std::fs::metadata(&d.path).is_err())
184 .map(|d| d.total_bytes)
185 .sum();
186
187 let data_ratio = if l_data_chunks > 0 {
188 r_data_chunks as f64 / l_data_chunks as f64
189 } else {
190 1.0
191 };
192 let meta_ratio = if l_meta_chunks > 0 {
193 r_meta_chunks as f64 / l_meta_chunks as f64
194 } else {
195 1.0
196 };
197 let max_data_ratio = max_ncopies as f64;
198
199 let free_base = if data_ratio > 0.0 {
200 ((r_data_chunks.saturating_sub(r_data_used)) as f64 / data_ratio) as u64
201 } else {
202 0
203 };
204 let (free_estimated, free_min) = if r_total_unused >= MIN_UNALLOCATED_THRESH
205 {
206 (
207 free_base + (r_total_unused as f64 / data_ratio) as u64,
208 free_base + (r_total_unused as f64 / max_data_ratio) as u64,
209 )
210 } else {
211 (free_base, free_base)
212 };
213
214 let free_statfs = nix::sys::statfs::statfs(path)
215 .map(|st| st.blocks_available() * st.block_size() as u64)
216 .unwrap_or(0);
217
218 let multiple = has_multiple_profiles(spaces);
219
220 OverallStats {
221 r_total_size,
222 r_total_chunks,
223 r_total_unused,
224 r_total_missing,
225 r_total_used,
226 data_ratio,
227 meta_ratio,
228 free_estimated,
229 free_min,
230 free_statfs,
231 l_global_reserve,
232 l_global_reserve_used,
233 multiple,
234 }
235}
236
237#[allow(
238 clippy::too_many_lines,
239 clippy::cast_precision_loss,
240 clippy::cast_possible_truncation,
241 clippy::cast_sign_loss
242)]
243fn print_usage(
244 path: &std::path::Path,
245 _tabular: bool,
246 mode: &SizeFormat,
247) -> Result<()> {
248 let file = File::open(path)
249 .with_context(|| format!("failed to open '{}'", path.display()))?;
250 let fd = file.as_fd();
251
252 let fs = filesystem_info(fd).with_context(|| {
253 format!("failed to get filesystem info for '{}'", path.display())
254 })?;
255 let devices = device_info_all(fd, &fs).with_context(|| {
256 format!("failed to get device info for '{}'", path.display())
257 })?;
258 let spaces = space_info(fd).with_context(|| {
259 format!("failed to get space info for '{}'", path.display())
260 })?;
261
262 let chunk_allocs = device_chunk_allocations(fd).ok();
265
266 let devid_to_path: HashMap<u64, &str> =
268 devices.iter().map(|d| (d.devid, d.path.as_str())).collect();
269
270 let stats = compute_overall(path, &devices, &spaces);
271
272 println!("Overall:");
273 println!(
274 " Device size:\t\t{:>10}",
275 fmt_size(stats.r_total_size, mode)
276 );
277 println!(
278 " Device allocated:\t\t{:>10}",
279 fmt_size(stats.r_total_chunks, mode)
280 );
281 println!(
282 " Device unallocated:\t\t{:>10}",
283 fmt_size(stats.r_total_unused, mode)
284 );
285 println!(
286 " Device missing:\t\t{:>10}",
287 fmt_size(stats.r_total_missing, mode)
288 );
289 println!(" Device slack:\t\t{:>10}", fmt_size(0, mode));
290 println!(" Used:\t\t\t{:>10}", fmt_size(stats.r_total_used, mode));
291 println!(
292 " Free (estimated):\t\t{:>10}\t(min: {})",
293 fmt_size(stats.free_estimated, mode),
294 fmt_size(stats.free_min, mode)
295 );
296 println!(
297 " Free (statfs, df):\t\t{:>10}",
298 fmt_size(stats.free_statfs, mode)
299 );
300 println!(" Data ratio:\t\t\t{:>10.2}", stats.data_ratio);
301 println!(" Metadata ratio:\t\t{:>10.2}", stats.meta_ratio);
302 println!(
303 " Global reserve:\t\t{:>10}\t(used: {})",
304 fmt_size(stats.l_global_reserve, mode),
305 fmt_size(stats.l_global_reserve_used, mode)
306 );
307 println!(
308 " Multiple profiles:\t\t{:>10}",
309 if stats.multiple { "yes" } else { "no" }
310 );
311
312 if chunk_allocs.is_none() {
313 eprintln!(
314 "NOTE: per-device usage breakdown unavailable \
315 (chunk tree requires CAP_SYS_ADMIN)"
316 );
317 }
318
319 for s in &spaces {
320 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
321 continue;
322 }
323 #[allow(clippy::cast_precision_loss)]
324 let pct = if s.total_bytes > 0 {
325 100.0 * s.used_bytes as f64 / s.total_bytes as f64
326 } else {
327 0.0
328 };
329 println!(
330 "\n{},{}: Size:{}, Used:{} ({:.2}%)",
331 s.flags.type_name(),
332 s.flags.profile_name(),
333 fmt_size(s.total_bytes, mode),
334 fmt_size(s.used_bytes, mode),
335 pct
336 );
337
338 if let Some(allocs) = &chunk_allocs {
341 let mut profile_allocs: Vec<_> =
342 allocs.iter().filter(|a| a.flags == s.flags).collect();
343 profile_allocs.sort_by_key(|a| a.devid);
344
345 for alloc in profile_allocs {
346 let path = devid_to_path
347 .get(&alloc.devid)
348 .copied()
349 .unwrap_or("<unknown>");
350 println!(" {}\t\t{:>10}", path, fmt_size(alloc.bytes, mode));
351 }
352 }
353 }
354
355 println!("\nUnallocated:");
356 for dev in &devices {
357 let unallocated = dev.total_bytes.saturating_sub(dev.bytes_used);
358 println!(" {}\t{:>10}", dev.path, fmt_size(unallocated, mode));
359 }
360
361 Ok(())
362}
363
364#[derive(Cols)]
367struct OverallRow {
368 #[column(header = "PROPERTY")]
369 label: String,
370 #[column(header = "VALUE", right)]
371 value: String,
372}
373
374#[derive(Cols)]
375struct ProfileRow {
376 #[column(header = "TYPE")]
377 bg_type: String,
378 #[column(header = "PROFILE")]
379 profile: String,
380 #[column(header = "TOTAL", right)]
381 total: String,
382 #[column(header = "USED", right)]
383 used: String,
384 #[column(header = "USED%", right)]
385 pct: String,
386}
387
388#[allow(
389 clippy::too_many_lines,
390 clippy::cast_precision_loss,
391 clippy::cast_possible_truncation,
392 clippy::cast_sign_loss
393)]
394fn print_usage_modern(path: &std::path::Path, mode: &SizeFormat) -> Result<()> {
395 let file = File::open(path)
396 .with_context(|| format!("failed to open '{}'", path.display()))?;
397 let fd = file.as_fd();
398
399 let fs = filesystem_info(fd).with_context(|| {
400 format!("failed to get filesystem info for '{}'", path.display())
401 })?;
402 let devices = device_info_all(fd, &fs).with_context(|| {
403 format!("failed to get device info for '{}'", path.display())
404 })?;
405 let spaces = space_info(fd).with_context(|| {
406 format!("failed to get space info for '{}'", path.display())
407 })?;
408 let chunk_allocs = device_chunk_allocations(fd).ok();
409
410 let stats = compute_overall(path, &devices, &spaces);
411
412 println!("Overall:");
414 let mut overall_rows = vec![
415 OverallRow {
416 label: "Device size".to_string(),
417 value: fmt_size(stats.r_total_size, mode),
418 },
419 OverallRow {
420 label: "Device allocated".to_string(),
421 value: fmt_size(stats.r_total_chunks, mode),
422 },
423 OverallRow {
424 label: "Device unallocated".to_string(),
425 value: fmt_size(stats.r_total_unused, mode),
426 },
427 OverallRow {
428 label: "Device missing".to_string(),
429 value: fmt_size(stats.r_total_missing, mode),
430 },
431 OverallRow {
432 label: "Device slack".to_string(),
433 value: fmt_size(0, mode),
434 },
435 OverallRow {
436 label: "Used".to_string(),
437 value: fmt_size(stats.r_total_used, mode),
438 },
439 OverallRow {
440 label: "Free (estimated)".to_string(),
441 value: format!(
442 "{} (min: {})",
443 fmt_size(stats.free_estimated, mode),
444 fmt_size(stats.free_min, mode)
445 ),
446 },
447 OverallRow {
448 label: "Free (statfs, df)".to_string(),
449 value: fmt_size(stats.free_statfs, mode),
450 },
451 OverallRow {
452 label: "Data ratio".to_string(),
453 value: format!("{:.2}", stats.data_ratio),
454 },
455 OverallRow {
456 label: "Metadata ratio".to_string(),
457 value: format!("{:.2}", stats.meta_ratio),
458 },
459 ];
460
461 overall_rows.push(OverallRow {
462 label: "Global reserve".to_string(),
463 value: format!(
464 "{} (used: {})",
465 fmt_size(stats.l_global_reserve, mode),
466 fmt_size(stats.l_global_reserve_used, mode)
467 ),
468 });
469 overall_rows.push(OverallRow {
470 label: "Multiple profiles".to_string(),
471 value: if stats.multiple { "yes" } else { "no" }.to_string(),
472 });
473
474 let mut out = std::io::stdout().lock();
475 let _ = OverallRow::print_table(&overall_rows, &mut out);
476
477 if chunk_allocs.is_none() {
478 eprintln!(
479 "NOTE: per-device usage breakdown unavailable \
480 (chunk tree requires CAP_SYS_ADMIN)"
481 );
482 }
483
484 let profile_rows: Vec<ProfileRow> = spaces
486 .iter()
487 .filter(|s| !s.flags.contains(BlockGroupFlags::GLOBAL_RSV))
488 .map(|s| {
489 let pct = if s.total_bytes > 0 {
490 100.0 * s.used_bytes as f64 / s.total_bytes as f64
491 } else {
492 0.0
493 };
494 ProfileRow {
495 bg_type: s.flags.type_name().to_string(),
496 profile: s.flags.profile_name().to_string(),
497 total: fmt_size(s.total_bytes, mode),
498 used: fmt_size(s.used_bytes, mode),
499 pct: format!("{pct:.2}%"),
500 }
501 })
502 .collect();
503
504 if !profile_rows.is_empty() {
505 println!();
506 let _ = ProfileRow::print_table(&profile_rows, &mut out);
507 }
508
509 if let Some(allocs) = &chunk_allocs {
511 let mut profile_flags: Vec<BlockGroupFlags> = Vec::new();
513 for s in &spaces {
514 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
515 continue;
516 }
517 if !profile_flags.contains(&s.flags) {
518 profile_flags.push(s.flags);
519 }
520 }
521
522 let mut table = cols::Table::new();
523 table.add_column(cols::Column::new("PATH"));
524 for flags in &profile_flags {
525 table.add_column(
526 cols::Column::new(&format!(
527 "{},{}",
528 flags.type_name(),
529 flags.profile_name()
530 ))
531 .right(true),
532 );
533 }
534 table.add_column(cols::Column::new("UNALLOC").right(true));
535
536 for dev in &devices {
537 let line = table.new_line(None);
538 let row = table.line_mut(line);
539 row.data_set(0, &dev.path);
540
541 let mut allocated: u64 = 0;
542 for (ci, flags) in profile_flags.iter().enumerate() {
543 let bytes: u64 = allocs
544 .iter()
545 .filter(|a| a.devid == dev.devid && a.flags == *flags)
546 .map(|a| a.bytes)
547 .sum();
548 allocated += bytes;
549 if bytes > 0 {
550 row.data_set(ci + 1, &fmt_size(bytes, mode));
551 } else {
552 row.data_set(ci + 1, "-");
553 }
554 }
555
556 let unallocated = dev.total_bytes.saturating_sub(allocated);
557 row.data_set(profile_flags.len() + 1, &fmt_size(unallocated, mode));
558 }
559
560 println!();
561 let _ = cols::print_table(&table, &mut out);
562 }
563
564 Ok(())
565}