Skip to main content

btrfs_cli/device/
usage.rs

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