use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use fs2::FileExt;
use crate::error::{Error, Result};
pub struct FileLock {
file: File,
path: PathBuf,
}
impl FileLock {
pub fn acquire(path: &Path) -> Result<Self> {
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path)
.map_err(|e| Error::LockFailed {
path: path.to_path_buf(),
reason: e.to_string(),
})?;
file.lock_exclusive().map_err(|e| Error::LockFailed {
path: path.to_path_buf(),
reason: e.to_string(),
})?;
Ok(Self {
file,
path: path.to_path_buf(),
})
}
pub fn try_acquire(path: &Path) -> Result<Option<Self>> {
let file = OpenOptions::new()
.create(true)
.truncate(false)
.write(true)
.open(path)
.map_err(|e| Error::LockFailed {
path: path.to_path_buf(),
reason: e.to_string(),
})?;
match file.try_lock_exclusive() {
Ok(()) => Ok(Some(Self {
file,
path: path.to_path_buf(),
})),
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => Ok(None),
Err(ref e)
if e.raw_os_error()
.is_some_and(|code| code == 11 || code == 35) =>
{
Ok(None)
}
Err(e) => Err(Error::LockFailed {
path: path.to_path_buf(),
reason: e.to_string(),
}),
}
}
pub fn path(&self) -> &Path {
&self.path
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn acquire_and_release() {
let dir = tempfile::tempdir().unwrap();
let lock_path = dir.path().join("test.lock");
let lock = FileLock::acquire(&lock_path).unwrap();
assert!(lock_path.exists());
drop(lock);
}
#[test]
fn try_acquire_non_blocking() {
let dir = tempfile::tempdir().unwrap();
let lock_path = dir.path().join("test.lock");
let lock1 = FileLock::try_acquire(&lock_path).unwrap();
assert!(lock1.is_some());
drop(lock1);
let lock2 = FileLock::try_acquire(&lock_path).unwrap();
assert!(lock2.is_some());
}
#[test]
fn lock_path_accessor() {
let dir = tempfile::tempdir().unwrap();
let lock_path = dir.path().join("test.lock");
let lock = FileLock::acquire(&lock_path).unwrap();
assert_eq!(lock.path(), lock_path);
}
}