btrfs_cli/filesystem/
usage.rs1use super::UnitMode;
2use crate::{Format, Runnable, util::human_bytes};
3use anyhow::{Context, Result};
4use btrfs_uapi::{
5 chunk::device_chunk_allocations,
6 device::device_info_all,
7 filesystem::filesystem_info,
8 space::{BlockGroupFlags, SpaceInfo, space_info},
9};
10use clap::Parser;
11use std::{
12 collections::{HashMap, HashSet},
13 fs::File,
14 os::unix::io::AsFd,
15 path::PathBuf,
16};
17
18#[derive(Parser, Debug)]
20pub struct FilesystemUsageCommand {
21 #[clap(flatten)]
22 pub units: UnitMode,
23
24 #[clap(short = 'H', long)]
26 pub human_si: bool,
27
28 #[clap(short = 'T', long)]
30 pub tabular: bool,
31
32 #[clap(required = true)]
34 pub paths: Vec<PathBuf>,
35}
36
37fn profile_ncopies(flags: BlockGroupFlags) -> u64 {
40 if flags.contains(BlockGroupFlags::RAID1C4) {
41 4
42 } else if flags.contains(BlockGroupFlags::RAID1C3) {
43 3
44 } else if flags.contains(BlockGroupFlags::RAID1)
45 || flags.contains(BlockGroupFlags::DUP)
46 || flags.contains(BlockGroupFlags::RAID10)
47 {
48 2
49 } else if flags.contains(BlockGroupFlags::RAID5)
50 || flags.contains(BlockGroupFlags::RAID6)
51 {
52 0
53 } else {
54 1
55 }
56}
57
58fn has_multiple_profiles(spaces: &[SpaceInfo]) -> bool {
59 let profile_mask = BlockGroupFlags::RAID0
60 | BlockGroupFlags::RAID1
61 | BlockGroupFlags::DUP
62 | BlockGroupFlags::RAID10
63 | BlockGroupFlags::RAID5
64 | BlockGroupFlags::RAID6
65 | BlockGroupFlags::RAID1C3
66 | BlockGroupFlags::RAID1C4
67 | BlockGroupFlags::SINGLE;
68
69 let profiles_for = |type_flag: BlockGroupFlags| {
70 spaces
71 .iter()
72 .filter(|s| {
73 s.flags.contains(type_flag)
74 && !s.flags.contains(BlockGroupFlags::GLOBAL_RSV)
75 })
76 .map(|s| s.flags & profile_mask)
77 .collect::<HashSet<_>>()
78 };
79
80 profiles_for(BlockGroupFlags::DATA).len() > 1
81 || profiles_for(BlockGroupFlags::METADATA).len() > 1
82 || profiles_for(BlockGroupFlags::SYSTEM).len() > 1
83}
84
85impl Runnable for FilesystemUsageCommand {
86 fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
87 for (i, path) in self.paths.iter().enumerate() {
88 if i > 0 {
89 println!();
90 }
91 print_usage(path, self.tabular)?;
92 }
93 Ok(())
94 }
95}
96
97fn print_usage(path: &std::path::Path, _tabular: bool) -> Result<()> {
98 let file = File::open(path)
99 .with_context(|| format!("failed to open '{}'", path.display()))?;
100 let fd = file.as_fd();
101
102 let fs = filesystem_info(fd).with_context(|| {
103 format!("failed to get filesystem info for '{}'", path.display())
104 })?;
105 let devices = device_info_all(fd, &fs).with_context(|| {
106 format!("failed to get device info for '{}'", path.display())
107 })?;
108 let spaces = space_info(fd).with_context(|| {
109 format!("failed to get space info for '{}'", path.display())
110 })?;
111
112 let chunk_allocs = device_chunk_allocations(fd).ok();
115
116 let devid_to_path: HashMap<u64, &str> =
118 devices.iter().map(|d| (d.devid, d.path.as_str())).collect();
119
120 let mut r_data_chunks: u64 = 0;
121 let mut r_data_used: u64 = 0;
122 let mut l_data_chunks: u64 = 0;
123 let mut r_meta_chunks: u64 = 0;
124 let mut r_meta_used: u64 = 0;
125 let mut l_meta_chunks: u64 = 0;
126 let mut r_sys_chunks: u64 = 0;
127 let mut r_sys_used: u64 = 0;
128 let mut l_global_reserve: u64 = 0;
129 let mut l_global_reserve_used: u64 = 0;
130 let mut max_ncopies: u64 = 1;
131
132 for s in &spaces {
133 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
134 l_global_reserve = s.total_bytes;
135 l_global_reserve_used = s.used_bytes;
136 continue;
137 }
138 let ncopies = profile_ncopies(s.flags);
139 if ncopies > max_ncopies {
140 max_ncopies = ncopies;
141 }
142 if s.flags.contains(BlockGroupFlags::DATA) {
143 r_data_chunks += s.total_bytes * ncopies;
144 r_data_used += s.used_bytes * ncopies;
145 l_data_chunks += s.total_bytes;
146 }
147 if s.flags.contains(BlockGroupFlags::METADATA) {
148 r_meta_chunks += s.total_bytes * ncopies;
149 r_meta_used += s.used_bytes * ncopies;
150 l_meta_chunks += s.total_bytes;
151 }
152 if s.flags.contains(BlockGroupFlags::SYSTEM) {
153 r_sys_chunks += s.total_bytes * ncopies;
154 r_sys_used += s.used_bytes * ncopies;
155 }
156 }
157
158 let r_total_size: u64 = devices.iter().map(|d| d.total_bytes).sum();
159 let r_total_chunks = r_data_chunks + r_meta_chunks + r_sys_chunks;
160 let r_total_used = r_data_used + r_meta_used + r_sys_used;
161 let r_total_unused = r_total_size.saturating_sub(r_total_chunks);
162
163 let r_total_missing: u64 = devices
164 .iter()
165 .filter(|d| std::fs::metadata(&d.path).is_err())
166 .map(|d| d.total_bytes)
167 .sum();
168
169 let data_ratio = if l_data_chunks > 0 {
170 r_data_chunks as f64 / l_data_chunks as f64
171 } else {
172 1.0
173 };
174 let meta_ratio = if l_meta_chunks > 0 {
175 r_meta_chunks as f64 / l_meta_chunks as f64
176 } else {
177 1.0
178 };
179 let max_data_ratio = max_ncopies as f64;
180
181 const MIN_UNALLOCATED_THRESH: u64 = 16 * 1024 * 1024;
182 let free_base = if data_ratio > 0.0 {
183 ((r_data_chunks.saturating_sub(r_data_used)) as f64 / data_ratio) as u64
184 } else {
185 0
186 };
187 let (free_estimated, free_min) = if r_total_unused >= MIN_UNALLOCATED_THRESH
188 {
189 (
190 free_base + (r_total_unused as f64 / data_ratio) as u64,
191 free_base + (r_total_unused as f64 / max_data_ratio) as u64,
192 )
193 } else {
194 (free_base, free_base)
195 };
196
197 let free_statfs = nix::sys::statfs::statfs(path)
198 .map(|st| st.blocks_available() as u64 * st.block_size() as u64)
199 .unwrap_or(0);
200
201 let multiple = has_multiple_profiles(&spaces);
202
203 println!("Overall:");
204 println!(" Device size:\t\t{:>10}", human_bytes(r_total_size));
205 println!(
206 " Device allocated:\t\t{:>10}",
207 human_bytes(r_total_chunks)
208 );
209 println!(
210 " Device unallocated:\t\t{:>10}",
211 human_bytes(r_total_unused)
212 );
213 println!(
214 " Device missing:\t\t{:>10}",
215 human_bytes(r_total_missing)
216 );
217 println!(" Device slack:\t\t{:>10}", human_bytes(0));
218 println!(" Used:\t\t\t{:>10}", human_bytes(r_total_used));
219 println!(
220 " Free (estimated):\t\t{:>10}\t(min: {})",
221 human_bytes(free_estimated),
222 human_bytes(free_min)
223 );
224 println!(" Free (statfs, df):\t\t{:>10}", human_bytes(free_statfs));
225 println!(" Data ratio:\t\t\t{:>10.2}", data_ratio);
226 println!(" Metadata ratio:\t\t{:>10.2}", meta_ratio);
227 println!(
228 " Global reserve:\t\t{:>10}\t(used: {})",
229 human_bytes(l_global_reserve),
230 human_bytes(l_global_reserve_used)
231 );
232 println!(
233 " Multiple profiles:\t\t{:>10}",
234 if multiple { "yes" } else { "no" }
235 );
236
237 if chunk_allocs.is_none() {
238 eprintln!(
239 "NOTE: per-device usage breakdown unavailable \
240 (chunk tree requires CAP_SYS_ADMIN)"
241 );
242 }
243
244 for s in &spaces {
245 if s.flags.contains(BlockGroupFlags::GLOBAL_RSV) {
246 continue;
247 }
248 let pct = if s.total_bytes > 0 {
249 100.0 * s.used_bytes as f64 / s.total_bytes as f64
250 } else {
251 0.0
252 };
253 println!(
254 "\n{},{}: Size:{}, Used:{} ({:.2}%)",
255 s.flags.type_name(),
256 s.flags.profile_name(),
257 human_bytes(s.total_bytes),
258 human_bytes(s.used_bytes),
259 pct
260 );
261
262 if let Some(allocs) = &chunk_allocs {
265 let mut profile_allocs: Vec<_> =
266 allocs.iter().filter(|a| a.flags == s.flags).collect();
267 profile_allocs.sort_by_key(|a| a.devid);
268
269 for alloc in profile_allocs {
270 let path = devid_to_path
271 .get(&alloc.devid)
272 .copied()
273 .unwrap_or("<unknown>");
274 println!(" {}\t\t{:>10}", path, human_bytes(alloc.bytes));
275 }
276 }
277 }
278
279 println!("\nUnallocated:");
280 for dev in &devices {
281 let unallocated = dev.total_bytes.saturating_sub(dev.bytes_used);
282 println!(" {}\t{:>10}", dev.path, human_bytes(unallocated));
283 }
284
285 Ok(())
286}