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