use crate::error::Result;
use fs2::FileExt;
use std::fs::{self, File};
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct SyncLock {
_lock_file: File,
lock_path: PathBuf,
}
impl SyncLock {
pub fn acquire(source: &Path, dest: &Path) -> Result<Self> {
let lock_path = Self::get_lock_path(source, dest)?;
let lock_file = fs::OpenOptions::new().create(true).write(true).truncate(true).open(&lock_path)?;
match lock_file.try_lock_exclusive() {
Ok(()) => {
use std::io::Write;
let mut file_mut = &lock_file;
let pid = std::process::id();
writeln!(file_mut, "{}", pid)?;
Ok(Self { _lock_file: lock_file, lock_path })
}
Err(_) => {
Err(crate::error::SyncError::SyncLocked {
source_path: source.display().to_string(),
dest_path: dest.display().to_string(),
lock_file: lock_path.display().to_string(),
})
}
}
}
fn get_lock_path(source: &Path, dest: &Path) -> Result<PathBuf> {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
source.to_string_lossy().hash(&mut hasher);
dest.to_string_lossy().hash(&mut hasher);
let hash = format!("{:x}", hasher.finish());
let cache_dir = if let Ok(xdg_cache) = std::env::var("XDG_CACHE_HOME") {
PathBuf::from(xdg_cache)
} else if let Ok(home) = std::env::var("HOME") {
PathBuf::from(home).join(".cache")
} else {
return Err(crate::error::SyncError::Config("Cannot determine cache directory (HOME not set)".to_string()));
};
let lock_dir = cache_dir.join("sy").join("locks");
fs::create_dir_all(&lock_dir)?;
Ok(lock_dir.join(format!("{}.lock", hash)))
}
}
impl Drop for SyncLock {
fn drop(&mut self) {
let _ = fs::remove_file(&self.lock_path);
}
}
#[cfg(test)]
mod tests {
use super::*;
use serial_test::serial;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
#[test]
#[serial]
fn test_acquire_lock() {
let temp_dir = TempDir::new().unwrap();
let source = temp_dir.path().join("source");
let dest = temp_dir.path().join("dest");
let original = std::env::var("XDG_CACHE_HOME").ok();
let cache_dir = temp_dir.path().join("cache");
unsafe {
std::env::set_var("XDG_CACHE_HOME", &cache_dir);
}
let lock = SyncLock::acquire(&source, &dest).unwrap();
let lock_path = SyncLock::get_lock_path(&source, &dest).unwrap();
assert!(lock_path.exists());
drop(lock);
assert!(!lock_path.exists());
match original {
Some(val) => unsafe { std::env::set_var("XDG_CACHE_HOME", val) },
None => unsafe { std::env::remove_var("XDG_CACHE_HOME") },
}
}
#[test]
#[serial]
fn test_concurrent_lock_fails() {
let temp_dir = TempDir::new().unwrap();
let source = temp_dir.path().join("source");
let dest = temp_dir.path().join("dest");
let original = std::env::var("XDG_CACHE_HOME").ok();
let cache_dir = temp_dir.path().join("cache");
unsafe {
std::env::set_var("XDG_CACHE_HOME", &cache_dir);
};
let _lock1 = SyncLock::acquire(&source, &dest).unwrap();
let result = SyncLock::acquire(&source, &dest);
assert!(result.is_err());
let err = result.unwrap_err();
let err_str = format!("{}", err);
assert!(err_str.contains("already in progress"));
match original {
Some(val) => unsafe { std::env::set_var("XDG_CACHE_HOME", val) },
None => unsafe { std::env::remove_var("XDG_CACHE_HOME") },
}
}
#[test]
#[serial]
fn test_lock_released_on_drop() {
let temp_dir = TempDir::new().unwrap();
let source = temp_dir.path().join("source");
let dest = temp_dir.path().join("dest");
let original = std::env::var("XDG_CACHE_HOME").ok();
let cache_dir = temp_dir.path().join("cache");
unsafe {
std::env::set_var("XDG_CACHE_HOME", &cache_dir);
};
{
let _lock1 = SyncLock::acquire(&source, &dest).unwrap();
}
let _lock2 = SyncLock::acquire(&source, &dest).unwrap();
match original {
Some(val) => unsafe { std::env::set_var("XDG_CACHE_HOME", val) },
None => unsafe { std::env::remove_var("XDG_CACHE_HOME") },
}
}
#[test]
#[serial]
fn test_different_pairs_independent() {
let temp_dir = TempDir::new().unwrap();
let source1 = temp_dir.path().join("source1");
let dest1 = temp_dir.path().join("dest1");
let source2 = temp_dir.path().join("source2");
let dest2 = temp_dir.path().join("dest2");
let original = std::env::var("XDG_CACHE_HOME").ok();
let cache_dir = temp_dir.path().join("cache");
unsafe {
std::env::set_var("XDG_CACHE_HOME", &cache_dir);
};
let _lock1 = SyncLock::acquire(&source1, &dest1).unwrap();
let _lock2 = SyncLock::acquire(&source2, &dest2).unwrap();
match original {
Some(val) => unsafe { std::env::set_var("XDG_CACHE_HOME", val) },
None => unsafe { std::env::remove_var("XDG_CACHE_HOME") },
}
}
#[test]
#[serial]
fn test_lock_across_threads() {
let temp_dir = TempDir::new().unwrap();
let source = temp_dir.path().join("source");
let dest = temp_dir.path().join("dest");
let original = std::env::var("XDG_CACHE_HOME").ok();
let cache_dir = temp_dir.path().join("cache");
unsafe {
std::env::set_var("XDG_CACHE_HOME", &cache_dir);
};
let lock = SyncLock::acquire(&source, &dest).unwrap();
let source_clone = source.clone();
let dest_clone = dest.clone();
let handle = thread::spawn(move || SyncLock::acquire(&source_clone, &dest_clone));
thread::sleep(Duration::from_millis(100));
let result = handle.join().unwrap();
assert!(result.is_err(), "Expected lock acquisition to fail while lock is held");
drop(lock);
let _lock2 = SyncLock::acquire(&source, &dest).unwrap();
match original {
Some(val) => unsafe { std::env::set_var("XDG_CACHE_HOME", val) },
None => unsafe { std::env::remove_var("XDG_CACHE_HOME") },
}
}
}