use std::path::{Path, PathBuf};
use sysinfo::{DiskRefreshKind, Disks};
pub const MIN_FREE_BYTES: u64 = 2 * 1024 * 1024 * 1024;
fn resolve(path: &Path) -> Option<PathBuf> {
let mut p = path.to_path_buf();
loop {
if let Ok(c) = std::fs::canonicalize(&p) {
let s = c.to_string_lossy();
return Some(PathBuf::from(s.strip_prefix(r"\\?\").unwrap_or(&s)));
}
if !p.pop() {
return None;
}
}
}
fn disk_index(disks: &Disks, path: &Path) -> Option<usize> {
disks
.iter()
.enumerate()
.filter(|(_, d)| path.starts_with(d.mount_point()))
.max_by_key(|(_, d)| d.mount_point().as_os_str().len())
.map(|(i, _)| i)
}
pub fn available_space(path: &Path) -> Option<u64> {
let path = resolve(path)?;
let disks = Disks::new_with_refreshed_list().ok()?;
disk_index(&disks, &path).map(|i| disks[i].available_space())
}
pub fn gb(bytes: u64) -> f64 {
bytes as f64 / 1e9
}
pub struct DiskGuard {
disks: Disks,
index: usize,
path: PathBuf,
}
impl DiskGuard {
pub fn new(output_dir: &Path) -> Option<Self> {
let path = resolve(output_dir)?;
let disks = Disks::new_with_refreshed_list().ok()?;
let index = disk_index(&disks, &path)?;
Some(Self { disks, index, path })
}
pub fn check(&mut self) -> Result<(), String> {
let disk = &mut self.disks[self.index];
disk.refresh_specifics(DiskRefreshKind::nothing().with_storage());
let free = disk.available_space();
if free < MIN_FREE_BYTES {
return Err(format!(
"stopped: only {:.1} GB left on the disk holding {} (minimum {:.1} GB) — \
free some space or choose another --output-dir. Files written so far \
are incomplete.",
gb(free),
self.path.display(),
gb(MIN_FREE_BYTES)
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_available_space_on_temp_dir() {
let free = available_space(&std::env::temp_dir());
assert!(free.is_some_and(|f| f > 0));
}
#[test]
fn test_resolve_falls_back_to_existing_ancestor() {
let missing = std::env::temp_dir().join("dupehell_missing_dir_xyz/sub");
let resolved = resolve(&missing).unwrap();
assert!(!resolved.to_string_lossy().starts_with(r"\\?\"));
assert!(available_space(&missing).is_some());
}
#[test]
fn test_guard_passes_with_space_left() {
let mut guard = DiskGuard::new(&std::env::temp_dir()).unwrap();
assert!(guard.check().is_ok());
}
}