use std::fs::{File, OpenOptions, TryLockError};
use std::path::{Path, PathBuf};
use crate::error::LinkError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LockMode {
Shared,
Exclusive,
}
pub fn lock_file_path<P: AsRef<Path>>(database_filename: P) -> PathBuf {
let path = database_filename.as_ref();
let mut name = path
.file_name()
.map(|n| n.to_os_string())
.unwrap_or_default();
name.push(".lock");
match path.parent() {
Some(parent) if !parent.as_os_str().is_empty() => parent.join(name),
_ => PathBuf::from(name),
}
}
#[derive(Debug)]
pub struct FileLock {
file: File,
path: PathBuf,
mode: LockMode,
}
impl FileLock {
pub fn acquire<P: AsRef<Path>>(lock_path: P, mode: LockMode) -> Result<Self, LinkError> {
let (file, path) = Self::open(lock_path)?;
let result = match mode {
LockMode::Shared => file.lock_shared(),
LockMode::Exclusive => file.lock(),
};
result.map_err(|error| {
LinkError::Lock(format!("failed to lock {}: {error}", path.display()))
})?;
Ok(Self { file, path, mode })
}
pub fn try_acquire<P: AsRef<Path>>(
lock_path: P,
mode: LockMode,
) -> Result<Option<Self>, LinkError> {
let (file, path) = Self::open(lock_path)?;
let result = match mode {
LockMode::Shared => file.try_lock_shared(),
LockMode::Exclusive => file.try_lock(),
};
match result {
Ok(()) => Ok(Some(Self { file, path, mode })),
Err(TryLockError::WouldBlock) => Ok(None),
Err(TryLockError::Error(error)) => Err(LinkError::Lock(format!(
"failed to lock {}: {error}",
path.display()
))),
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn mode(&self) -> LockMode {
self.mode
}
fn open<P: AsRef<Path>>(lock_path: P) -> Result<(File, PathBuf), LinkError> {
let path = lock_path.as_ref().to_path_buf();
if let Some(parent) = path.parent() {
if !parent.as_os_str().is_empty() && !parent.exists() {
std::fs::create_dir_all(parent)?;
}
}
let file = OpenOptions::new()
.create(true)
.truncate(false)
.read(true)
.write(true)
.open(&path)?;
Ok((file, path))
}
}
impl Drop for FileLock {
fn drop(&mut self) {
let _ = self.file.unlock();
}
}