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#[derive(Parser, Debug)]
20#[allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
21pub struct DeviceUsageCommand {
22 #[clap(required = true)]
24 pub paths: Vec<PathBuf>,
25
26 #[clap(short = 'b', long, overrides_with_all = ["human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
28 pub raw: bool,
29
30 #[clap(long, overrides_with_all = ["raw", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
32 pub human_readable: bool,
33
34 #[clap(short = 'H', overrides_with_all = ["raw", "human_readable", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
36 pub human_base1000: bool,
37
38 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
40 pub iec: bool,
41
42 #[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "kbytes", "mbytes", "gbytes", "tbytes"])]
44 pub si: bool,
45
46 #[clap(short = 'k', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "mbytes", "gbytes", "tbytes"])]
48 pub kbytes: bool,
49
50 #[clap(short = 'm', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "gbytes", "tbytes"])]
52 pub mbytes: bool,
53
54 #[clap(short = 'g', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
56 pub gbytes: bool,
57
58 #[clap(short = 't', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
60 pub tbytes: bool,
61}
62
63fn 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}