use std::path::{Path, PathBuf};
pub struct FileLock {
path: PathBuf,
#[cfg(unix)]
file: Option<std::fs::File>,
#[cfg(windows)]
_handle: Option<()>,
#[cfg(not(any(unix, windows)))]
_handle: Option<()>,
}
impl FileLock {
pub fn acquire(path: &Path) -> Result<Self, String> {
#[cfg(unix)]
{
Self::acquire_unix(path)
}
#[cfg(windows)]
{
Self::acquire_windows(path)
}
#[cfg(not(any(unix, windows)))]
{
Self::acquire_fallback(path)
}
}
#[cfg(unix)]
fn acquire_unix(path: &Path) -> Result<Self, String> {
use std::os::unix::io::AsRawFd;
let f = std::fs::File::create(path)
.map_err(|e| format!("Failed to create '{}': {}", path.display(), e))?;
let fd = f.as_raw_fd();
let ret = unsafe { libc::flock(fd, libc::LOCK_EX | libc::LOCK_NB) };
if ret != 0 {
let err = std::io::Error::last_os_error();
return Err(format!(
"Cannot acquire lock on '{}': {} (already in use by another process?)",
path.display(),
err
));
}
tracing::debug!(path = %path.display(), "File lock acquired (Unix flock)");
Ok(Self {
path: path.to_path_buf(),
file: Some(f),
})
}
#[cfg(windows)]
fn acquire_windows(path: &Path) -> Result<Self, String> {
use std::io::Write;
let lock_path = if let Some(parent) = path.parent() {
if let Some(stem) = path.file_stem() {
parent.join(format!("{}.lock", stem.to_string_lossy()))
} else {
path.with_extension(".lock")
}
} else {
path.with_extension(".lock")
};
match std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(&lock_path)
{
Ok(mut f) => {
let pid = std::process::id();
let _ = write!(f, "{}", pid);
drop(f);
Ok(Self {
path: path.to_path_buf(),
_handle: Some(()),
})
}
Err(e) => {
if e.kind() == std::io::ErrorKind::AlreadyExists {
Err(format!(
"Cannot acquire lock on '{}': lock file '{}' already exists",
path.display(),
lock_path.display()
))
} else {
Err(format!(
"Failed to create lock file '{}': {}",
lock_path.display(),
e
))
}
}
}
}
#[cfg(not(any(unix, windows)))]
fn acquire_fallback(path: &Path) -> Result<Self, String> {
tracing::warn!(
path = %path.display(),
"File locking not supported on this platform; proceeding without lock"
);
Ok(Self {
path: path.to_path_buf(),
_handle: None,
})
}
pub fn release(self) {
#[cfg(unix)]
{
use std::os::fd::AsRawFd;
if let Some(f) = &self.file {
let fd = f.as_raw_fd();
let _ = unsafe { libc::flock(fd, libc::LOCK_UN) };
tracing::debug!(
path = %self.path.display(),
"File lock released (Unix flock)"
);
}
}
#[cfg(windows)]
{
let lock_path = self.lock_file_path();
let _ = std::fs::remove_file(&lock_path);
tracing::debug!(
path = %self.path.display(),
lock_file = %lock_path.display(),
"Lock file removed (Windows)"
);
}
#[cfg(not(any(unix, windows)))]
{
let _ = self; }
}
pub fn is_held(&self) -> bool {
#[cfg(unix)]
{
self.file.is_some()
}
#[cfg(windows)]
{
self._handle.is_some()
}
#[cfg(not(any(unix, windows)))]
{
false
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn lock_file_path(&self) -> PathBuf {
#[cfg(unix)]
{
self.path.clone()
}
#[cfg(windows)]
{
if let Some(parent) = self.path.parent() {
if let Some(stem) = self.path.file_stem() {
parent.join(format!("{}.lock", stem.to_string_lossy()))
} else {
self.path.with_extension(".lock")
}
} else {
self.path.with_extension(".lock")
}
}
#[cfg(not(any(unix, windows)))]
{
self.path.clone()
}
}
}
impl Drop for FileLock {
fn drop(&mut self) {
#[cfg(unix)]
{
use std::os::fd::AsRawFd;
if let Some(ref f) = self.file {
let fd = f.as_raw_fd();
let _ = unsafe { libc::flock(fd, libc::LOCK_UN) };
tracing::debug!(
path = %self.path.display(),
"File lock auto-released on drop (Unix)"
);
}
}
#[cfg(windows)]
{
let lock_path = self.lock_file_path();
let _ = std::fs::remove_file(&lock_path);
tracing::debug!(
path = %self.path.display(),
lock_file = %lock_path.display(),
"Lock file auto-removed on drop (Windows)"
);
}
}
}
impl std::fmt::Debug for FileLock {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("FileLock")
.field("path", &self.path)
.field("is_held", &self.is_held())
.finish()
}
}
pub struct DownloadPathLock {
base_lock: Option<FileLock>,
dir: PathBuf,
}
impl DownloadPathLock {
pub fn acquire_for_download(output_dir: &Path) -> Result<Self, String> {
if !output_dir.exists() {
std::fs::create_dir_all(output_dir).map_err(|e| {
format!(
"Failed to create output directory '{}': {}",
output_dir.display(),
e
)
})?;
}
let lock_path = output_dir.join(".aria2.lock");
let base = FileLock::acquire(&lock_path)?;
tracing::info!(
dir = %output_dir.display(),
lock = %lock_path.display(),
"Download path lock acquired"
);
Ok(Self {
base_lock: Some(base),
dir: output_dir.to_path_buf(),
})
}
pub fn release(self) {
if let Some(lock) = self.base_lock {
lock.release();
tracing::info!(dir = %self.dir.display(), "Download path lock released");
}
}
pub fn dir(&self) -> &Path {
&self.dir
}
pub fn file_lock(&self) -> Option<&FileLock> {
self.base_lock.as_ref()
}
pub fn is_held(&self) -> bool {
self.base_lock.as_ref().is_some_and(|l| l.is_held())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn unique_suffix() -> String {
format!(
"_t{}_p{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or_else(|_| 0, |d| d.as_nanos()),
std::process::id()
)
}
#[test]
fn test_file_lock_acquire_release() {
let tmp_dir = std::env::temp_dir();
let lock_path = tmp_dir.join(format!("aria2_test_lock{}.tmp", unique_suffix()));
let lock = FileLock::acquire(&lock_path).expect("First acquire should succeed");
assert!(lock.is_held());
assert_eq!(lock.path(), lock_path);
lock.release();
let lock2 = FileLock::acquire(&lock_path).expect("Re-acquire after release should succeed");
assert!(lock2.is_held());
let _ = std::fs::remove_file(&lock_path);
#[cfg(windows)]
let _ = std::fs::remove_file(lock_path.with_extension(".lock"));
}
#[test]
#[cfg(unix)]
fn test_file_lock_double_acquire_fails() {
let tmp_dir = std::env::temp_dir();
let lock_path = tmp_dir.join(format!("aria2_test_double_lock{}.tmp", unique_suffix()));
let lock1 = FileLock::acquire(&lock_path).expect("First acquire should succeed");
let result = FileLock::acquire(&lock_path);
assert!(
result.is_err(),
"Second acquire on same path should fail, got: {:?}",
result
);
drop(lock1);
let lock3 = FileLock::acquire(&lock_path).expect("Acquire after drop should succeed");
assert!(lock3.is_held());
let _ = std::fs::remove_file(&lock_path);
}
#[test]
fn test_download_path_lock_creates_marker() {
let tmp_dir = std::env::temp_dir();
let test_dir = tmp_dir.join(format!("aria2_test_dl_lock{}", unique_suffix()));
let _ = std::fs::remove_dir_all(&test_dir);
let dl_lock =
DownloadPathLock::acquire_for_download(&test_dir).expect("Acquire should succeed");
let lock_file = dl_lock.base_lock.as_ref().unwrap().lock_file_path();
assert!(
lock_file.exists(),
"lock marker file should exist while lock is held"
);
assert!(dl_lock.is_held());
assert_eq!(dl_lock.dir(), test_dir);
assert!(test_dir.exists());
dl_lock.release();
#[cfg(windows)]
assert!(
!lock_file.exists(),
".lock file should be removed after release on Windows"
);
let _ = std::fs::remove_dir_all(&test_dir);
}
#[test]
fn test_download_path_lock_reacquire_after_drop() {
let tmp_dir = std::env::temp_dir();
let test_dir = tmp_dir.join(format!("aria2_test_reacquire{}", unique_suffix()));
let _ = std::fs::remove_dir_all(&test_dir);
{
let _lock1 =
DownloadPathLock::acquire_for_download(&test_dir).expect("First acquire OK");
let lock_path = _lock1.file_lock().unwrap().lock_file_path();
assert!(lock_path.exists());
}
{
let _lock2 = DownloadPathLock::acquire_for_download(&test_dir)
.expect("Re-acquire after drop OK");
assert!(_lock2.is_held());
}
let _ = std::fs::remove_dir_all(&test_dir);
}
}