1use super::UnitMode;
2use crate::{
3 Format, 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 std::{
15 collections::{HashMap, HashSet},
16 fs::File,
17 os::unix::io::AsFd,
18 path::PathBuf,
19};
20
21#[derive(Parser, Debug)]
23pub struct FilesystemUsageCommand {
24 #[clap(flatten)]
25 pub units: UnitMode,
26
27 #[clap(short = 'H', long)]
29 pub human_si: bool,
30
31 #[clap(short = 'T', long)]
33 pub tabular: bool,
34
35 #[clap(required = true)]
37 pub paths: Vec<PathBuf>,
38}
39
40fn profile_ncopies(flags: BlockGroupFlags) -> u64 {
43 if flags.contains(BlockGroupFlags::RAID1C4) {
44 4
45 } else if flags.contains(BlockGroupFlags::RAID1C3) {
46 3
47 } else if flags.contains(BlockGroupFlags::RAID1)
48 || flags.contains(BlockGroupFlags::DUP)
49 || flags.contains(BlockGroupFlags::RAID10)
50 {
51 2
52 } else {
53 u64::from(
54 !(flags.contains(BlockGroupFlags::RAID5)
55 || flags.contains(BlockGroupFlags::RAID6)),
56 )
57 }
58}
59
60fn has_multiple_profiles(spaces: &[SpaceInfo]) -> bool {
61 let profile_mask = BlockGroupFlags::RAID0
62 | BlockGroupFlags::RAID1
63 | BlockGroupFlags::DUP
64 | BlockGroupFlags::RAID10
65 | BlockGroupFlags::RAID5
66 | BlockGroupFlags::RAID6
67 | BlockGroupFlags::RAID1C3
68 | BlockGroupFlags::RAID1C4
69 | BlockGroupFlags::SINGLE;
70
71 let profiles_for = |type_flag: BlockGroupFlags| {
72 spaces
73 .iter()
74 .filter(|s| {
75 s.flags.contains(type_flag)
76 && !s.flags.contains(BlockGroupFlags::GLOBAL_RSV)
77 })
78 .map(|s| s.flags & profile_mask)
79 .collect::<HashSet<_>>()
80 };
81
82 profiles_for(BlockGroupFlags::DATA).len() > 1
83 || profiles_for(BlockGroupFlags::METADATA).len() > 1
84 || profiles_for(BlockGroupFlags::SYSTEM).len() > 1
85}
86
87impl Runnable for FilesystemUsageCommand {
88 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
89 let mut mode = self.units.resolve();
90 if self.human_si {
91 mode = SizeFormat::HumanSi;
92 }
93 for (i, path) in self.paths.iter().enumerate() {
94 if i > 0 {
95 println!();
96 }
97 print_usage(path, self.tabular, &mode)?;
98 }
99 Ok(())
100 }
101}
102
103const MIN_UNALLOCATED_THRESH: u64 = 16 * 1024 * 1024;
104
105#[allow(
106 clippy::too_many_lines,
107 clippy::cast_precision_loss,
108 clippy::cast_possible_truncation,
109 clippy::cast_sign_loss
110)]
111fn print_usage(
112 path: &std::path::Path,
113 _tabular: bool,
114 mode: &SizeFormat,
115) -> Result<()> {
116 let file = File::open(path)
117 .with_context(|| format!("failed to open '{}'", path.display()))?;
118 let fd = file.as_fd();
119
120 let fs = filesystem_info(fd).with_context(|| {
121 format!("failed to get filesystem info for '{}'", path.display())
122 })?;
123 let devices = device_info_all(fd, &fs).with_context(|| {
124 format!("failed to get device info for '{}'", path.display())
125 })?;
126 let spaces = space_info(fd).with_context(|| {
127 format!("failed to get space info for '{}'", path.display())
128 })?;
129
130 let chunk_allocs = device_chunk_allocations(fd).ok();
133
134 let devid_to_path: HashMap<u64, &str> =
136 devices.iter().map(|d| (d.devid, d.path.as_str())).collect();
137
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 #[allow(clippy::cast_precision_loss)]
188 let data_ratio = if l_data_chunks > 0 {
189 r_data_chunks as f64 / l_data_chunks as f64
190 } else {
191 1.0
192 };
193 #[allow(clippy::cast_precision_loss)]
194 let meta_ratio = if l_meta_chunks > 0 {
195 r_meta_chunks as f64 / l_meta_chunks as f64
196 } else {
197 1.0
198 };
199 #[allow(clippy::cast_precision_loss)]
200 let max_data_ratio = max_ncopies as f64;
201
202 #[allow(
203 clippy::cast_precision_loss,
204 clippy::cast_possible_truncation,
205 clippy::cast_sign_loss
206 )]
207 let free_base = if data_ratio > 0.0 {
208 ((r_data_chunks.saturating_sub(r_data_used)) as f64 / data_ratio) as u64
209 } else {
210 0
211 };
212 #[allow(
213 clippy::cast_precision_loss,
214 clippy::cast_possible_truncation,
215 clippy::cast_sign_loss
216 )]
217 let (free_estimated, free_min) = if r_total_unused >= MIN_UNALLOCATED_THRESH
218 {
219 (
220 free_base + (r_total_unused as f64 / data_ratio) as u64,
221 free_base + (r_total_unused as f64 / max_data_ratio) as u64,
222 )
223 } else {
224 (free_base, free_base)
225 };
226
227 #[allow(clippy::cast_sign_loss)]
228 let free_statfs = nix::sys::statfs::statfs(path)
229 .map(|st| st.blocks_available() * st.block_size() as u64)
230 .unwrap_or(0);
231
232 let multiple = has_multiple_profiles(&spaces);
233
234 println!("Overall:");
235 println!(" Device size:\t\t{:>10}", fmt_size(r_total_size, mode));
236 println!(
237 " Device allocated:\t\t{:>10}",
238 fmt_size(r_total_chunks, mode)
239 );
240 println!(
241 " Device unallocated:\t\t{:>10}",
242 fmt_size(r_total_unused, mode)
243 );
244 println!(
245 " Device missing:\t\t{:>10}",
246 fmt_size(r_total_missing, mode)
247 );
248 println!(" Device slack:\t\t{:>10}", fmt_size(0, mode));
249 println!(" Used:\t\t\t{:>10}", fmt_size(r_total_used, mode));
250 println!(
251 " Free (estimated):\t\t{:>10}\t(min: {})",
252 fmt_size(free_estimated, mode),
253 fmt_size(free_min, mode)
254 );
255 println!(
256 " Free (statfs, df):\t\t{:>10}",
257 fmt_size(free_statfs, mode)
258 );
259 println!(" Data ratio:\t\t\t{data_ratio:>10.2}");
260 println!(" Metadata ratio:\t\t{meta_ratio:>10.2}");
261 println!(
262 " Global reserve:\t\t{:>10}\t(used: {})",
263 fmt_size(l_global_reserve, mode),
264 fmt_size(l_global_reserve_used, mode)
265 );
266 println!(
267 " Multiple profiles:\t\t{:>10}",
268 if multiple { "yes" } else { "no" }
269 );
270
271 if chunk_allocs.is_none() {
272 eprintln!(
273 "NOTE: per-device usage breakdown unavailable \
274 (chunk tree requires CAP_SYS_ADMIN)"
275 );
276 }
277
278 for s in &spaces {
279 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
280 continue;
281 }
282 #[allow(clippy::cast_precision_loss)]
283 let pct = if s.total_bytes > 0 {
284 100.0 * s.used_bytes as f64 / s.total_bytes as f64
285 } else {
286 0.0
287 };
288 println!(
289 "\n{},{}: Size:{}, Used:{} ({:.2}%)",
290 s.flags.type_name(),
291 s.flags.profile_name(),
292 fmt_size(s.total_bytes, mode),
293 fmt_size(s.used_bytes, mode),
294 pct
295 );
296
297 if let Some(allocs) = &chunk_allocs {
300 let mut profile_allocs: Vec<_> =
301 allocs.iter().filter(|a| a.flags == s.flags).collect();
302 profile_allocs.sort_by_key(|a| a.devid);
303
304 for alloc in profile_allocs {
305 let path = devid_to_path
306 .get(&alloc.devid)
307 .copied()
308 .unwrap_or("<unknown>");
309 println!(" {}\t\t{:>10}", path, fmt_size(alloc.bytes, mode));
310 }
311 }
312 }
313
314 println!("\nUnallocated:");
315 for dev in &devices {
316 let unallocated = dev.total_bytes.saturating_sub(dev.bytes_used);
317 println!(" {}\t{:>10}", dev.path, fmt_size(unallocated, mode));
318 }
319
320 Ok(())
321}