use std::{
ffi::CString,
fs::{read_dir, Metadata},
mem,
ops::{Add, AddAssign},
os::unix::prelude::OsStrExt,
path::Path,
};
use eyre::{eyre, Result};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct DiskSize {
pub bytes: u64,
pub inodes: u64,
}
impl DiskSize {
pub fn new_capacity(bytes: u64) -> Self {
Self {
bytes,
inodes: u64::MAX,
}
}
pub const ZERO: Self = Self {
bytes: 0,
inodes: 0,
};
pub fn min(a: Self, b: Self) -> Self {
Self {
bytes: a.bytes.min(b.bytes),
inodes: a.inodes.min(b.inodes),
}
}
pub fn max(a: Self, b: Self) -> Self {
Self {
bytes: a.bytes.max(b.bytes),
inodes: a.inodes.max(b.inodes),
}
}
pub fn exceeds(&self, other: &Self) -> bool {
(self.bytes != other.bytes || self.inodes != other.inodes)
&& self.bytes >= other.bytes
&& self.inodes >= other.inodes
}
pub fn saturating_sub(self, other: Self) -> Self {
Self {
bytes: self.bytes.saturating_sub(other.bytes),
inodes: self.inodes.saturating_sub(other.inodes),
}
}
}
impl Add for DiskSize {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
bytes: self.bytes + other.bytes,
inodes: self.inodes + other.inodes,
}
}
}
impl AddAssign for DiskSize {
fn add_assign(&mut self, rhs: Self) {
self.bytes += rhs.bytes;
self.inodes += rhs.inodes;
}
}
impl From<Metadata> for DiskSize {
fn from(metadata: Metadata) -> Self {
Self {
bytes: metadata.len(),
inodes: 1,
}
}
}
pub fn get_disk_space(path: &Path) -> Result<DiskSize> {
let mut stat: libc::statvfs = unsafe { mem::zeroed() };
let cpath = CString::new(path.as_os_str().as_bytes()).map_err(|_| eyre!("Invalid path"))?;
if unsafe { libc::statvfs(cpath.as_ptr() as *const _, &mut stat) } != 0 {
Err(eyre!("Unable to call statvfs"))
} else {
let f_frsize: u64 = stat.f_frsize as _;
let f_blocks: u64 = stat.f_blocks as _;
let bytes = f_frsize * f_blocks;
Ok(DiskSize {
bytes,
inodes: stat.f_favail as _,
})
}
}
pub fn get_size<P>(path: P) -> Result<DiskSize>
where
P: AsRef<Path>,
{
let path_metadata = path.as_ref().symlink_metadata()?;
let mut size = DiskSize::ZERO;
if path_metadata.is_dir() {
for entry in read_dir(&path)? {
let entry = entry?;
let entry_metadata = entry.metadata()?;
if entry_metadata.is_dir() {
size += get_size(entry.path())?;
} else {
size += entry_metadata.into();
}
}
} else {
size = path_metadata.into();
}
Ok(size)
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case(0, 0, 0, 0, false)]
#[case(1024, 1, 0, 0, true)]
#[case(1024, 10, 2048, 20, false)]
#[case(1024, 100, 2048, 20, false)]
#[case(4096, 10, 2048, 20, false)]
#[case(1024, 100, 1024, 10, true)]
fn test_size_cmp(
#[case] bytes: u64,
#[case] inodes: u64,
#[case] free_bytes: u64,
#[case] free_inodes: u64,
#[case] exceeds: bool,
) {
let size1 = DiskSize { bytes, inodes };
let size2 = DiskSize {
bytes: free_bytes,
inodes: free_inodes,
};
assert_eq!(size1.exceeds(&size2), exceeds);
}
}