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