use crate::{
Format, RunContext, Runnable,
util::{SizeFormat, fmt_size},
};
use anyhow::{Context, Result};
use btrfs_uapi::{
chunk::device_chunk_allocations, device::device_info_all,
filesystem::filesystem_info, space::BlockGroupFlags,
};
use clap::Parser;
use cols::Cols;
use std::{fs::File, os::unix::io::AsFd, path::PathBuf};
#[derive(Parser, Debug)]
#[allow(clippy::doc_markdown, clippy::struct_excessive_bools)]
pub struct DeviceUsageCommand {
#[clap(required = true)]
pub paths: Vec<PathBuf>,
#[clap(short = 'b', long, overrides_with_all = ["human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
pub raw: bool,
#[clap(long, overrides_with_all = ["raw", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
pub human_readable: bool,
#[clap(short = 'H', overrides_with_all = ["raw", "human_readable", "iec", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
pub human_base1000: bool,
#[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "si", "kbytes", "mbytes", "gbytes", "tbytes"])]
pub iec: bool,
#[clap(long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "kbytes", "mbytes", "gbytes", "tbytes"])]
pub si: bool,
#[clap(short = 'k', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "mbytes", "gbytes", "tbytes"])]
pub kbytes: bool,
#[clap(short = 'm', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "gbytes", "tbytes"])]
pub mbytes: bool,
#[clap(short = 'g', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
pub gbytes: bool,
#[clap(short = 't', long, overrides_with_all = ["raw", "human_readable", "human_base1000", "iec", "si", "kbytes", "mbytes", "gbytes"])]
pub tbytes: bool,
}
fn physical_device_size(path: &str) -> u64 {
if path.is_empty() {
return 0;
}
let Ok(file) = File::open(path) else {
return 0;
};
btrfs_uapi::blkdev::device_size(file.as_fd()).unwrap_or(0)
}
impl DeviceUsageCommand {
fn size_format(&self) -> SizeFormat {
let si = self.si;
if self.raw {
SizeFormat::Raw
} else if self.kbytes {
SizeFormat::Fixed(if si { 1000 } else { 1024 })
} else if self.mbytes {
SizeFormat::Fixed(if si { 1_000_000 } else { 1024 * 1024 })
} else if self.gbytes {
SizeFormat::Fixed(if si {
1_000_000_000
} else {
1024 * 1024 * 1024
})
} else if self.tbytes {
SizeFormat::Fixed(if si {
1_000_000_000_000
} else {
1024u64.pow(4)
})
} else if si || self.human_base1000 {
SizeFormat::HumanSi
} else {
SizeFormat::HumanIec
}
}
}
impl Runnable for DeviceUsageCommand {
fn run(&self, ctx: &RunContext) -> Result<()> {
let mode = self.size_format();
match ctx.format {
Format::Modern => {
for (i, path) in self.paths.iter().enumerate() {
if i > 0 {
println!();
}
print_device_usage_modern(path, &mode)?;
}
}
Format::Text | Format::Json => {
for (i, path) in self.paths.iter().enumerate() {
if i > 0 {
println!();
}
print_device_usage(path, &mode)?;
}
}
}
Ok(())
}
}
fn print_device_usage(path: &std::path::Path, mode: &SizeFormat) -> Result<()> {
let file = File::open(path)
.with_context(|| format!("failed to open '{}'", path.display()))?;
let fd = file.as_fd();
let fs = filesystem_info(fd).with_context(|| {
format!("failed to get filesystem info for '{}'", path.display())
})?;
let devices = device_info_all(fd, &fs).with_context(|| {
format!("failed to get device info for '{}'", path.display())
})?;
let allocs = device_chunk_allocations(fd).with_context(|| {
format!("failed to get chunk allocations for '{}'", path.display())
})?;
for (di, dev) in devices.iter().enumerate() {
if di > 0 {
println!();
}
let phys_size = physical_device_size(&dev.path);
let slack = if phys_size > 0 {
phys_size.saturating_sub(dev.total_bytes)
} else {
0
};
println!("{}, ID: {}", dev.path, dev.devid);
print_line("Device size", &fmt_size(dev.total_bytes, mode));
print_line("Device slack", &fmt_size(slack, mode));
let mut allocated: u64 = 0;
let mut dev_allocs: Vec<_> =
allocs.iter().filter(|a| a.devid == dev.devid).collect();
dev_allocs.sort_by_key(|a| {
let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
0
} else if a.flags.contains(BlockGroupFlags::METADATA) {
1
} else {
2
};
(type_order, a.flags.bits())
});
for alloc in &dev_allocs {
allocated += alloc.bytes;
let label = format!(
"{},{}",
alloc.flags.type_name(),
alloc.flags.profile_name()
);
print_line(&label, &fmt_size(alloc.bytes, mode));
}
let unallocated = dev.total_bytes.saturating_sub(allocated);
print_line("Unallocated", &fmt_size(unallocated, mode));
}
Ok(())
}
fn print_line(label: &str, value: &str) {
let padding = 20usize.saturating_sub(label.len());
println!(" {label}:{:>pad$}{value:>10}", "", pad = padding);
}
#[derive(Cols)]
struct UsageRow {
#[column(tree)]
name: String,
#[column(header = "SIZE", right)]
size: String,
#[column(children)]
children: Vec<Self>,
}
fn print_device_usage_modern(
path: &std::path::Path,
mode: &SizeFormat,
) -> Result<()> {
let file = File::open(path)
.with_context(|| format!("failed to open '{}'", path.display()))?;
let fd = file.as_fd();
let fs = filesystem_info(fd).with_context(|| {
format!("failed to get filesystem info for '{}'", path.display())
})?;
let devices = device_info_all(fd, &fs).with_context(|| {
format!("failed to get device info for '{}'", path.display())
})?;
let allocs = device_chunk_allocations(fd).with_context(|| {
format!("failed to get chunk allocations for '{}'", path.display())
})?;
let mut roots: Vec<UsageRow> = Vec::new();
for dev in &devices {
let phys_size = physical_device_size(&dev.path);
let slack = if phys_size > 0 {
phys_size.saturating_sub(dev.total_bytes)
} else {
0
};
let mut children: Vec<UsageRow> = Vec::new();
let mut allocated: u64 = 0;
let mut dev_allocs: Vec<_> =
allocs.iter().filter(|a| a.devid == dev.devid).collect();
dev_allocs.sort_by_key(|a| {
let type_order = if a.flags.contains(BlockGroupFlags::DATA) {
0
} else if a.flags.contains(BlockGroupFlags::METADATA) {
1
} else {
2
};
(type_order, a.flags.bits())
});
for alloc in &dev_allocs {
allocated += alloc.bytes;
children.push(UsageRow {
name: format!(
"{},{}",
alloc.flags.type_name(),
alloc.flags.profile_name()
),
size: fmt_size(alloc.bytes, mode),
children: Vec::new(),
});
}
let unallocated = dev.total_bytes.saturating_sub(allocated);
children.push(UsageRow {
name: "Unallocated".to_string(),
size: fmt_size(unallocated, mode),
children: Vec::new(),
});
if slack > 0 {
children.push(UsageRow {
name: "Slack".to_string(),
size: fmt_size(slack, mode),
children: Vec::new(),
});
}
roots.push(UsageRow {
name: format!("{}, ID: {}", dev.path, dev.devid),
size: fmt_size(dev.total_bytes, mode),
children,
});
}
let mut out = std::io::stdout().lock();
let _ = UsageRow::print_table(&roots, &mut out);
Ok(())
}