Skip to main content

btrfs_cli/filesystem/
usage.rs

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/// Show detailed information about internal filesystem usage
22#[derive(Parser, Debug)]
23pub struct FilesystemUsageCommand {
24    #[clap(flatten)]
25    pub units: UnitMode,
26
27    /// Use base 1000 for human-readable sizes
28    #[clap(short = 'H', long)]
29    pub human_si: bool,
30
31    /// Show data in tabular format
32    #[clap(short = 'T', long)]
33    pub tabular: bool,
34
35    /// One or more mount points to show usage for
36    #[clap(required = true)]
37    pub paths: Vec<PathBuf>,
38}
39
40/// Number of raw-device copies per chunk for a given profile.
41/// Returns 0 for RAID5/6 which require chunk tree data to compute accurately.
42fn 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    // Per-device chunk allocations from the chunk tree.  This requires
131    // CAP_SYS_ADMIN; if it fails we degrade gracefully and note it below.
132    let chunk_allocs = device_chunk_allocations(fd).ok();
133
134    // Map devid -> path for display.
135    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        // Per-device lines: one row per device that holds stripes for this
298        // exact profile.  Sorted by devid for stable output.
299        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}