use anyhow::Context;
use anyhow::Result;
use std::fs::File;
use std::fs::OpenOptions;
use std::fs::TryLockError;
use std::path::Path;
use std::path::PathBuf;
pub struct FileLock {
_file: File,
pub path: PathBuf,
}
impl FileLock {
pub fn lock_exclusive(path: impl AsRef<Path>) -> Result<Self> {
let path = path.as_ref().to_path_buf();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create lock dir: {}", parent.display()))?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.with_context(|| format!("Failed to open lock file: {}", path.display()))?;
file.lock()
.with_context(|| format!("Failed to acquire exclusive lock: {}", path.display()))?;
Ok(Self { _file: file, path })
}
pub fn try_lock_exclusive(path: impl AsRef<Path>) -> Result<Option<Self>> {
let path = path.as_ref().to_path_buf();
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("Failed to create lock dir: {}", parent.display()))?;
}
let file = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&path)
.with_context(|| format!("Failed to open lock file: {}", path.display()))?;
match file.try_lock() {
Ok(()) => Ok(Some(Self { _file: file, path })),
Err(TryLockError::WouldBlock) => Ok(None), Err(TryLockError::Error(e)) => Err(anyhow::Error::from(e).context(format!(
"Failed to acquire exclusive lock: {}",
path.display()
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_lock_exclusive_basic() {
let dir = TempDir::new().unwrap();
let lock_path = dir.path().join("test.lock");
let lock = FileLock::lock_exclusive(&lock_path).unwrap();
assert!(lock_path.exists());
drop(lock);
}
#[test]
fn test_lock_creates_parent_dirs() {
let dir = TempDir::new().unwrap();
let lock_path = dir.path().join("nested").join("dirs").join("test.lock");
let lock = FileLock::lock_exclusive(&lock_path).unwrap();
assert!(lock_path.exists());
drop(lock);
}
#[test]
fn test_try_lock_exclusive_succeeds_when_available() {
let dir = TempDir::new().unwrap();
let lock_path = dir.path().join("test.lock");
let lock = FileLock::try_lock_exclusive(&lock_path).unwrap();
assert!(lock.is_some());
}
#[test]
fn test_try_lock_exclusive_fails_when_held() {
let dir = TempDir::new().unwrap();
let lock_path = dir.path().join("test.lock");
let _lock1 = FileLock::lock_exclusive(&lock_path).unwrap();
let lock2 = FileLock::try_lock_exclusive(&lock_path).unwrap();
assert!(
lock2.is_none(),
"Second lock should fail when first is held"
);
}
}