use std::path::{Path, PathBuf};
const INDENT: &str = " ";
const LEAD: &str = "free space:";
const RUNS_ROOT: &str = "the runs root";
const WORKSPACES: &str = "the lifecycle workspaces under";
const GIB: f64 = 1024.0 * 1024.0 * 1024.0;
pub(crate) fn lines(runs_root: &Path) -> String {
let mut asked: Vec<(&str, PathBuf)> = vec![(RUNS_ROOT, runs_root.to_path_buf())];
match workspaces_root() {
Ok(root) => asked.push((WORKSPACES, root)),
Err(why) => {
return format!(
"{}{INDENT}{LEAD} could not be read for the lifecycle workspaces, because the \
linked onevcs could not resolve its state root: {}, so what is free there is \
unknown\n",
lines_of(&asked),
crate::views::one_line(&why)
)
}
}
lines_of(&asked)
}
fn lines_of(asked: &[(&str, PathBuf)]) -> String {
let mut lines: Vec<String> = Vec::new();
let mut at: Vec<(Identity, usize)> = Vec::new();
for (label, path) in asked {
let named = format!(
"{label} {}",
crate::views::one_line(&path.display().to_string())
);
match Filesystem::holding(path) {
Err(why) => lines.push(format!(
"{INDENT}{LEAD} could not be read for {named}: {}, so what is free there is \
unknown\n",
crate::views::one_line(&why)
)),
Ok(filesystem) => {
let named = match &filesystem.measured_at {
Some(ancestor) => format!(
"{named} (measured at {}, the nearest directory that exists)",
crate::views::one_line(&ancestor.display().to_string())
),
None => named,
};
if let Some((_, index)) = at
.iter()
.find(|(identity, _)| *identity == filesystem.identity)
{
let line = &mut lines[*index];
line.truncate(line.len().saturating_sub(1));
line.push_str(&format!(" and {named}\n"));
continue;
}
at.push((filesystem.identity.clone(), lines.len()));
lines.push(format!(
"{INDENT}{LEAD} {} on the filesystem holding {named}\n",
filesystem.reading()
));
}
}
}
lines.concat()
}
#[derive(Debug)]
struct Filesystem {
identity: Identity,
measured_at: Option<PathBuf>,
free: u64,
total: u64,
}
impl Filesystem {
fn holding(path: &Path) -> Result<Self, String> {
let mut measured = path.to_path_buf();
loop {
match measure(&measured) {
Ok(_) if measured != path && !measured.is_dir() => {
return Err(format!("{}: not a directory", measured.display()))
}
Ok((identity, free, total)) => {
return Ok(Self {
identity,
measured_at: (measured != path).then_some(measured),
free,
total,
})
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
let Some(parent) = measured.parent().map(Path::to_path_buf) else {
return Err(format!("{}: {error}", path.display()));
};
measured = if parent.as_os_str().is_empty() {
PathBuf::from(".")
} else {
parent
};
}
Err(error) => return Err(format!("{}: {error}", measured.display())),
}
}
}
fn reading(&self) -> String {
let share = if self.total == 0 {
0
} else {
u128::from(self.free) * 100 / u128::from(self.total)
};
format!(
"{:.1} GiB of {:.1} GiB ({share}% free)",
self.free as f64 / GIB,
self.total as f64 / GIB
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct Identity(String);
fn workspaces_root() -> Result<PathBuf, String> {
let sizing = onevcs::workspaces::default_path().map_err(|error| error.to_string())?;
let root = sizing
.parent()
.ok_or_else(|| format!("{} has no parent directory", sizing.display()))?;
Ok(root.join("workspaces"))
}
#[cfg(unix)]
fn measure(path: &Path) -> std::io::Result<(Identity, u64, u64)> {
use std::os::unix::ffi::OsStrExt;
use std::os::unix::fs::MetadataExt;
let device = std::fs::metadata(path)?.dev();
let c_path = std::ffi::CString::new(path.as_os_str().as_bytes())
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidInput, error))?;
let mut stat = std::mem::MaybeUninit::<libc::statvfs>::uninit();
let status = unsafe { libc::statvfs(c_path.as_ptr(), stat.as_mut_ptr()) };
if status != 0 {
return Err(std::io::Error::last_os_error());
}
let stat = unsafe { stat.assume_init() };
let block = u128::from(stat.f_frsize);
let free = u64::try_from(u128::from(stat.f_bavail) * block).unwrap_or(u64::MAX);
let total = u64::try_from(u128::from(stat.f_blocks) * block).unwrap_or(u64::MAX);
Ok((Identity(device.to_string()), free, total))
}
#[cfg(windows)]
fn measure(path: &Path) -> std::io::Result<(Identity, u64, u64)> {
use std::os::windows::ffi::OsStrExt;
use windows_sys::Win32::Storage::FileSystem::{GetDiskFreeSpaceExW, GetVolumePathNameW};
std::fs::metadata(path)?;
let wide: Vec<u16> = path
.as_os_str()
.encode_wide()
.chain(std::iter::once(0))
.collect();
let mut volume = vec![0u16; wide.len().max(261)];
let named = unsafe {
GetVolumePathNameW(
wide.as_ptr(),
volume.as_mut_ptr(),
u32::try_from(volume.len()).unwrap_or(u32::MAX),
)
};
if named == 0 {
return Err(std::io::Error::last_os_error());
}
let end = volume.iter().position(|&c| c == 0).unwrap_or(volume.len());
let mount = String::from_utf16_lossy(&volume[..end]);
let mut free = 0u64;
let mut total = 0u64;
let mut total_free = 0u64;
let read = unsafe {
GetDiskFreeSpaceExW(
volume.as_ptr(),
&raw mut free,
&raw mut total,
&raw mut total_free,
)
};
if read == 0 {
return Err(std::io::Error::last_os_error());
}
Ok((Identity(mount), free, total))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_filesystem_is_one_line_and_a_missing_root_is_measured_at_its_ancestor() {
let root =
std::env::temp_dir().join(format!("onepipeline-freespace-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a scratch root");
let runs = root.join("runs");
let workspaces = root.join("not-there-yet").join("workspaces");
let rendered = lines_of(&[(RUNS_ROOT, runs.clone()), (WORKSPACES, workspaces.clone())]);
let _ = std::fs::remove_dir_all(&root);
assert_eq!(rendered.lines().count(), 1, "{rendered}");
assert!(rendered.starts_with(" free space: "), "{rendered}");
assert!(rendered.contains(" GiB of "), "{rendered}");
assert!(
rendered.contains("% free) on the filesystem holding the runs root"),
"{rendered}"
);
assert!(
rendered.contains(&format!(
"{} (measured at {}, the nearest directory that exists)",
runs.display(),
root.display()
)),
"{rendered}"
);
assert!(rendered.contains(&format!("and the lifecycle workspaces under {} (measured at {}, the nearest directory that exists)", workspaces.display(), root.display())), "{rendered}");
assert!(rendered.ends_with('\n'), "{rendered}");
}
#[test]
fn a_root_that_cannot_be_read_says_so_and_the_other_is_still_measured() {
let root =
std::env::temp_dir().join(format!("onepipeline-freespace-file-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a scratch root");
let file = root.join("a-file");
std::fs::write(&file, "not a directory").expect("a file");
let under = file.join("runs");
let rendered = lines_of(&[(RUNS_ROOT, under.clone()), (WORKSPACES, root.clone())]);
let _ = std::fs::remove_dir_all(&root);
let lines: Vec<&str> = rendered.lines().collect();
assert_eq!(lines.len(), 2, "{rendered}");
assert!(
lines[0].starts_with(&format!(
" free space: could not be read for the runs root {}",
under.display()
)),
"{rendered}"
);
assert!(
lines[0].ends_with("so what is free there is unknown"),
"{rendered}"
);
assert!(
lines[1].contains("on the filesystem holding the lifecycle workspaces under"),
"{rendered}"
);
}
#[test]
fn the_share_is_integer_and_a_full_filesystem_reads_zero() {
let full = Filesystem {
identity: Identity("1".into()),
measured_at: None,
free: 1024 * 1024,
total: 197 * 1024 * 1024 * 1024,
};
assert_eq!(full.reading(), "0.0 GiB of 197.0 GiB (0% free)");
let empty = Filesystem {
identity: Identity("1".into()),
measured_at: None,
free: 0,
total: 0,
};
assert_eq!(empty.reading(), "0.0 GiB of 0.0 GiB (0% free)");
}
}