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