use std::{fs::File, path::Path};
use fs4::FileExt;
use miette::{IntoDiagnostic as _, Result, WrapErr as _};
use tracing::trace;
#[derive(Debug)]
pub struct Lock {
file: File,
}
impl Lock {
pub fn try_hold(file: File) -> Result<Option<Self>> {
match FileExt::try_lock(&file) {
Ok(()) => Ok(Some(Self { file })),
Err(fs4::TryLockError::WouldBlock) => Ok(None),
Err(fs4::TryLockError::Error(err)) => Err(err).into_diagnostic(),
}
}
pub fn try_segment(segment: &Path) -> Result<Option<Self>> {
Self::at(&super::paths::lock_of(segment))
}
fn at(path: &Path) -> Result<Option<Self>> {
let file = super::paths::private()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(path)
.into_diagnostic()
.wrap_err_with(|| format!("opening audit lock file {}", path.display()))?;
Self::try_hold(file)
}
pub fn try_directory(dir: &Path) -> Result<Option<Self>> {
Self::at(&dir.join(super::paths::DIRECTORY_LOCK))
}
}
impl Drop for Lock {
fn drop(&mut self) {
if let Err(err) = FileExt::unlock(&self.file) {
trace!(?err, "releasing audit lock");
}
}
}
#[cfg(test)]
pub fn is_free(path: &Path) -> bool {
matches!(Lock::try_segment(path), Ok(Some(_)))
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::OpenOptions;
#[test]
fn a_held_lock_excludes_a_second_holder() {
let dir = tempfile::tempdir().unwrap();
let held = Lock::try_directory(dir.path()).unwrap();
assert!(held.is_some());
let path = dir.path().join(super::super::paths::DIRECTORY_LOCK);
let second = OpenOptions::new()
.read(true)
.write(true)
.open(&path)
.unwrap();
assert!(Lock::try_hold(second).unwrap().is_none());
drop(held);
assert!(Lock::try_directory(dir.path()).unwrap().is_some());
}
#[test]
fn a_held_segment_reads_as_taken_but_stays_usable() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("segment");
std::fs::write(&path, b"records").unwrap();
let held = Lock::try_segment(&path).unwrap().unwrap();
assert!(!is_free(&path));
assert_eq!(std::fs::read(&path).unwrap(), b"records");
let mut appending = OpenOptions::new().append(true).open(&path).unwrap();
std::io::Write::write_all(&mut appending, b" more").unwrap();
drop(appending);
assert_eq!(std::fs::read(&path).unwrap(), b"records more");
drop(held);
assert!(is_free(&path));
}
#[test]
fn a_segment_nothing_holds_reads_as_free() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("segment");
std::fs::write(&path, b"").unwrap();
assert!(is_free(&path));
}
}