use std::fs::{self, OpenOptions};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct FileLock {
path: PathBuf,
held: bool,
}
impl FileLock {
pub fn acquire(path: impl Into<PathBuf>) -> io::Result<Self> {
let path = path.into();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let mut lock_file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)?;
write!(lock_file, "{}", std::process::id())?;
Ok(Self { path, held: true })
}
#[must_use]
pub fn path(&self) -> &Path {
&self.path
}
#[must_use]
pub fn is_held(&self) -> bool {
self.held
}
pub fn release(&mut self) -> io::Result<()> {
if self.held {
fs::remove_file(&self.path)?;
self.held = false;
}
Ok(())
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = self.release();
}
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::{Path, PathBuf};
use std::process;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
use super::FileLock;
static UNIQUE_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TestDir {
path: PathBuf,
}
impl TestDir {
fn new() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("clock should be after epoch")
.as_nanos();
let counter = UNIQUE_COUNTER.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"rjango-file-lock-test-{}-{nanos}-{counter}",
process::id()
));
fs::create_dir_all(&path).expect("create test directory");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
#[test]
fn acquire_creates_lock_file() {
let dir = TestDir::new();
let lock_path = dir.path().join("upload.lock");
let lock = FileLock::acquire(lock_path.clone()).expect("acquire lock");
assert!(lock.is_held());
assert_eq!(lock.path(), lock_path.as_path());
assert!(lock_path.exists());
}
#[test]
fn second_acquire_fails_while_lock_is_held() {
let dir = TestDir::new();
let lock_path = dir.path().join("upload.lock");
let first_lock = FileLock::acquire(lock_path.clone()).expect("acquire first lock");
let error = FileLock::acquire(lock_path.clone()).expect_err("expected duplicate lock");
assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
drop(first_lock);
}
#[test]
fn release_removes_lock_file() {
let dir = TestDir::new();
let lock_path = dir.path().join("upload.lock");
let mut lock = FileLock::acquire(lock_path.clone()).expect("acquire lock");
lock.release().expect("release lock");
assert!(!lock.is_held());
assert!(!lock_path.exists());
}
}