#![allow(missing_docs)]
#[allow(unused_imports)]
use firkin_types::Size;
#[allow(unused_imports)]
use std::path::Path;
#[allow(unused_imports)]
use std::path::PathBuf;
#[allow(unused_imports)]
use std::process::Command;
#[allow(unused_imports)]
use thiserror::Error as ThisError;
pub trait DiskPressureProbe {
type Error;
fn available_disk(&mut self, path: &Path) -> Result<Size, Self::Error>;
}
#[derive(Debug, ThisError)]
pub enum HostDiskPressureProbeError {
#[error("failed to execute df for host disk probe: {0}")]
Command(#[source] std::io::Error),
#[error("df host disk probe failed: {status}")]
CommandStatus {
status: std::process::ExitStatus,
},
#[error("df host disk probe output was not UTF-8: {0}")]
Utf8(#[source] std::string::FromUtf8Error),
#[error("df host disk probe output missing available-kilobytes field")]
MissingAvailable,
#[error("df host disk probe available-kilobytes field was invalid: {0}")]
InvalidAvailable(#[source] std::num::ParseIntError),
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HostDiskPressureProbe;
impl HostDiskPressureProbe {
#[must_use]
pub const fn new() -> Self {
Self
}
pub fn parse_df_available(output: &str) -> Result<Size, HostDiskPressureProbeError> {
let available_kib = output
.lines()
.skip(1)
.find_map(|line| line.split_whitespace().nth(3))
.ok_or(HostDiskPressureProbeError::MissingAvailable)?
.parse::<u64>()
.map_err(HostDiskPressureProbeError::InvalidAvailable)?;
Ok(Size::kib(available_kib))
}
}
impl DiskPressureProbe for HostDiskPressureProbe {
type Error = HostDiskPressureProbeError;
fn available_disk(&mut self, path: &Path) -> Result<Size, Self::Error> {
let output = Command::new("df")
.arg("-Pk")
.arg(path)
.output()
.map_err(HostDiskPressureProbeError::Command)?;
if !output.status.success() {
return Err(HostDiskPressureProbeError::CommandStatus {
status: output.status,
});
}
let stdout = String::from_utf8(output.stdout).map_err(HostDiskPressureProbeError::Utf8)?;
Self::parse_df_available(&stdout)
}
}
#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
pub enum DiskPressureError<E> {
#[error("disk pressure probe failed: {source}")]
Probe {
source: E,
},
#[error("available disk space {available} is below required minimum {minimum}")]
BelowMinimum {
minimum: Size,
available: Size,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiskPressureReport {
pub(crate) root: PathBuf,
minimum_free: Size,
available_free: Size,
}
impl DiskPressureReport {
#[must_use]
pub fn new(root: PathBuf, minimum_free: Size, available_free: Size) -> Self {
Self {
root,
minimum_free,
available_free,
}
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub const fn minimum_free(&self) -> Size {
self.minimum_free
}
#[must_use]
pub const fn available_free(&self) -> Size {
self.available_free
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RuntimeDiskPressureGuard {
pub(crate) root: PathBuf,
minimum_free: Size,
}
impl RuntimeDiskPressureGuard {
#[must_use]
pub fn new(root: impl Into<PathBuf>, minimum_free: Size) -> Self {
Self {
root: root.into(),
minimum_free,
}
}
#[must_use]
pub fn root(&self) -> &Path {
&self.root
}
#[must_use]
pub const fn minimum_free(&self) -> Size {
self.minimum_free
}
pub fn check<P>(&self, probe: &mut P) -> Result<DiskPressureReport, DiskPressureError<P::Error>>
where
P: DiskPressureProbe,
{
let available_free = probe
.available_disk(&self.root)
.map_err(|source| DiskPressureError::Probe { source })?;
if available_free < self.minimum_free {
return Err(DiskPressureError::BelowMinimum {
minimum: self.minimum_free,
available: available_free,
});
}
Ok(DiskPressureReport::new(
self.root.clone(),
self.minimum_free,
available_free,
))
}
}