1use crate::{
2 Format, RunContext, 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 cols::Cols;
12use std::{fs::File, os::unix::io::AsFd, path::PathBuf};
13
14#[derive(Parser, Debug)]
21#[allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
22pub struct DeviceUsageCommand {
23 #[clap(required = true)]
25 pub paths: Vec<PathBuf>,
26
27 #[clap(short = 'b', long, overrides_with_all = ["human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
29 pub raw: bool,
30
31 #[clap(long, overrides_with_all = ["raw", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
33 pub human_readable: bool,
34
35 #[clap(short = 'H', overrides_with_all = ["raw", "human_readable", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
37 pub human_base1000: bool,
38
39 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
41 pub iec: bool,
42
43 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "kbytes", "mbytes", "gbytes", "tbytes"])]
45 pub si: bool,
46
47 #[clap(short = 'k', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "mbytes", "gbytes", "tbytes"])]
49 pub kbytes: bool,
50
51 #[clap(short = 'm', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "gbytes", "tbytes"])]
53 pub mbytes: bool,
54
55 #[clap(short = 'g', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
57 pub gbytes: bool,
58
59 #[clap(short = 't', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
61 pub tbytes: bool,
62}
63
64fn physical_device_size(path: &str) -> u64 {
67 if path.is_empty() {
68 return 0;
69 }
70 let Ok(file) = File::open(path) else {
71 return 0;
72 };
73 btrfs_uapi::blkdev::device_size(file.as_fd()).unwrap_or(0)
74}
75
76impl DeviceUsageCommand {
77 fn size_format(&self) -> SizeFormat {
78 let si = self.si;
79 if self.raw {
80 SizeFormat::Raw
81 } else if self.kbytes {
82 SizeFormat::Fixed(if si { 1000 } else { 1024 })
83 } else if self.mbytes {
84 SizeFormat::Fixed(if si { 1_000_000 } else { 1024 * 1024 })
85 } else if self.gbytes {
86 SizeFormat::Fixed(if si {
87 1_000_000_000
88 } else {
89 1024 * 1024 * 1024
90 })
91 } else if self.tbytes {
92 SizeFormat::Fixed(if si {
93 1_000_000_000_000
94 } else {
95 1024u64.pow(4)
96 })
97 } else if si || self.human_base1000 {
98 SizeFormat::HumanSi
99 } else {
100 SizeFormat::HumanIec
101 }
102 }
103}
104
105impl Runnable for DeviceUsageCommand {
106 fn run(&self, ctx: &RunContext) -> Result<()> {
107 let mode = self.size_format();
108 match ctx.format {
109 Format::Modern => {
110 for (i, path) in self.paths.iter().enumerate() {
111 if i > 0 {
112 println!();
113 }
114 print_device_usage_modern(path, &mode)?;
115 }
116 }
117 Format::Text | Format::Json => {
118 for (i, path) in self.paths.iter().enumerate() {
119 if i > 0 {
120 println!();
121 }
122 print_device_usage(path, &mode)?;
123 }
124 }
125 }
126 Ok(())
127 }
128}
129
130fn print_device_usage(path: &std::path::Path, mode: &SizeFormat) -> Result<()> {
131 let file = File::open(path)
132 .with_context(|| format!("failed to open '{}'", path.display()))?;
133 let fd = file.as_fd();
134
135 let fs = filesystem_info(fd).with_context(|| {
136 format!("failed to get filesystem info for '{}'", path.display())
137 })?;
138 let devices = device_info_all(fd, &fs).with_context(|| {
139 format!("failed to get device info for '{}'", path.display())
140 })?;
141 let allocs = device_chunk_allocations(fd).with_context(|| {
142 format!("failed to get chunk allocations for '{}'", path.display())
143 })?;
144
145 for (di, dev) in devices.iter().enumerate() {
146 if di > 0 {
147 println!();
148 }
149
150 let phys_size = physical_device_size(&dev.path);
151 let slack = if phys_size > 0 {
152 phys_size.saturating_sub(dev.total_bytes)
153 } else {
154 0
155 };
156
157 println!("{}, ID: {}", dev.path, dev.devid);
158
159 print_line("Device size", &fmt_size(dev.total_bytes, mode));
160 print_line("Device slack", &fmt_size(slack, mode));
161
162 let mut allocated: u64 = 0;
163 let mut dev_allocs: Vec<_> =
164 allocs.iter().filter(|a| a.devid == dev.devid).collect();
165 dev_allocs.sort_by_key(|a| {
166 let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
167 0
168 } else if a.flags.contains(BlockGroupFlags::METADATA) {
169 1
170 } else {
171 2
172 };
173 (type_order, a.flags.bits())
174 });
175
176 for alloc in &dev_allocs {
177 allocated += alloc.bytes;
178 let label = format!(
179 "{},{}",
180 alloc.flags.type_name(),
181 alloc.flags.profile_name()
182 );
183 print_line(&label, &fmt_size(alloc.bytes, mode));
184 }
185
186 let unallocated = dev.total_bytes.saturating_sub(allocated);
187 print_line("Unallocated", &fmt_size(unallocated, mode));
188 }
189
190 Ok(())
191}
192
193fn print_line(label: &str, value: &str) {
194 let padding = 20usize.saturating_sub(label.len());
195 println!(" {label}:{:>pad$}{value:>10}", "", pad = padding);
196}
197
198#[derive(Cols)]
199struct UsageRow {
200 #[column(tree)]
201 name: String,
202 #[column(header = "SIZE", right)]
203 size: String,
204 #[column(children)]
205 children: Vec<Self>,
206}
207
208fn print_device_usage_modern(
209 path: &std::path::Path,
210 mode: &SizeFormat,
211) -> Result<()> {
212 let file = File::open(path)
213 .with_context(|| format!("failed to open '{}'", path.display()))?;
214 let fd = file.as_fd();
215
216 let fs = filesystem_info(fd).with_context(|| {
217 format!("failed to get filesystem info for '{}'", path.display())
218 })?;
219 let devices = device_info_all(fd, &fs).with_context(|| {
220 format!("failed to get device info for '{}'", path.display())
221 })?;
222 let allocs = device_chunk_allocations(fd).with_context(|| {
223 format!("failed to get chunk allocations for '{}'", path.display())
224 })?;
225
226 let mut roots: Vec<UsageRow> = Vec::new();
227
228 for dev in &devices {
229 let phys_size = physical_device_size(&dev.path);
230 let slack = if phys_size > 0 {
231 phys_size.saturating_sub(dev.total_bytes)
232 } else {
233 0
234 };
235
236 let mut children: Vec<UsageRow> = Vec::new();
237 let mut allocated: u64 = 0;
238
239 let mut dev_allocs: Vec<_> =
240 allocs.iter().filter(|a| a.devid == dev.devid).collect();
241 dev_allocs.sort_by_key(|a| {
242 let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
243 0
244 } else if a.flags.contains(BlockGroupFlags::METADATA) {
245 1
246 } else {
247 2
248 };
249 (type_order, a.flags.bits())
250 });
251
252 for alloc in &dev_allocs {
253 allocated += alloc.bytes;
254 children.push(UsageRow {
255 name: format!(
256 "{},{}",
257 alloc.flags.type_name(),
258 alloc.flags.profile_name()
259 ),
260 size: fmt_size(alloc.bytes, mode),
261 children: Vec::new(),
262 });
263 }
264
265 let unallocated = dev.total_bytes.saturating_sub(allocated);
266 children.push(UsageRow {
267 name: "Unallocated".to_string(),
268 size: fmt_size(unallocated, mode),
269 children: Vec::new(),
270 });
271
272 if slack > 0 {
273 children.push(UsageRow {
274 name: "Slack".to_string(),
275 size: fmt_size(slack, mode),
276 children: Vec::new(),
277 });
278 }
279
280 roots.push(UsageRow {
281 name: format!("{}, ID: {}", dev.path, dev.devid),
282 size: fmt_size(dev.total_bytes, mode),
283 children,
284 });
285 }
286
287 let mut out = std::io::stdout().lock();
288 let _ = UsageRow::print_table(&roots, &mut out);
289
290 Ok(())
291}