#![deny(missing_docs)]
use std::fs::{File, OpenOptions, TryLockError};
use std::path::{Path, PathBuf};
use miette::{Context as _, IntoDiagnostic as _};
use crate::Result;
pub struct FsLock {
file: Option<File>,
path: PathBuf,
}
impl FsLock {
pub async fn acquire_exclusive(path: PathBuf) -> Result<Self> {
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 path_for_blocking = path.clone();
let file = tokio::task::spawn_blocking(move || -> Result<File> {
match file.try_lock() {
Ok(()) => Ok(file),
Err(TryLockError::WouldBlock) => {
log::info!("waiting for lock on {:?}", path_for_blocking);
file.lock().into_diagnostic().wrap_err_with(|| {
format!("Failed to acquire lock on {:?}.", path_for_blocking)
})?;
Ok(file)
}
Err(TryLockError::Error(e)) => Err(e)
.into_diagnostic()
.wrap_err_with(|| format!("Failed to try-lock {:?}.", path_for_blocking)),
}
})
.await
.into_diagnostic()
.wrap_err("Lock acquisition task panicked.")??;
Ok(FsLock {
file: Some(file),
path,
})
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for FsLock {
fn drop(&mut self) {
if let Some(file) = self.file.take() {
let _ = file.unlock();
}
}
}