use std::path::Path;
#[cfg(unix)]
fn path_to_cstring(path: &Path) -> Option<std::ffi::CString> {
use std::os::unix::ffi::OsStrExt;
std::ffi::CString::new(path.as_os_str().as_bytes()).ok()
}
pub fn check_disk_space(path: &Path, required_bytes: u64) -> std::result::Result<(), String> {
#[cfg(unix)]
{
let check_path = if path.as_os_str().is_empty() {
Path::new(".")
} else {
path
};
let Some(c_path) = path_to_cstring(check_path) else {
tracing::warn!(
path = %check_path.display(),
required = required_bytes,
"path contains embedded null byte, skipping disk space check"
);
return Ok(());
};
let mut stat: libc::statvfs = unsafe { std::mem::zeroed() };
let ret = unsafe { libc::statvfs(c_path.as_ptr(), &mut stat) };
if ret != 0 {
tracing::warn!(
path = %check_path.display(),
required = required_bytes,
"statvfs failed, skipping disk space check: {}",
std::io::Error::last_os_error()
);
return Ok(());
}
let available = stat.f_bavail as u64 * stat.f_frsize as u64;
let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
if available < needed_with_headroom {
let available_gb = available as f64 / (1024.0 * 1024.0 * 1024.0);
let required_gb = required_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
return Err(format!(
"Insufficient disk space: need {:.2} GiB but only {:.2} GiB available on '{}'",
required_gb,
available_gb,
check_path.display()
));
}
Ok(())
}
#[cfg(windows)]
{
use std::os::windows::ffi::OsStrExt;
let check_path = if path.as_os_str().is_empty() {
Path::new(".")
} else {
path
};
let abs_path = match std::fs::canonicalize(check_path) {
Ok(p) => p,
Err(_) => {
std::env::current_dir().unwrap_or_else(|_| Path::new(".").to_path_buf())
}
};
let mut wide_path: Vec<u16> = abs_path.as_os_str().encode_wide().collect();
wide_path.push(0);
let mut free_bytes_available: u64 = 0;
let mut total_number_of_bytes: u64 = 0;
let mut total_number_of_free_bytes: u64 = 0;
let result = unsafe {
windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(
wide_path.as_ptr(),
&mut free_bytes_available as *mut u64 as *mut _,
&mut total_number_of_bytes as *mut u64 as *mut _,
&mut total_number_of_free_bytes as *mut u64 as *mut _,
)
};
if result == 0 {
tracing::warn!(
path = %check_path.display(),
required = required_bytes,
"GetDiskFreeSpaceEx failed, skipping disk space check"
);
return Ok(());
}
let needed_with_headroom = required_bytes.saturating_add(required_bytes / 10);
if free_bytes_available < needed_with_headroom {
let available_gb = free_bytes_available as f64 / (1024.0 * 1024.0 * 1024.0);
let required_gb = required_bytes as f64 / (1024.0 * 1024.0 * 1024.0);
return Err(format!(
"Insufficient disk space: need {:.2} GiB but only {:.2} GiB available on '{}'",
required_gb,
available_gb,
check_path.display()
));
}
Ok(())
}
#[cfg(not(any(unix, windows)))]
{
tracing::warn!(
path = %path.display(),
required = required_bytes,
"Disk space check skipped on unsupported platform"
);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sufficient_space_ok() {
let required = 100 * 1024 * 1024; let result = check_disk_space(Path::new("."), required);
assert!(
result.is_ok() || result.is_err(),
"Should return Ok or Err without panicking"
);
}
#[test]
fn test_insufficient_space_err() {
let required = u64::MAX / 2; let result = check_disk_space(Path::new("."), required);
if let Err(error_msg) = result {
assert!(
error_msg.to_lowercase().contains("insufficient")
|| error_msg.to_lowercase().contains("space"),
"Error message should mention space: {}",
error_msg
);
}
}
#[test]
fn test_check_disk_space_empty_path() {
let result = check_disk_space(Path::new(""), 1024);
assert!(
result.is_ok() || result.is_err(),
"Empty path should be handled gracefully"
);
}
#[test]
fn test_check_disk_space_zero_bytes() {
let result = check_disk_space(Path::new("."), 0);
assert!(
result.is_ok(),
"Requesting zero bytes should always succeed"
);
}
}