use anyhow::{Context, Result};
use std::path::Path;
#[derive(Debug, Clone)]
pub struct DiskSpaceInfo {
pub total_bytes: u64,
pub available_bytes: u64,
#[allow(dead_code)]
pub used_bytes: u64,
#[allow(dead_code)]
pub used_percent: f64,
}
impl DiskSpaceInfo {
pub fn bytes_to_human(bytes: u64) -> String {
const KB: u64 = 1024;
const MB: u64 = 1024 * KB;
const GB: u64 = 1024 * MB;
const TB: u64 = 1024 * GB;
if bytes >= TB {
format!("{:.1} TB", bytes as f64 / TB as f64)
} else if bytes >= GB {
format!("{:.1} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.1} MB", bytes as f64 / MB as f64)
} else if bytes >= KB {
format!("{:.1} KB", bytes as f64 / KB as f64)
} else {
format!("{} B", bytes)
}
}
pub fn available_human(&self) -> String {
Self::bytes_to_human(self.available_bytes)
}
pub fn total_human(&self) -> String {
Self::bytes_to_human(self.total_bytes)
}
}
#[cfg(unix)]
pub fn get_disk_space(path: &Path) -> Result<DiskSpaceInfo> {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let c_path =
CString::new(path.as_os_str().as_bytes()).context("Failed to convert path to CString")?;
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
unsafe {
if libc::statvfs(c_path.as_ptr(), &mut stat) != 0 {
return Err(anyhow::anyhow!(
"Failed to get disk space for '{}': {}",
path.display(),
std::io::Error::last_os_error()
));
}
}
#[allow(clippy::unnecessary_cast)] let frsize = stat.f_frsize as u64;
#[allow(clippy::unnecessary_cast)] let total_bytes = stat.f_blocks as u64 * frsize;
#[allow(clippy::unnecessary_cast)] let available_bytes = stat.f_bavail as u64 * frsize;
let used_bytes = total_bytes - available_bytes;
let used_percent = if total_bytes > 0 {
(used_bytes as f64 / total_bytes as f64) * 100.0
} else {
0.0
};
Ok(DiskSpaceInfo {
total_bytes,
available_bytes,
used_bytes,
used_percent,
})
}
#[cfg(windows)]
pub fn get_disk_space(path: &Path) -> Result<DiskSpaceInfo> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW;
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut free_bytes_available: u64 = 0;
let mut total_bytes: u64 = 0;
let mut total_free_bytes: u64 = 0;
let ok = unsafe {
GetDiskFreeSpaceExW(
wide.as_ptr(),
&mut free_bytes_available,
&mut total_bytes,
&mut total_free_bytes,
)
};
if ok == 0 {
return Err(anyhow::anyhow!(
"Failed to get disk space for '{}': {}",
path.display(),
std::io::Error::last_os_error()
));
}
let available_bytes = free_bytes_available;
let used_bytes = total_bytes.saturating_sub(available_bytes);
let used_percent = if total_bytes > 0 {
(used_bytes as f64 / total_bytes as f64) * 100.0
} else {
0.0
};
Ok(DiskSpaceInfo {
total_bytes,
available_bytes,
used_bytes,
used_percent,
})
}
pub fn check_disk_space_for_backup(
backup_dir: &Path,
file_size: u64,
max_percent: f64,
) -> Result<()> {
let space = get_disk_space(backup_dir).context("Failed to check disk space")?;
let percent_of_free = if space.available_bytes > 0 {
(file_size as f64 / space.available_bytes as f64) * 100.0
} else {
100.0 };
if percent_of_free > max_percent {
return Err(anyhow::anyhow!(
"Insufficient disk space for backup\n\
backup partition: {}\n\
available: {} (total: {})\n\
backup required: {} ({:.1}% of free space)\n\
maximum allowed: {:.1}% of free space\n\
\n\
Options:\n\
1. Remove old backups: sedx backup prune --keep=5\n\
2. Use different location: --backup-dir /mnt/backups\n\
3. Skip backup: --no-backup --force (not recommended)",
backup_dir.display(),
space.available_human(),
space.total_human(),
DiskSpaceInfo::bytes_to_human(file_size),
percent_of_free,
max_percent
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_disk_space() {
let space = get_disk_space(&std::env::temp_dir()).expect("disk space");
assert!(space.total_bytes > 0);
assert!(space.available_bytes > 0);
assert!(space.used_percent >= 0.0 && space.used_percent <= 100.0);
}
#[test]
fn test_bytes_to_human() {
assert_eq!(DiskSpaceInfo::bytes_to_human(500), "500 B");
assert_eq!(DiskSpaceInfo::bytes_to_human(1024), "1.0 KB");
assert_eq!(DiskSpaceInfo::bytes_to_human(1024 * 1024), "1.0 MB");
assert_eq!(DiskSpaceInfo::bytes_to_human(1024 * 1024 * 1024), "1.0 GB");
}
}