1use std::fs::{File, OpenOptions};
4use std::io;
5use std::path::Path;
6
7use fs4::FileExt;
8
9#[derive(Debug)]
11pub struct StoreLock {
12 file: File,
13}
14
15fn open_lockfile(root: &Path) -> io::Result<File> {
16 std::fs::create_dir_all(root)?;
17 OpenOptions::new()
18 .create(true)
19 .truncate(false)
20 .read(true)
21 .write(true)
22 .open(root.join(".lock"))
23}
24
25impl StoreLock {
26 pub fn exclusive(root: &Path) -> io::Result<Self> {
28 let file = open_lockfile(root)?;
29 <File as FileExt>::lock(&file)?;
30 Ok(Self { file })
31 }
32
33 pub fn shared(root: &Path) -> io::Result<Self> {
35 let file = open_lockfile(root)?;
36 <File as FileExt>::lock_shared(&file)?;
37 Ok(Self { file })
38 }
39
40 pub fn try_exclusive(root: &Path) -> io::Result<Self> {
42 let file = open_lockfile(root)?;
43 <File as FileExt>::try_lock(&file)
44 .map_err(|e| io::Error::other(format!("lock not acquired: {e}")))?;
45 Ok(Self { file })
46 }
47}
48
49impl Drop for StoreLock {
50 fn drop(&mut self) {
51 let _ = <File as FileExt>::unlock(&self.file);
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use tempfile::TempDir;
59
60 #[test]
61 fn exclusive_then_shared_release_roundtrip() {
62 let dir = TempDir::new().unwrap();
63 {
64 let _g = StoreLock::exclusive(dir.path()).unwrap();
65 assert!(dir.path().join(".lock").is_file());
66 } let _g = StoreLock::shared(dir.path()).unwrap();
68 }
69
70 #[test]
71 fn try_exclusive_fails_while_held() {
72 let dir = TempDir::new().unwrap();
73 let _held = StoreLock::exclusive(dir.path()).unwrap();
74 assert!(StoreLock::try_exclusive(dir.path()).is_err());
75 }
76}