Skip to main content

decdn_common/
disk.rs

1//! Filesystem free-space probing: `statvfs` on Unix, `GetDiskFreeSpaceExW` on
2//! Windows, both through `fs4`.
3//!
4//! Two consumers share this: `decdn node doctor` compares the configured cache
5//! budget against the real volume advisorily, and the node's eviction driver
6//! clamps its effective ceiling to real free disk each tick (#1930). Sharing one
7//! implementation keeps the advisory check and the runtime clamp measuring the
8//! same numbers.
9
10use std::path::Path;
11
12/// Total and unprivileged-available bytes at a path.
13#[derive(Debug, Clone, Copy)]
14pub struct DiskSpace {
15    /// Total filesystem capacity, in bytes.
16    pub total: u64,
17    /// Bytes available to an unprivileged user, in bytes.
18    pub avail: u64,
19}
20
21/// Read filesystem capacity at `path`. On Unix, bytes = fragment size × block
22/// counts (`f_frsize × f_blocks`, `f_frsize × f_bavail`); on Windows, the
23/// caller-visible totals `GetDiskFreeSpaceExW` reports.
24pub fn read_disk_space(path: &Path) -> anyhow::Result<DiskSpace> {
25    let total = fs4::total_space(path)
26        .map_err(|e| anyhow::anyhow!("disk total_space({}) failed: {e}", path.display()))?;
27    let avail = fs4::available_space(path)
28        .map_err(|e| anyhow::anyhow!("disk available_space({}) failed: {e}", path.display()))?;
29    Ok(DiskSpace { total, avail })
30}
31
32/// Probe the path, or the nearest existing ancestor if it does not exist
33/// yet. The cache directory can be probed before it is created (pre-boot doctor)
34/// or after (the running eviction driver); walking to the nearest existing
35/// ancestor gives the right volume in both cases.
36pub fn statvfs_target(path: &Path) -> anyhow::Result<DiskSpace> {
37    let mut cur = path;
38    loop {
39        if cur.exists() {
40            return read_disk_space(cur);
41        }
42        match cur.parent() {
43            Some(p) => cur = p,
44            None => return read_disk_space(path), // let the probe surface the error
45        }
46    }
47}
48
49#[cfg(test)]
50#[allow(clippy::unwrap_used)] // tests
51mod tests;