1use std::path::Path;
4
5use crate::FsError;
6
7#[derive(Debug)]
13pub struct ExclusiveLock {
14 _file: std::fs::File,
15}
16
17pub async fn acquire_exclusive_lock(path: impl AsRef<Path>) -> Result<ExclusiveLock, FsError> {
23 let path = path.as_ref().to_owned();
24 let task_path = path.clone();
25 tokio::task::spawn_blocking(move || acquire_blocking(&task_path))
26 .await
27 .map_err(|error| FsError::Lock {
28 path: path.display().to_string(),
29 detail: format!("lock task failed: {error}"),
30 })?
31}
32
33fn acquire_blocking(path: &Path) -> Result<ExclusiveLock, FsError> {
34 let file = open_lock_file(path).map_err(|error| FsError::Lock {
35 path: path.display().to_string(),
36 detail: format!("failed to open lock file: {error}"),
37 })?;
38
39 ensure_lock_file_permissions(path);
40 fs2::FileExt::try_lock_exclusive(&file).map_err(|error| FsError::Lock {
41 path: path.display().to_string(),
42 detail: format!("already held by another process: {error}"),
43 })?;
44 Ok(ExclusiveLock { _file: file })
45}
46
47fn open_lock_file(path: &Path) -> Result<std::fs::File, std::io::Error> {
48 let mut write_options = std::fs::OpenOptions::new();
49 write_options
50 .create(true)
51 .truncate(false)
52 .read(true)
53 .write(true);
54 #[cfg(unix)]
55 std::os::unix::fs::OpenOptionsExt::mode(&mut write_options, 0o664);
56
57 match write_options.open(path) {
58 Ok(file) => Ok(file),
59 Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => {
60 let mut read_options = std::fs::OpenOptions::new();
61 read_options.read(true).open(path)
62 }
63 Err(error) => Err(error),
64 }
65}
66
67#[cfg(unix)]
68fn ensure_lock_file_permissions(path: &Path) {
69 use std::os::unix::fs::PermissionsExt;
70
71 let permissions = std::fs::Permissions::from_mode(0o664);
72 let _ = std::fs::set_permissions(path, permissions);
73}
74
75#[cfg(not(unix))]
76fn ensure_lock_file_permissions(_path: &Path) {}