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;