#![deny(missing_docs)]
use std::fs::{File, OpenOptions, TryLockError};
use std::path::PathBuf;
use miette::{Context as _, IntoDiagnostic as _};
use crate::Result;
pub struct FsLock {
path: PathBuf,
}
impl FsLock {
pub fn new(path: PathBuf) -> Self {
FsLock { path }
}
pub async fn acquire_exclusive(&self) -> Result<FsLockGuard> {
self.acquire(true).await
}
pub async fn acquire_shared(&self) -> Result<FsLockGuard> {
self.acquire(false).await
}
async fn acquire(&self, exclusive: bool) -> Result<FsLockGuard> {
let path = self.path.clone();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to create lock directory {:?}.", parent))?;
}
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&path)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to open lock file {:?}.", path))?;
let file = tokio::task::spawn_blocking(move || -> Result<File> {
let try_lock = if exclusive {
file.try_lock()
} else {
file.try_lock_shared()
};
match try_lock {
Ok(()) => Ok(file),
Err(TryLockError::WouldBlock) => {
log::info!("waiting for lock on {:?}", path);
let blocking_lock = if exclusive {
file.lock()
} else {
file.lock_shared()
};
blocking_lock
.into_diagnostic()
.wrap_err_with(|| format!("Failed to acquire lock on {:?}.", path))?;
Ok(file)
}
Err(TryLockError::Error(e)) => Err(e)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to try-lock {:?}.", path)),
}
})
.await
.into_diagnostic()
.wrap_err("Lock acquisition task panicked.")??;
Ok(FsLockGuard { file })
}
}
pub struct FsLockGuard {
file: File,
}
impl Drop for FsLockGuard {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}