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::fs_std::FileExt;
17
18        fn lock_exclusive(file: &File) -> io::Result<()> {
19            FileExt::lock_exclusive(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: io::Result<bool>) -> Result<(), TryLockError> {
27            match result {
28                Ok(true) => Ok(()),
29                Ok(false) => Err(TryLockError::WouldBlock),
30                Err(e) if e.raw_os_error() == fs4::lock_contended_error().raw_os_error() => {
31                    Err(TryLockError::WouldBlock)
32                }
33                Err(e) => Err(TryLockError::Error(e)),
34            }
35        }
36
37        fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
38            map_try_lock_result(FileExt::try_lock_exclusive(file))
39        }
40
41        fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
42            map_try_lock_result(FileExt::try_lock_shared(file))
43        }
44
45        fn unlock(file: &File) -> io::Result<()> {
46            FileExt::unlock(file)
47        }
48    } else {
49        fn lock_exclusive(file: &File) -> io::Result<()> {
50            file.lock()
51        }
52
53        fn lock_shared(file: &File) -> io::Result<()> {
54            file.lock_shared()
55        }
56
57        fn try_lock_exclusive(file: &File) -> Result<(), TryLockError> {
58            file.try_lock()
59        }
60
61        fn try_lock_shared(file: &File) -> Result<(), TryLockError> {
62            file.try_lock_shared()
63        }
64
65        fn unlock(file: &File) -> io::Result<()> {
66            file.unlock()
67        }
68    }
69}
70
71/// A locked file.
72#[derive(Debug)]
73pub struct FileLock(File, #[cfg(feature = "tracing")] Option<Box<Path>>);
74
75impl FileLock {
76    #[cfg(not(feature = "tracing"))]
77    fn new(file: File) -> Self {
78        Self(file)
79    }
80
81    #[cfg(feature = "tracing")]
82    fn new(file: File) -> Self {
83        Self(file, None)
84    }
85
86    /// Take an exclusive lock on a [`File`].
87    ///
88    /// Note that this operation is blocking, and should not be called in async contexts.
89    pub fn new_exclusive(file: File) -> io::Result<Self> {
90        lock_exclusive(&file)?;
91
92        Ok(Self::new(file))
93    }
94
95    /// Try to take an exclusive lock on a [`File`].
96    ///
97    /// On success returns [`Self`]. On error the original [`File`] and optionally
98    /// an [`io::Error`] if the the failure was caused by anything other than
99    /// the lock being taken already.
100    ///
101    /// Note that this operation is blocking, and should not be called in async contexts.
102    pub fn new_try_exclusive(file: File) -> Result<Self, (File, Option<io::Error>)> {
103        match try_lock_exclusive(&file) {
104            Ok(()) => Ok(Self::new(file)),
105            Err(TryLockError::WouldBlock) => Err((file, None)),
106            Err(TryLockError::Error(e)) => Err((file, Some(e))),
107        }
108    }
109
110    /// Take a shared lock on a [`File`].
111    ///
112    /// Note that this operation is blocking, and should not be called in async contexts.
113    pub fn new_shared(file: File) -> io::Result<Self> {
114        lock_shared(&file)?;
115
116        Ok(Self::new(file))
117    }
118
119    /// Try to take a shared lock on a [`File`].
120    ///
121    /// On success returns [`Self`]. On error the original [`File`] and optionally
122    /// an [`io::Error`] if the the failure was caused by anything other than
123    /// the lock being taken already.
124    ///
125    /// Note that this operation is blocking, and should not be called in async contexts.
126    pub fn new_try_shared(file: File) -> Result<Self, (File, Option<io::Error>)> {
127        match try_lock_shared(&file) {
128            Ok(()) => Ok(Self::new(file)),
129            Err(TryLockError::WouldBlock) => Err((file, None)),
130            Err(TryLockError::Error(e)) => Err((file, Some(e))),
131        }
132    }
133
134    /// Set path to the file for logging on unlock error, if feature tracing is enabled
135    pub fn set_file_path(mut self, path: impl Into<Box<Path>>) -> Self {
136        #[cfg(feature = "tracing")]
137        {
138            self.1 = Some(path.into());
139        }
140        self
141    }
142}
143
144impl Drop for FileLock {
145    fn drop(&mut self) {
146        let _res = unlock(&self.0);
147
148        #[cfg(feature = "tracing")]
149        if let Err(err) = _res {
150            use std::fmt;
151
152            struct OptionalPath<'a>(Option<&'a Path>);
153            impl fmt::Display for OptionalPath<'_> {
154                fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
155                    if let Some(path) = self.0 {
156                        fmt::Display::fmt(&path.display(), f)
157                    } else {
158                        Ok(())
159                    }
160                }
161            }
162
163            tracing::warn!(
164                "Failed to unlock file{}: {err}",
165                OptionalPath(self.1.as_deref()),
166            );
167        }
168    }
169}
170
171impl ops::Deref for FileLock {
172    type Target = File;
173
174    fn deref(&self) -> &Self::Target {
175        &self.0
176    }
177}
178impl ops::DerefMut for FileLock {
179    fn deref_mut(&mut self) -> &mut Self::Target {
180        &mut self.0
181    }
182}
183
184impl io::Write for FileLock {
185    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
186        self.0.write(buf)
187    }
188    fn flush(&mut self) -> io::Result<()> {
189        self.0.flush()
190    }
191
192    fn write_vectored(&mut self, bufs: &[IoSlice<'_>]) -> io::Result<usize> {
193        self.0.write_vectored(bufs)
194    }
195}
196
197impl io::Read for FileLock {
198    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
199        self.0.read(buf)
200    }
201
202    fn read_vectored(&mut self, bufs: &mut [IoSliceMut<'_>]) -> io::Result<usize> {
203        self.0.read_vectored(bufs)
204    }
205}
206
207impl io::Seek for FileLock {
208    fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
209        self.0.seek(pos)
210    }
211
212    fn rewind(&mut self) -> io::Result<()> {
213        self.0.rewind()
214    }
215    fn stream_position(&mut self) -> io::Result<u64> {
216        self.0.stream_position()
217    }
218}