Skip to main content

fs_lock/
lib.rs

1//! Locked files with the same API as normal [`File`]s.
2//!
3//! These use the same mechanisms as, and are interoperable with, Cargo.
4
5use std::{
6    fs::{File, TryLockError},
7    io::{self, IoSlice, IoSliceMut, SeekFrom},
8    ops,
9    path::Path,
10};
11
12use cfg_if::cfg_if;
13
14cfg_if! {
15    if #[cfg(any(target_os = "android", target_os = "illumos"))] {
16        use fs4::FileExt;
17
18        fn lock_exclusive(file: &File) -> io::Result<()> {
19            FileExt::lock(file)
20        }
21
22        fn lock_shared(file: &File) -> io::Result<()> {
23            FileExt::lock_shared(file)
24        }
25
26        fn map_try_lock_result(result: Result<(), fs4::TryLockError>) -> Result<(), TryLockError> {
27            match result {
28                Ok(()) => Ok(()),
29                Err(fs4::TryLockError::WouldBlock) => Err(TryLockError::WouldBlock),
30                Err(fs4::TryLockError::Error(e)) => Err(TryLockError::Error(e)),
31            }
32        }
33
34        fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
35            map_try_lock_result(FileExt::try_lock(file))
36        }
37
38        fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
39            map_try_lock_result(FileExt::try_lock_shared(file))
40        }
41
42        fn unlock(file: &File) -> io::Result<()> {
43            FileExt::unlock(file)
44        }
45    } else {
46        fn lock_exclusive(file: &File) -> io::Result<()> {
47            file.lock()
48        }
49
50        fn lock_shared(file: &File) -> io::Result<()> {
51            file.lock_shared()
52        }
53
54        fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
55            file.try_lock()
56        }
57
58        fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
59            file.try_lock_shared()
60        }
61
62        fn unlock(file: &File) -> io::Result<()> {
63            file.unlock()
64        }
65    }
66}
67
68/// A locked file.
69#[derive(Debug)]
70pub struct FileLock(File, #[cfg(feature = "tracing")] Option<Box<Path>>);
71
72impl FileLock {
73    #[cfg(not(feature = "tracing"))]
74    fn new(file: File) -> Self {
75        Self(file)
76    }
77
78    #[cfg(feature = "tracing")]
79    fn new(file: File) -> Self {
80        Self(file, None)
81    }
82
83    /// Take an exclusive lock on a [`File`].
84    ///
85    /// Note that this operation is blocking, and should not be called in async contexts.
86    pub fn new_exclusive(file: File) -> io::Result<Self> {
87        lock_exclusive(&file)?;
88
89        Ok(Self::new(file))
90    }
91
92    /// Try to take an exclusive lock on a [`File`].
93    ///
94    /// On success returns [`Self`]. On error the original [`File`] and optionally
95    /// an [`io::Error`] if the the failure was caused by anything other than
96    /// the lock being taken already.
97    ///
98    /// Note that this operation is blocking, and should not be called in async contexts.
99    pub fn new_try_exclusive(file: File) -> Result<Self, (File, Option<io::Error>)> {
100        match try_lock_exclusive(&file) {
101            Ok(()) => Ok(Self::new(file)),
102            Err(TryLockError::WouldBlock) => Err((file, None)),
103            Err(TryLockError::Error(e)) => Err((file, Some(e))),
104        }
105    }
106
107    /// Take a shared lock on a [`File`].
108    ///
109    /// Note that this operation is blocking, and should not be called in async contexts.
110    pub fn new_shared(file: File) -> io::Result<Self> {
111        lock_shared(&file)?;
112
113        Ok(Self::new(file))
114    }
115
116    /// Try to take a shared lock on a [`File`].
117    ///
118    /// On success returns [`Self`]. On error the original [`File`] and optionally
119    /// an [`io::Error`] if the the failure was caused by anything other than
120    /// the lock being taken already.
121    ///
122    /// Note that this operation is blocking, and should not be called in async contexts.
123    pub fn new_try_shared(file: File) -> Result<Self, (File, Option<io::Error>)> {
124        match try_lock_shared(&file) {
125            Ok(()) => Ok(Self::new(file)),
126            Err(TryLockError::WouldBlock) => Err((file, None)),
127            Err(TryLockError::Error(e)) => Err((file, Some(e))),
128        }
129    }
130
131    /// Set path to the file for logging on unlock error, if feature tracing is enabled
132    pub fn set_file_path(mut self, path: impl Into<Box<Path>>) -> Self {
133        #[cfg(feature = "tracing")]
134        {
135            self.1 = Some(path.into());
136        }
137        self
138    }
139
140    pub fn get_file_path(&self) -> Option<&Path> {
141        cfg_if! {
142            if #[cfg(feature = "tracing")] {
143                self.1.as_deref()
144            } else {
145                None
146            }
147        }
148    }
149}
150
151impl Drop for FileLock {
152    fn drop(&mut self) {
153        let _res = unlock(&self.0);
154
155        #[cfg(feature = "tracing")]
156        if let Err(err) = _res {
157            use std::fmt;
158
159            struct OptionalPath<'a>(Option<&'a Path>);
160            impl fmt::Display for OptionalPath<'_> {
161                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162                    if let Some(path) = self.0 {
163                        fmt::Display::fmt(&path.display(), f)
164                    } else {
165                        Ok(())
166                    }
167                }
168            }
169
170            tracing::warn!(
171                "Failed to unlock file{}: {err}",
172                OptionalPath(self.1.as_deref()),
173            );
174        }
175    }
176}
177
178impl ops::Deref for FileLock {
179    type Target = File;
180
181    fn deref(&self) -> &Self::Target {
182        &self.0
183    }
184}
185impl ops::DerefMut for FileLock {
186    fn deref_mut(&mut self) -> &mut Self::Target {
187        &mut self.0
188    }
189}
190
191impl io::Write for FileLock {
192    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
193        self.0.write(buf)
194    }
195    fn flush(&mut self) -> io::Result<()> {
196        self.0.flush()
197    }
198
199    fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
200        self.0.write_vectored(bufs)
201    }
202}
203
204impl io::Write for &FileLock {
205    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
206        (&self.0).write(buf)
207    }
208    fn flush(&mut self) -> io::Result<()> {
209        (&self.0).flush()
210    }
211
212    fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
213        (&self.0).write_vectored(bufs)
214    }
215}
216
217impl io::Read for FileLock {
218    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
219        self.0.read(buf)
220    }
221
222    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
223        self.0.read_vectored(bufs)
224    }
225
226    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
227        self.0.read_to_end(buf)
228    }
229
230    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
231        self.0.read_to_string(buf)
232    }
233}
234
235impl io::Read for &FileLock {
236    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
237        (&self.0).read(buf)
238    }
239
240    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
241        (&self.0).read_vectored(bufs)
242    }
243
244    fn read_to_end(&mut self, buf: &mut Vec<u8>) -> io::Result<usize> {
245        (&self.0).read_to_end(buf)
246    }
247
248    fn read_to_string(&mut self, buf: &mut String) -> io::Result<usize> {
249        (&self.0).read_to_string(buf)
250    }
251}
252
253impl io::Seek for FileLock {
254    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
255        self.0.seek(pos)
256    }
257
258    fn rewind(&mut self) -> io::Result<()> {
259        self.0.rewind()
260    }
261    fn stream_position(&mut self) -> io::Result<u64> {
262        self.0.stream_position()
263    }
264}
265
266impl io::Seek for &FileLock {
267    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
268        (&self.0).seek(pos)
269    }
270
271    fn rewind(&mut self) -> io::Result<()> {
272        (&self.0).rewind()
273    }
274    fn stream_position(&mut self) -> io::Result<u64> {
275        (&self.0).stream_position()
276    }
277}