1#![allow(deprecated)]
27
28use std::fs::File;
29use std::os::unix::io::AsRawFd;
30
31use either::{Either, Left, Right};
32use nix::errno::Errno;
33use nix::fcntl::{flock, FlockArg};
34use nix::Result;
35use uuid::Uuid;
36
37#[derive(Debug)]
38pub struct UnlockedFile(File);
39#[derive(Debug)]
40pub struct LockedFileShared(File);
41#[derive(Debug)]
42pub struct LockedFileExclusive(File);
43
44impl From<File> for UnlockedFile {
45 fn from(file: File) -> Self {
46 Self(file)
47 }
48}
49
50impl TryFrom<&std::path::Path> for UnlockedFile {
51 type Error = std::io::Error;
52
53 fn try_from(path: &std::path::Path) -> std::io::Result<Self> {
54 std::fs::OpenOptions::new()
55 .append(true)
56 .create(true)
57 .open(path)
58 .map(UnlockedFile)
59 }
60}
61
62impl TryFrom<&std::path::PathBuf> for UnlockedFile {
63 type Error = std::io::Error;
64
65 fn try_from(path: &std::path::PathBuf) -> std::io::Result<Self> {
66 Self::try_from(path.as_path())
67 }
68}
69
70impl TryFrom<&Uuid> for UnlockedFile {
71 type Error = std::io::Error;
72
73 fn try_from(uuid: &Uuid) -> std::io::Result<Self> {
74 let mut buffer = Uuid::encode_buffer();
75 let uuid = uuid.simple().encode_lower(&mut buffer);
76 let filename = ".pgdo.".to_owned() + uuid;
77 let path = std::env::temp_dir().join(filename);
78 UnlockedFile::try_from(&*path)
79 }
80}
81
82#[allow(unused)]
83impl UnlockedFile {
84 pub fn try_lock_shared(self) -> Result<Either<Self, LockedFileShared>> {
85 match flock(self.0.as_raw_fd(), FlockArg::LockSharedNonblock) {
86 Ok(()) => Ok(Right(LockedFileShared(self.0))),
87 Err(Errno::EAGAIN) => Ok(Left(self)),
88 Err(err) => Err(err),
89 }
90 }
91
92 pub fn lock_shared(self) -> Result<LockedFileShared> {
93 flock(self.0.as_raw_fd(), FlockArg::LockShared)?;
94 Ok(LockedFileShared(self.0))
95 }
96
97 pub fn try_lock_exclusive(self) -> Result<Either<Self, LockedFileExclusive>> {
98 match flock(self.0.as_raw_fd(), FlockArg::LockExclusiveNonblock) {
99 Ok(()) => Ok(Right(LockedFileExclusive(self.0))),
100 Err(Errno::EAGAIN) => Ok(Left(self)),
101 Err(err) => Err(err),
102 }
103 }
104
105 pub fn lock_exclusive(self) -> Result<LockedFileExclusive> {
106 flock(self.0.as_raw_fd(), FlockArg::LockExclusive)?;
107 Ok(LockedFileExclusive(self.0))
108 }
109}
110
111#[allow(unused)]
112impl LockedFileShared {
113 pub fn try_lock_exclusive(self) -> Result<Either<Self, LockedFileExclusive>> {
114 match flock(self.0.as_raw_fd(), FlockArg::LockExclusiveNonblock) {
115 Ok(()) => Ok(Right(LockedFileExclusive(self.0))),
116 Err(Errno::EAGAIN) => Ok(Left(self)),
117 Err(err) => Err(err),
118 }
119 }
120
121 pub fn lock_exclusive(self) -> Result<LockedFileExclusive> {
122 flock(self.0.as_raw_fd(), FlockArg::LockExclusive)?;
123 Ok(LockedFileExclusive(self.0))
124 }
125
126 pub fn try_unlock(self) -> Result<Either<Self, UnlockedFile>> {
127 match flock(self.0.as_raw_fd(), FlockArg::UnlockNonblock) {
128 Ok(()) => Ok(Right(UnlockedFile(self.0))),
129 Err(Errno::EAGAIN) => Ok(Left(self)),
130 Err(err) => Err(err),
131 }
132 }
133
134 pub fn unlock(self) -> Result<UnlockedFile> {
135 flock(self.0.as_raw_fd(), FlockArg::Unlock)?;
136 Ok(UnlockedFile(self.0))
137 }
138}
139
140#[allow(unused)]
141impl LockedFileExclusive {
142 pub fn try_lock_shared(self) -> Result<Either<Self, LockedFileShared>> {
143 match flock(self.0.as_raw_fd(), FlockArg::LockSharedNonblock) {
144 Ok(()) => Ok(Right(LockedFileShared(self.0))),
145 Err(Errno::EAGAIN) => Ok(Left(self)),
146 Err(err) => Err(err),
147 }
148 }
149
150 pub fn lock_shared(self) -> Result<LockedFileShared> {
151 flock(self.0.as_raw_fd(), FlockArg::LockShared)?;
152 Ok(LockedFileShared(self.0))
153 }
154
155 pub fn try_unlock(self) -> Result<Either<Self, UnlockedFile>> {
156 match flock(self.0.as_raw_fd(), FlockArg::UnlockNonblock) {
157 Ok(()) => Ok(Right(UnlockedFile(self.0))),
158 Err(Errno::EAGAIN) => Ok(Left(self)),
159 Err(err) => Err(err),
160 }
161 }
162
163 pub fn unlock(self) -> Result<UnlockedFile> {
164 flock(self.0.as_raw_fd(), FlockArg::Unlock)?;
165 Ok(UnlockedFile(self.0))
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::UnlockedFile;
172
173 use std::fs::OpenOptions;
174 use std::io;
175 use std::os::unix::io::AsRawFd;
176 use std::path::Path;
177
178 use either::Left;
179 use nix::fcntl::{flock, FlockArg};
180
181 fn can_lock<P: AsRef<Path>>(filename: P, exclusive: bool) -> nix::Result<()> {
182 let file = OpenOptions::new()
183 .append(true)
184 .create(true)
185 .open(filename)
186 .unwrap();
187 let mode = if exclusive {
188 FlockArg::LockExclusiveNonblock
189 } else {
190 FlockArg::LockSharedNonblock
191 };
192 flock(file.as_raw_fd(), mode)
193 }
194
195 fn can_lock_exclusive<P: AsRef<Path>>(filename: P) -> nix::Result<()> {
196 can_lock(filename, true)
197 }
198
199 fn can_lock_shared<P: AsRef<Path>>(filename: P) -> nix::Result<()> {
200 can_lock(filename, false)
201 }
202
203 #[test]
204 fn file_lock_exclusive_takes_exclusive_flock() -> io::Result<()> {
205 let lock_dir = tempfile::tempdir()?;
206 let lock_filename = lock_dir.path().join("lock");
207 let lock = OpenOptions::new()
208 .append(true)
209 .create(true)
210 .open(&lock_filename)
211 .map(UnlockedFile::from)?;
212
213 assert_eq!(Ok(()), can_lock_exclusive(&lock_filename));
214 assert_eq!(Ok(()), can_lock_shared(&lock_filename));
215
216 let lock = lock.lock_exclusive()?;
217
218 assert_ne!(Ok(()), can_lock_exclusive(&lock_filename));
219 assert_ne!(Ok(()), can_lock_shared(&lock_filename));
220
221 lock.unlock()?;
222
223 assert_eq!(Ok(()), can_lock_exclusive(&lock_filename));
224 assert_eq!(Ok(()), can_lock_shared(&lock_filename));
225
226 Ok(())
227 }
228
229 #[test]
230 fn file_try_lock_exclusive_does_not_block_on_existing_shared_lock() -> io::Result<()> {
231 let lock_dir = tempfile::tempdir()?;
232 let lock_filename = lock_dir.path().join("lock");
233 let open_lock_file = || {
234 OpenOptions::new()
235 .append(true)
236 .create(true)
237 .open(&lock_filename)
238 .map(UnlockedFile::from)
239 };
240
241 let _lock_shared = open_lock_file()?.lock_shared()?;
242
243 assert!(matches!(
244 open_lock_file()?.try_lock_exclusive(),
245 Ok(Left(_))
246 ));
247
248 Ok(())
249 }
250
251 #[test]
252 fn file_try_lock_exclusive_does_not_block_on_existing_exclusive_lock() -> io::Result<()> {
253 let lock_dir = tempfile::tempdir()?;
254 let lock_filename = lock_dir.path().join("lock");
255 let open_lock_file = || {
256 OpenOptions::new()
257 .append(true)
258 .create(true)
259 .open(&lock_filename)
260 .map(UnlockedFile::from)
261 };
262
263 let _lock_exclusive = open_lock_file()?.lock_exclusive()?;
264
265 assert!(matches!(
266 open_lock_file()?.try_lock_exclusive(),
267 Ok(Left(_)),
268 ));
269
270 Ok(())
271 }
272}