use fs2::FileExt;
use std::fs::{File, OpenOptions};
use std::io;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
pub const DATA_DIR_ALREADY_OWNED: &str =
"DATA_DIR_ALREADY_OWNED: another process owns this data directory";
#[derive(Debug)]
pub struct OwnerLock {
_file: File,
pub path: PathBuf,
}
impl OwnerLock {
pub fn acquire(data_dir: &Path) -> io::Result<Self> {
std::fs::create_dir_all(data_dir)?;
let path = data_dir.join(".owner.lock");
let file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.mode(0o600)
.open(&path)?;
match file.try_lock_exclusive() {
Ok(()) => Ok(Self { _file: file, path }),
Err(_) => Err(io::Error::new(
io::ErrorKind::AlreadyExists,
DATA_DIR_ALREADY_OWNED,
)),
}
}
}
impl Drop for OwnerLock {
fn drop(&mut self) {
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn acquire_and_reacquire_after_drop() {
let dir = tempdir().unwrap();
let lock1 = OwnerLock::acquire(dir.path());
assert!(lock1.is_ok(), "first acquire should succeed");
drop(lock1);
let lock2 = OwnerLock::acquire(dir.path());
assert!(lock2.is_ok(), "reacquire after drop should succeed");
}
#[test]
fn second_acquire_fails() {
let dir = tempdir().unwrap();
let _lock1 = OwnerLock::acquire(dir.path()).expect("first acquire");
let lock2 = OwnerLock::acquire(dir.path());
assert!(lock2.is_err(), "second concurrent acquire should fail");
let err = lock2.unwrap_err();
assert!(
err.to_string().contains("ALREADY_OWNED"),
"error should mention ALREADY_OWNED"
);
}
#[test]
fn lock_file_has_private_permissions() {
let dir = tempdir().unwrap();
let _lock = OwnerLock::acquire(dir.path()).expect("acquire");
let meta = std::fs::metadata(dir.path().join(".owner.lock")).unwrap();
use std::os::unix::fs::PermissionsExt;
let mode = meta.permissions().mode() & 0o777;
assert_eq!(
mode, 0o600,
"lock file should have 0600 permissions, got 0o{:o}",
mode
);
}
}