1use crate::{
2 Format, Runnable,
3 util::{SizeFormat, fmt_size},
4};
5use anyhow::{Context, Result};
6use btrfs_uapi::{
7 chunk::device_chunk_allocations, device::device_info_all,
8 filesystem::filesystem_info, space::BlockGroupFlags,
9};
10use clap::Parser;
11use std::{fs::File, os::unix::io::AsFd, path::PathBuf};
12
13#[derive(Parser, Debug)]
20pub struct DeviceUsageCommand {
21 #[clap(required = true)]
23 pub paths: Vec<PathBuf>,
24
25 #[clap(short = 'b', long, overrides_with_all = ["human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
27 pub raw: bool,
28
29 #[clap(long, overrides_with_all = ["raw", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
31 pub human_readable: bool,
32
33 #[clap(short = 'H', overrides_with_all = ["raw", "human_readable", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
35 pub human_base1000: bool,
36
37 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
39 pub iec: bool,
40
41 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "kbytes", "mbytes", "gbytes", "tbytes"])]
43 pub si: bool,
44
45 #[clap(short = 'k', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "mbytes", "gbytes", "tbytes"])]
47 pub kbytes: bool,
48
49 #[clap(short = 'm', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "gbytes", "tbytes"])]
51 pub mbytes: bool,
52
53 #[clap(short = 'g', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
55 pub gbytes: bool,
56
57 #[clap(short = 't', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
59 pub tbytes: bool,
60}
61
62fn physical_device_size(path: &str) -> u64 {
65 if path.is_empty() {
66 return 0;
67 }
68 let Ok(file) = File::open(path) else {
69 return 0;
70 };
71 btrfs_uapi::blkdev::device_size(file.as_fd()).unwrap_or(0)
72}
73
74impl DeviceUsageCommand {
75 fn size_format(&self) -> SizeFormat {
76 let si = self.si;
77 if self.raw {
78 SizeFormat::Raw
79 } else if self.kbytes {
80 SizeFormat::Fixed(if si { 1000 } else { 1024 })
81 } else if self.mbytes {
82 SizeFormat::Fixed(if si { 1_000_000 } else { 1024 * 1024 })
83 } else if self.gbytes {
84 SizeFormat::Fixed(if si {
85 1_000_000_000
86 } else {
87 1024 * 1024 * 1024
88 })
89 } else if self.tbytes {
90 SizeFormat::Fixed(if si {
91 1_000_000_000_000
92 } else {
93 1024u64.pow(4)
94 })
95 } else if si || self.human_base1000 {
96 SizeFormat::HumanSi
97 } else {
98 SizeFormat::HumanIec
99 }
100 }
101}
102
103impl Runnable for DeviceUsageCommand {
104 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
105 let mode = self.size_format();
106 for (i, path) in self.paths.iter().enumerate() {
107 if i > 0 {
108 println!();
109 }
110 print_device_usage(path, &mode)?;
111 }
112 Ok(())
113 }
114}
115
116fn print_device_usage(path: &std::path::Path, mode: &SizeFormat) -> Result<()> {
117 let file = File::open(path)
118 .with_context(|| format!("failed to open '{}'", path.display()))?;
119 let fd = file.as_fd();
120
121 let fs = filesystem_info(fd).with_context(|| {
122 format!("failed to get filesystem info for '{}'", path.display())
123 })?;
124 let devices = device_info_all(fd, &fs).with_context(|| {
125 format!("failed to get device info for '{}'", path.display())
126 })?;
127 let allocs = device_chunk_allocations(fd).with_context(|| {
128 format!("failed to get chunk allocations for '{}'", path.display())
129 })?;
130
131 for (di, dev) in devices.iter().enumerate() {
132 if di > 0 {
133 println!();
134 }
135
136 let phys_size = physical_device_size(&dev.path);
137 let slack = if phys_size > 0 {
138 phys_size.saturating_sub(dev.total_bytes)
139 } else {
140 0
141 };
142
143 println!("{}, ID: {}", dev.path, dev.devid);
144
145 print_line("Device size", &fmt_size(dev.total_bytes, mode));
146 print_line("Device slack", &fmt_size(slack, mode));
147
148 let mut allocated: u64 = 0;
149 let mut dev_allocs: Vec<_> =
150 allocs.iter().filter(|a| a.devid == dev.devid).collect();
151 dev_allocs.sort_by_key(|a| {
152 let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
153 0
154 } else if a.flags.contains(BlockGroupFlags::METADATA) {
155 1
156 } else {
157 2
158 };
159 (type_order, a.flags.bits())
160 });
161
162 for alloc in &dev_allocs {
163 allocated += alloc.bytes;
164 let label = format!(
165 "{},{}",
166 alloc.flags.type_name(),
167 alloc.flags.profile_name()
168 );
169 print_line(&label, &fmt_size(alloc.bytes, mode));
170 }
171
172 let unallocated = dev.total_bytes.saturating_sub(allocated);
173 print_line("Unallocated", &fmt_size(unallocated, mode));
174 }
175
176 Ok(())
177}
178
179fn print_line(label: &str, value: &str) {
180 let padding = 20usize.saturating_sub(label.len());
181 println!(" {label}:{:>pad$}{value:>10}", "", pad = padding);
182}