use fs2::FileExt;
use std::fs::File;
use std::io::Error;
use std::path::Path;
use std::sync::Mutex;
static GLOBAL_LOCK: Mutex<()> = Mutex::new(());
pub struct FileLock {
_file: Option<File>,
path: std::path::PathBuf,
}
#[allow(dead_code)]
impl FileLock {
#[allow(dead_code)]
pub fn path(&self) -> &std::path::PathBuf {
&self.path
}
pub fn acquire_exclusive<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
let path_ref = path.as_ref();
let _global_guard = GLOBAL_LOCK.lock().unwrap();
let lock_path = path_ref.with_extension("lock");
if let Some(parent) = lock_path.parent() {
std::fs::create_dir_all(parent)?;
}
let file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&lock_path)?;
file.lock_exclusive()?;
drop(_global_guard);
Ok(FileLock {
_file: Some(file),
path: lock_path,
})
}
}
impl Drop for FileLock {
fn drop(&mut self) {
if let Some(ref file) = self._file {
let _ = file.unlock();
}
let _ = std::fs::remove_file(&self.path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_file_lock() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("test.json");
let lock1 = FileLock::acquire_exclusive(&target_file).expect("Should acquire first lock");
assert!(lock1.path().exists());
drop(lock1);
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_non_existent_directory() {
let temp_dir = tempfile::TempDir::new().unwrap();
let target_file = temp_dir
.path()
.join("/non/existent/relative/path/file.json");
let result = FileLock::acquire_exclusive(&target_file);
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_rapid_relock() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("test.json");
for _ in 0..10 {
let lock = FileLock::acquire_exclusive(&target_file).unwrap();
assert!(lock.path().exists());
drop(lock);
assert!(!target_file.with_extension("lock").exists());
}
}
#[test]
fn test_lock_file_with_special_characters_in_name() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("file-with.special_chars @#$%.json");
let lock = FileLock::acquire_exclusive(&target_file)
.expect("Should acquire lock with special chars in filename");
assert!(lock.path().exists());
drop(lock);
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_file_with_unicode_in_name() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("file_🚀_with_🌍_unicode.json");
let lock = FileLock::acquire_exclusive(&target_file)
.expect("Should acquire lock with unicode in filename");
assert!(lock.path().exists());
drop(lock);
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_file_with_long_name() {
let temp_dir = TempDir::new().unwrap();
let long_name = "a".repeat(200) + ".json"; let target_file = temp_dir.path().join(long_name);
let lock = FileLock::acquire_exclusive(&target_file)
.expect("Should acquire lock with long filename");
assert!(lock.path().exists());
drop(lock);
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_file_in_deeply_nested_directory() {
let temp_dir = TempDir::new().unwrap();
let mut path = temp_dir.path().to_path_buf();
for i in 0..20 {
path.push(format!("level{}", i));
}
std::fs::create_dir_all(&path).unwrap();
let target_file = path.join("deep_file.json");
let lock =
FileLock::acquire_exclusive(&target_file).expect("Should acquire lock in deep nesting");
assert!(lock.path().exists());
drop(lock);
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_file_permissions_edge_case() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("permissions_test.json");
std::fs::write(&target_file, "test").unwrap();
let lock = FileLock::acquire_exclusive(&target_file)
.expect("Should acquire lock regardless of original file permissions");
assert!(lock.path().exists());
drop(lock);
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_file_concurrent_access_simulation() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("concurrent_test.json");
std::fs::write(&target_file, "initial").unwrap();
let lock1 = FileLock::acquire_exclusive(&target_file).unwrap();
assert!(lock1.path.exists());
assert!(lock1.path.exists());
drop(lock1);
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_file_with_existing_lock_file() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("existing_lock_test.json");
let lock_file_path = target_file.with_extension("lock");
std::fs::write(&lock_file_path, "stale lock").unwrap();
assert!(lock_file_path.exists());
let lock = FileLock::acquire_exclusive(&target_file).unwrap();
assert!(lock.path().exists());
drop(lock);
assert!(!lock_file_path.exists());
}
#[test]
fn test_lock_file_drop_behavior() {
let temp_dir = TempDir::new().unwrap();
let target_file = temp_dir.path().join("drop_test.json");
{
let lock = FileLock::acquire_exclusive(&target_file).unwrap();
let lock_path = lock.path().clone();
assert!(lock_path.exists());
}
assert!(!target_file.with_extension("lock").exists());
}
#[test]
fn test_lock_file_with_readonly_parent_directory() {
let temp_dir = TempDir::new().unwrap();
let subdir = temp_dir.path().join("readonly");
std::fs::create_dir(&subdir).unwrap();
let target_file = subdir.join("test.json");
std::fs::write(&target_file, "content").unwrap();
let lock = FileLock::acquire_exclusive(&target_file);
assert!(lock.is_ok());
drop(lock.unwrap());
}
}