Skip to main content

btrfs_cli/device/
usage.rs

1use crate::{
2    Format, RunContext, 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 cols::Cols;
12use std::{fs::File, os::unix::io::AsFd, path::PathBuf};
13
14/// Show detailed information about internal allocations in devices
15///
16/// For each device, prints the total device size, the "slack" (difference
17/// between the physical block device size and the size btrfs uses), per-profile
18/// chunk allocations (Data, Metadata, System), and unallocated space. Requires
19/// CAP_SYS_ADMIN for the chunk tree walk.
20#[derive(Parser, Debug)]
21#[allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
22pub struct DeviceUsageCommand {
23    /// Path(s) to a mounted btrfs filesystem
24    #[clap(required = true)]
25    pub paths: Vec<PathBuf>,
26
27    /// Show raw numbers in bytes
28    #[clap(short = 'b', long, overrides_with_all = ["human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
29    pub raw: bool,
30
31    /// Show human-friendly numbers using base 1024 (default)
32    #[clap(long, overrides_with_all = ["raw", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
33    pub human_readable: bool,
34
35    /// Show human-friendly numbers using base 1000
36    #[clap(short = 'H', overrides_with_all = ["raw", "human_readable", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
37    pub human_base1000: bool,
38
39    /// Use 1024 as a base (KiB, MiB, GiB, TiB)
40    #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
41    pub iec: bool,
42
43    /// Use 1000 as a base (kB, MB, GB, TB)
44    #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "kbytes", "mbytes", "gbytes", "tbytes"])]
45    pub si: bool,
46
47    /// Show sizes in KiB, or kB with --si
48    #[clap(short = 'k', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "mbytes", "gbytes", "tbytes"])]
49    pub kbytes: bool,
50
51    /// Show sizes in MiB, or MB with --si
52    #[clap(short = 'm', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "gbytes", "tbytes"])]
53    pub mbytes: bool,
54
55    /// Show sizes in GiB, or GB with --si
56    #[clap(short = 'g', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
57    pub gbytes: bool,
58
59    /// Show sizes in TiB, or TB with --si
60    #[clap(short = 't', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
61    pub tbytes: bool,
62}
63
64/// Try to get the physical block device size.  Returns 0 on failure (e.g.
65/// device path is empty, inaccessible, or not a block device).
66fn physical_device_size(path: &str) -> u64 {
67    if path.is_empty() {
68        return 0;
69    }
70    let Ok(file) = File::open(path) else {
71        return 0;
72    };
73    btrfs_uapi::blkdev::device_size(file.as_fd()).unwrap_or(0)
74}
75
76impl DeviceUsageCommand {
77    fn size_format(&self) -> SizeFormat {
78        let si = self.si;
79        if self.raw {
80            SizeFormat::Raw
81        } else if self.kbytes {
82            SizeFormat::Fixed(if si { 1000 } else { 1024 })
83        } else if self.mbytes {
84            SizeFormat::Fixed(if si { 1_000_000 } else { 1024 * 1024 })
85        } else if self.gbytes {
86            SizeFormat::Fixed(if si {
87                1_000_000_000
88            } else {
89                1024 * 1024 * 1024
90            })
91        } else if self.tbytes {
92            SizeFormat::Fixed(if si {
93                1_000_000_000_000
94            } else {
95                1024u64.pow(4)
96            })
97        } else if si || self.human_base1000 {
98            SizeFormat::HumanSi
99        } else {
100            SizeFormat::HumanIec
101        }
102    }
103}
104
105impl Runnable for DeviceUsageCommand {
106    fn run(&self, ctx: &RunContext) -> Result<()> {
107        let mode = self.size_format();
108        match ctx.format {
109            Format::Modern => {
110                for (i, path) in self.paths.iter().enumerate() {
111                    if i > 0 {
112                        println!();
113                    }
114                    print_device_usage_modern(path, &mode)?;
115                }
116            }
117            Format::Text | Format::Json => {
118                for (i, path) in self.paths.iter().enumerate() {
119                    if i > 0 {
120                        println!();
121                    }
122                    print_device_usage(path, &mode)?;
123                }
124            }
125        }
126        Ok(())
127    }
128}
129
130fn print_device_usage(path: &std::path::Path, mode: &SizeFormat) -> Result<()> {
131    let file = File::open(path)
132        .with_context(|| format!("failed to open '{}'", path.display()))?;
133    let fd = file.as_fd();
134
135    let fs = filesystem_info(fd).with_context(|| {
136        format!("failed to get filesystem info for '{}'", path.display())
137    })?;
138    let devices = device_info_all(fd, &fs).with_context(|| {
139        format!("failed to get device info for '{}'", path.display())
140    })?;
141    let allocs = device_chunk_allocations(fd).with_context(|| {
142        format!("failed to get chunk allocations for '{}'", path.display())
143    })?;
144
145    for (di, dev) in devices.iter().enumerate() {
146        if di > 0 {
147            println!();
148        }
149
150        let phys_size = physical_device_size(&dev.path);
151        let slack = if phys_size > 0 {
152            phys_size.saturating_sub(dev.total_bytes)
153        } else {
154            0
155        };
156
157        println!("{}, ID: {}", dev.path, dev.devid);
158
159        print_line("Device size", &fmt_size(dev.total_bytes, mode));
160        print_line("Device slack", &fmt_size(slack, mode));
161
162        let mut allocated: u64 = 0;
163        let mut dev_allocs: Vec<_> =
164            allocs.iter().filter(|a| a.devid == dev.devid).collect();
165        dev_allocs.sort_by_key(|a| {
166            let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
167                0
168            } else if a.flags.contains(BlockGroupFlags::METADATA) {
169                1
170            } else {
171                2
172            };
173            (type_order, a.flags.bits())
174        });
175
176        for alloc in &dev_allocs {
177            allocated += alloc.bytes;
178            let label = format!(
179                "{},{}",
180                alloc.flags.type_name(),
181                alloc.flags.profile_name()
182            );
183            print_line(&label, &fmt_size(alloc.bytes, mode));
184        }
185
186        let unallocated = dev.total_bytes.saturating_sub(allocated);
187        print_line("Unallocated", &fmt_size(unallocated, mode));
188    }
189
190    Ok(())
191}
192
193fn print_line(label: &str, value: &str) {
194    let padding = 20usize.saturating_sub(label.len());
195    println!("   {label}:{:>pad$}{value:>10}", "", pad = padding);
196}
197
198#[derive(Cols)]
199struct UsageRow {
200    #[column(tree)]
201    name: String,
202    #[column(header = "SIZE", right)]
203    size: String,
204    #[column(children)]
205    children: Vec<Self>,
206}
207
208fn print_device_usage_modern(
209    path: &std::path::Path,
210    mode: &SizeFormat,
211) -> Result<()> {
212    let file = File::open(path)
213        .with_context(|| format!("failed to open '{}'", path.display()))?;
214    let fd = file.as_fd();
215
216    let fs = filesystem_info(fd).with_context(|| {
217        format!("failed to get filesystem info for '{}'", path.display())
218    })?;
219    let devices = device_info_all(fd, &fs).with_context(|| {
220        format!("failed to get device info for '{}'", path.display())
221    })?;
222    let allocs = device_chunk_allocations(fd).with_context(|| {
223        format!("failed to get chunk allocations for '{}'", path.display())
224    })?;
225
226    let mut roots: Vec<UsageRow> = Vec::new();
227
228    for dev in &devices {
229        let phys_size = physical_device_size(&dev.path);
230        let slack = if phys_size > 0 {
231            phys_size.saturating_sub(dev.total_bytes)
232        } else {
233            0
234        };
235
236        let mut children: Vec<UsageRow> = Vec::new();
237        let mut allocated: u64 = 0;
238
239        let mut dev_allocs: Vec<_> =
240            allocs.iter().filter(|a| a.devid == dev.devid).collect();
241        dev_allocs.sort_by_key(|a| {
242            let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
243                0
244            } else if a.flags.contains(BlockGroupFlags::METADATA) {
245                1
246            } else {
247                2
248            };
249            (type_order, a.flags.bits())
250        });
251
252        for alloc in &dev_allocs {
253            allocated += alloc.bytes;
254            children.push(UsageRow {
255                name: format!(
256                    "{},{}",
257                    alloc.flags.type_name(),
258                    alloc.flags.profile_name()
259                ),
260                size: fmt_size(alloc.bytes, mode),
261                children: Vec::new(),
262            });
263        }
264
265        let unallocated = dev.total_bytes.saturating_sub(allocated);
266        children.push(UsageRow {
267            name: "Unallocated".to_string(),
268            size: fmt_size(unallocated, mode),
269            children: Vec::new(),
270        });
271
272        if slack > 0 {
273            children.push(UsageRow {
274                name: "Slack".to_string(),
275                size: fmt_size(slack, mode),
276                children: Vec::new(),
277            });
278        }
279
280        roots.push(UsageRow {
281            name: format!("{}, ID: {}", dev.path, dev.devid),
282            size: fmt_size(dev.total_bytes, mode),
283            children,
284        });
285    }
286
287    let mut out = std::io::stdout().lock();
288    let _ = UsageRow::print_table(&roots, &mut out);
289
290    Ok(())
291}