btrfs_cli/filesystem/
usage.rs1use 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 if flags.contains(BlockGroupFlags::RAID5)
53 || flags.contains(BlockGroupFlags::RAID6)
54 {
55 0
56 } else {
57 1
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, _format: Format, _dry_run: bool) -> 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 print_usage(path, self.tabular, &mode)?;
99 }
100 Ok(())
101 }
102}
103
104fn print_usage(
105 path: &std::path::Path,
106 _tabular: bool,
107 mode: &SizeFormat,
108) -> Result<()> {
109 let file = File::open(path)
110 .with_context(|| format!("failed to open '{}'", path.display()))?;
111 let fd = file.as_fd();
112
113 let fs = filesystem_info(fd).with_context(|| {
114 format!("failed to get filesystem info for '{}'", path.display())
115 })?;
116 let devices = device_info_all(fd, &fs).with_context(|| {
117 format!("failed to get device info for '{}'", path.display())
118 })?;
119 let spaces = space_info(fd).with_context(|| {
120 format!("failed to get space info for '{}'", path.display())
121 })?;
122
123 let chunk_allocs = device_chunk_allocations(fd).ok();
126
127 let devid_to_path: HashMap<u64, &str> =
129 devices.iter().map(|d| (d.devid, d.path.as_str())).collect();
130
131 let mut r_data_chunks: u64 = 0;
132 let mut r_data_used: u64 = 0;
133 let mut l_data_chunks: u64 = 0;
134 let mut r_meta_chunks: u64 = 0;
135 let mut r_meta_used: u64 = 0;
136 let mut l_meta_chunks: u64 = 0;
137 let mut r_sys_chunks: u64 = 0;
138 let mut r_sys_used: u64 = 0;
139 let mut l_global_reserve: u64 = 0;
140 let mut l_global_reserve_used: u64 = 0;
141 let mut max_ncopies: u64 = 1;
142
143 for s in &spaces {
144 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
145 l_global_reserve = s.total_bytes;
146 l_global_reserve_used = s.used_bytes;
147 continue;
148 }
149 let ncopies = profile_ncopies(s.flags);
150 if ncopies > max_ncopies {
151 max_ncopies = ncopies;
152 }
153 if s.flags.contains(BlockGroupFlags::DATA) {
154 r_data_chunks += s.total_bytes * ncopies;
155 r_data_used += s.used_bytes * ncopies;
156 l_data_chunks += s.total_bytes;
157 }
158 if s.flags.contains(BlockGroupFlags::METADATA) {
159 r_meta_chunks += s.total_bytes * ncopies;
160 r_meta_used += s.used_bytes * ncopies;
161 l_meta_chunks += s.total_bytes;
162 }
163 if s.flags.contains(BlockGroupFlags::SYSTEM) {
164 r_sys_chunks += s.total_bytes * ncopies;
165 r_sys_used += s.used_bytes * ncopies;
166 }
167 }
168
169 let r_total_size: u64 = devices.iter().map(|d| d.total_bytes).sum();
170 let r_total_chunks = r_data_chunks + r_meta_chunks + r_sys_chunks;
171 let r_total_used = r_data_used + r_meta_used + r_sys_used;
172 let r_total_unused = r_total_size.saturating_sub(r_total_chunks);
173
174 let r_total_missing: u64 = devices
175 .iter()
176 .filter(|d| std::fs::metadata(&d.path).is_err())
177 .map(|d| d.total_bytes)
178 .sum();
179
180 let data_ratio = if l_data_chunks > 0 {
181 r_data_chunks as f64 / l_data_chunks as f64
182 } else {
183 1.0
184 };
185 let meta_ratio = if l_meta_chunks > 0 {
186 r_meta_chunks as f64 / l_meta_chunks as f64
187 } else {
188 1.0
189 };
190 let max_data_ratio = max_ncopies as f64;
191
192 const MIN_UNALLOCATED_THRESH: u64 = 16 * 1024 * 1024;
193 let free_base = if data_ratio > 0.0 {
194 ((r_data_chunks.saturating_sub(r_data_used)) as f64 / data_ratio) as u64
195 } else {
196 0
197 };
198 let (free_estimated, free_min) = if r_total_unused >= MIN_UNALLOCATED_THRESH
199 {
200 (
201 free_base + (r_total_unused as f64 / data_ratio) as u64,
202 free_base + (r_total_unused as f64 / max_data_ratio) as u64,
203 )
204 } else {
205 (free_base, free_base)
206 };
207
208 let free_statfs = nix::sys::statfs::statfs(path)
209 .map(|st| st.blocks_available() * st.block_size() as u64)
210 .unwrap_or(0);
211
212 let multiple = has_multiple_profiles(&spaces);
213
214 println!("Overall:");
215 println!(" Device size:\t\t{:>10}", fmt_size(r_total_size, mode));
216 println!(
217 " Device allocated:\t\t{:>10}",
218 fmt_size(r_total_chunks, mode)
219 );
220 println!(
221 " Device unallocated:\t\t{:>10}",
222 fmt_size(r_total_unused, mode)
223 );
224 println!(
225 " Device missing:\t\t{:>10}",
226 fmt_size(r_total_missing, mode)
227 );
228 println!(" Device slack:\t\t{:>10}", fmt_size(0, mode));
229 println!(" Used:\t\t\t{:>10}", fmt_size(r_total_used, mode));
230 println!(
231 " Free (estimated):\t\t{:>10}\t(min: {})",
232 fmt_size(free_estimated, mode),
233 fmt_size(free_min, mode)
234 );
235 println!(
236 " Free (statfs, df):\t\t{:>10}",
237 fmt_size(free_statfs, mode)
238 );
239 println!(" Data ratio:\t\t\t{:>10.2}", data_ratio);
240 println!(" Metadata ratio:\t\t{:>10.2}", meta_ratio);
241 println!(
242 " Global reserve:\t\t{:>10}\t(used: {})",
243 fmt_size(l_global_reserve, mode),
244 fmt_size(l_global_reserve_used, mode)
245 );
246 println!(
247 " Multiple profiles:\t\t{:>10}",
248 if multiple { "yes" } else { "no" }
249 );
250
251 if chunk_allocs.is_none() {
252 eprintln!(
253 "NOTE: per-device usage breakdown unavailable \
254 (chunk tree requires CAP_SYS_ADMIN)"
255 );
256 }
257
258 for s in &spaces {
259 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
260 continue;
261 }
262 let pct = if s.total_bytes > 0 {
263 100.0 * s.used_bytes as f64 / s.total_bytes as f64
264 } else {
265 0.0
266 };
267 println!(
268 "\n{},{}: Size:{}, Used:{} ({:.2}%)",
269 s.flags.type_name(),
270 s.flags.profile_name(),
271 fmt_size(s.total_bytes, mode),
272 fmt_size(s.used_bytes, mode),
273 pct
274 );
275
276 if let Some(allocs) = &chunk_allocs {
279 let mut profile_allocs: Vec<_> =
280 allocs.iter().filter(|a| a.flags == s.flags).collect();
281 profile_allocs.sort_by_key(|a| a.devid);
282
283 for alloc in profile_allocs {
284 let path = devid_to_path
285 .get(&alloc.devid)
286 .copied()
287 .unwrap_or("<unknown>");
288 println!(" {}\t\t{:>10}", path, fmt_size(alloc.bytes, mode));
289 }
290 }
291 }
292
293 println!("\nUnallocated:");
294 for dev in &devices {
295 let unallocated = dev.total_bytes.saturating_sub(dev.bytes_used);
296 println!(" {}\t{:>10}", dev.path, fmt_size(unallocated, mode));
297 }
298
299 Ok(())
300}