Skip to main content

btrfs_cli/device/
usage.rs

1use crate::{
2    Format, Runnable,
3    util::{SizeFormat, fmt_size},
4};
5use anyhow::{Context, Result};
6use btrfs_uapi::{
7    chunk::device_chunk_allocations, device::device_info_all,
8    filesystem::filesystem_info, space::BlockGroupFlags,
9};
10use clap::Parser;
11use std::{fs::File, os::unix::io::AsFd, path::PathBuf};
12
13/// Show detailed information about internal allocations in devices
14///
15/// For each device, prints the total device size, the "slack" (difference
16/// between the physical block device size and the size btrfs uses), per-profile
17/// chunk allocations (Data, Metadata, System), and unallocated space. Requires
18/// CAP_SYS_ADMIN for the chunk tree walk.
19#[derive(Parser, Debug)]
20pub struct DeviceUsageCommand {
21    /// Path(s) to a mounted btrfs filesystem
22    #[clap(required = true)]
23    pub paths: Vec<PathBuf>,
24
25    /// Show raw numbers in bytes
26    #[clap(short = 'b', long, overrides_with_all = ["human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
27    pub raw: bool,
28
29    /// Show human-friendly numbers using base 1024 (default)
30    #[clap(long, overrides_with_all = ["raw", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
31    pub human_readable: bool,
32
33    /// Show human-friendly numbers using base 1000
34    #[clap(short = 'H', overrides_with_all = ["raw", "human_readable", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
35    pub human_base1000: bool,
36
37    /// Use 1024 as a base (KiB, MiB, GiB, TiB)
38    #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
39    pub iec: bool,
40
41    /// Use 1000 as a base (kB, MB, GB, TB)
42    #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "kbytes", "mbytes", "gbytes", "tbytes"])]
43    pub si: bool,
44
45    /// Show sizes in KiB, or kB with --si
46    #[clap(short = 'k', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "mbytes", "gbytes", "tbytes"])]
47    pub kbytes: bool,
48
49    /// Show sizes in MiB, or MB with --si
50    #[clap(short = 'm', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "gbytes", "tbytes"])]
51    pub mbytes: bool,
52
53    /// Show sizes in GiB, or GB with --si
54    #[clap(short = 'g', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
55    pub gbytes: bool,
56
57    /// Show sizes in TiB, or TB with --si
58    #[clap(short = 't', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
59    pub tbytes: bool,
60}
61
62/// Try to get the physical block device size.  Returns 0 on failure (e.g.
63/// device path is empty, inaccessible, or not a block device).
64fn physical_device_size(path: &str) -> u64 {
65    if path.is_empty() {
66        return 0;
67    }
68    let Ok(file) = File::open(path) else {
69        return 0;
70    };
71    btrfs_uapi::blkdev::device_size(file.as_fd()).unwrap_or(0)
72}
73
74impl DeviceUsageCommand {
75    fn size_format(&self) -> SizeFormat {
76        let si = self.si;
77        if self.raw {
78            SizeFormat::Raw
79        } else if self.kbytes {
80            SizeFormat::Fixed(if si { 1000 } else { 1024 })
81        } else if self.mbytes {
82            SizeFormat::Fixed(if si { 1_000_000 } else { 1024 * 1024 })
83        } else if self.gbytes {
84            SizeFormat::Fixed(if si {
85                1_000_000_000
86            } else {
87                1024 * 1024 * 1024
88            })
89        } else if self.tbytes {
90            SizeFormat::Fixed(if si {
91                1_000_000_000_000
92            } else {
93                1024u64.pow(4)
94            })
95        } else if si || self.human_base1000 {
96            SizeFormat::HumanSi
97        } else {
98            SizeFormat::HumanIec
99        }
100    }
101}
102
103impl Runnable for DeviceUsageCommand {
104    fn run(&self, _format: Format, _dry_run: bool) -> Result<()> {
105        let mode = self.size_format();
106        for (i, path) in self.paths.iter().enumerate() {
107            if i > 0 {
108                println!();
109            }
110            print_device_usage(path, &mode)?;
111        }
112        Ok(())
113    }
114}
115
116fn print_device_usage(path: &std::path::Path, mode: &SizeFormat) -> Result<()> {
117    let file = File::open(path)
118        .with_context(|| format!("failed to open '{}'", path.display()))?;
119    let fd = file.as_fd();
120
121    let fs = filesystem_info(fd).with_context(|| {
122        format!("failed to get filesystem info for '{}'", path.display())
123    })?;
124    let devices = device_info_all(fd, &fs).with_context(|| {
125        format!("failed to get device info for '{}'", path.display())
126    })?;
127    let allocs = device_chunk_allocations(fd).with_context(|| {
128        format!("failed to get chunk allocations for '{}'", path.display())
129    })?;
130
131    for (di, dev) in devices.iter().enumerate() {
132        if di > 0 {
133            println!();
134        }
135
136        let phys_size = physical_device_size(&dev.path);
137        let slack = if phys_size > 0 {
138            phys_size.saturating_sub(dev.total_bytes)
139        } else {
140            0
141        };
142
143        println!("{}, ID: {}", dev.path, dev.devid);
144
145        print_line("Device size", &fmt_size(dev.total_bytes, mode));
146        print_line("Device slack", &fmt_size(slack, mode));
147
148        let mut allocated: u64 = 0;
149        let mut dev_allocs: Vec<_> =
150            allocs.iter().filter(|a| a.devid == dev.devid).collect();
151        dev_allocs.sort_by_key(|a| {
152            let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
153                0
154            } else if a.flags.contains(BlockGroupFlags::METADATA) {
155                1
156            } else {
157                2
158            };
159            (type_order, a.flags.bits())
160        });
161
162        for alloc in &dev_allocs {
163            allocated += alloc.bytes;
164            let label = format!(
165                "{},{}",
166                alloc.flags.type_name(),
167                alloc.flags.profile_name()
168            );
169            print_line(&label, &fmt_size(alloc.bytes, mode));
170        }
171
172        let unallocated = dev.total_bytes.saturating_sub(allocated);
173        print_line("Unallocated", &fmt_size(unallocated, mode));
174    }
175
176    Ok(())
177}
178
179fn print_line(label: &str, value: &str) {
180    let padding = 20usize.saturating_sub(label.len());
181    println!("   {label}:{:>pad$}{value:>10}", "", pad = padding);
182}