1use crate::{Format, Runnable, util::human_bytes};
2use anyhow::{Context, Result};
3use btrfs_uapi::{
4 chunk::device_chunk_allocations,
5 device::device_info_all,
6 filesystem::filesystem_info,
7};
8use clap::Parser;
9use std::{fs::File, os::unix::io::AsFd, path::PathBuf};
10
11#[derive(Parser, Debug)]
18pub struct DeviceUsageCommand {
19 #[clap(required = true)]
21 pub paths: Vec<PathBuf>,
22
23 #[clap(short = 'b', long, overrides_with_all = ["human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
25 pub raw: bool,
26
27 #[clap(long, overrides_with_all = ["raw", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
29 pub human_readable: bool,
30
31 #[clap(short = 'H', overrides_with_all = ["raw", "human_readable", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
33 pub human_base1000: bool,
34
35 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
37 pub iec: bool,
38
39 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "kbytes", "mbytes", "gbytes", "tbytes"])]
41 pub si: bool,
42
43 #[clap(short = 'k', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "mbytes", "gbytes", "tbytes"])]
45 pub kbytes: bool,
46
47 #[clap(short = 'm', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "gbytes", "tbytes"])]
49 pub mbytes: bool,
50
51 #[clap(short = 'g', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
53 pub gbytes: bool,
54
55 #[clap(short = 't', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
57 pub tbytes: bool,
58}
59
60enum UnitMode {
62 Raw,
63 HumanIec,
64 HumanSi,
65 Fixed(u64),
66}
67
68fn fmt_size(bytes: u64, mode: &UnitMode) -> String {
69 match mode {
70 UnitMode::Raw => bytes.to_string(),
71 UnitMode::HumanIec => human_bytes(bytes),
72 UnitMode::HumanSi => human_bytes_si(bytes),
73 UnitMode::Fixed(divisor) => format!("{}", bytes / divisor),
74 }
75}
76
77fn human_bytes_si(bytes: u64) -> String {
78 const UNITS: &[&str] = &["B", "kB", "MB", "GB", "TB", "PB"];
79 let mut value = bytes as f64;
80 let mut unit = 0;
81 while value >= 1000.0 && unit + 1 < UNITS.len() {
82 value /= 1000.0;
83 unit += 1;
84 }
85 if unit == 0 {
86 format!("{bytes}B")
87 } else {
88 format!("{value:.2}{}", UNITS[unit])
89 }
90}
91
92fn physical_device_size(path: &str) -> u64 {
95 if path.is_empty() {
96 return 0;
97 }
98 let Ok(file) = File::open(path) else {
99 return 0;
100 };
101 btrfs_uapi::blkdev::device_size(file.as_fd()).unwrap_or(0)
102}
103
104impl DeviceUsageCommand {
105 fn unit_mode(&self) -> UnitMode {
106 if self.raw {
107 UnitMode::Raw
108 } else if self.kbytes {
109 UnitMode::Fixed(if self.si { 1000 } else { 1024 })
110 } else if self.mbytes {
111 UnitMode::Fixed(if self.si { 1000 * 1000 } else { 1024 * 1024 })
112 } else if self.gbytes {
113 UnitMode::Fixed(if self.si {
114 1000 * 1000 * 1000
115 } else {
116 1024 * 1024 * 1024
117 })
118 } else if self.tbytes {
119 UnitMode::Fixed(if self.si {
120 1000u64.pow(4)
121 } else {
122 1024u64.pow(4)
123 })
124 } else if self.si || self.human_base1000 {
125 UnitMode::HumanSi
126 } else {
127 UnitMode::HumanIec
128 }
129 }
130}
131
132impl Runnable for DeviceUsageCommand {
133 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
134 let mode = self.unit_mode();
135 for (i, path) in self.paths.iter().enumerate() {
136 if i > 0 {
137 println!();
138 }
139 print_device_usage(path, &mode)?;
140 }
141 Ok(())
142 }
143}
144
145fn print_device_usage(
146 path: &std::path::Path,
147 mode: &UnitMode,
148) -> Result<()> {
149 let file = File::open(path).with_context(|| {
150 format!("failed to open '{}'", path.display())
151 })?;
152 let fd = file.as_fd();
153
154 let fs = filesystem_info(fd).with_context(|| {
155 format!(
156 "failed to get filesystem info for '{}'",
157 path.display()
158 )
159 })?;
160 let devices = device_info_all(fd, &fs).with_context(|| {
161 format!("failed to get device info for '{}'", path.display())
162 })?;
163 let allocs = device_chunk_allocations(fd).with_context(|| {
164 format!(
165 "failed to get chunk allocations for '{}'",
166 path.display()
167 )
168 })?;
169
170 for (di, dev) in devices.iter().enumerate() {
171 if di > 0 {
172 println!();
173 }
174
175 let phys_size = physical_device_size(&dev.path);
176 let slack = if phys_size > 0 {
177 phys_size.saturating_sub(dev.total_bytes)
178 } else {
179 0
180 };
181
182 println!("{}, ID: {}", dev.path, dev.devid);
183
184 print_line("Device size", &fmt_size(dev.total_bytes, mode));
185 print_line("Device slack", &fmt_size(slack, mode));
186
187 let mut allocated: u64 = 0;
188 let mut dev_allocs: Vec<_> = allocs
189 .iter()
190 .filter(|a| a.devid == dev.devid)
191 .collect();
192 dev_allocs.sort_by_key(|a| {
193 let type_order = if a.flags.contains(
194 btrfs_uapi::space::BlockGroupFlags::DATA,
195 ) {
196 0
197 } else if a.flags.contains(
198 btrfs_uapi::space::BlockGroupFlags::METADATA,
199 ) {
200 1
201 } else {
202 2
203 };
204 (type_order, a.flags.bits())
205 });
206
207 for alloc in &dev_allocs {
208 allocated += alloc.bytes;
209 let label = format!(
210 "{},{}",
211 alloc.flags.type_name(),
212 alloc.flags.profile_name()
213 );
214 print_line(&label, &fmt_size(alloc.bytes, mode));
215 }
216
217 let unallocated = dev.total_bytes.saturating_sub(allocated);
218 print_line("Unallocated", &fmt_size(unallocated, mode));
219 }
220
221 Ok(())
222}
223
224fn print_line(label: &str, value: &str) {
225 let padding = 20usize.saturating_sub(label.len());
226 println!(" {label}:{:>pad$}{value:>10}", "", pad = padding);
227}