Skip to main content

maelstrom_util/cache/
fs.rs

1pub mod std;
2pub mod test;
3
4use ::std::{
5    error,
6    ffi::OsString,
7    fmt::Debug,
8    fs::{self},
9    path::{Path, PathBuf},
10};
11use strum::Display;
12
13/// Dependencies that [`Cache`] has on the file system.
14pub trait Fs: Clone {
15    /// Error type for methods.
16    type Error: error::Error + Send + Sync + 'static;
17
18    /// Return a random u64. This is used for creating unique path names.
19    fn rand_u64(&self) -> u64;
20
21    /// Get the metadata of the file at `path`. If `path` doesn't exist, return `Ok(None)`. If the
22    /// last component is a symlink, return the metadata about the symlink instead of trying to
23    /// resolve it. An error may be returned if there is an error attempting to resolve `path`.
24    /// For example, if `path` contains a dangling symlink, or if an intermediate component of the
25    /// path is a file.
26    fn metadata(&self, path: &Path) -> Result<Option<Metadata>, Self::Error>;
27
28    /// Read the first bytes of the file at `path`. If `path` resolves to a symlink, it will be
29    /// resolved, recursively, until a file is found or an error occurs. The number of bytes
30    /// actually read is returned. If this equals the size of the slice passed it, it's
31    /// undetermined whether there are more bytes available in the file.
32    fn read_file(&self, path: &Path, contents: &mut [u8]) -> Result<usize, Self::Error>;
33
34    /// Return and iterator that will yield all of the children of a directory, excluding "." and
35    /// "..". There must be a directory at `path`, or an error will be returned. If `path` resolves
36    /// to a symlink, it will be resolved, recursively, until a directory is found or an error
37    /// occurs.
38    #[allow(clippy::type_complexity)]
39    fn read_dir(
40        &self,
41        path: &Path,
42    ) -> Result<impl Iterator<Item = Result<(OsString, Metadata), Self::Error>>, Self::Error>;
43
44    /// Create a file with given `path` and `contents`. There must not be any file or directory at
45    /// `path`, but its parent directory must be exist.
46    fn create_file(&self, path: &Path, contents: &[u8]) -> Result<(), Self::Error>;
47
48    /// Type used to hold an exclusive lock on a file open. The lock should be released when the
49    /// `FileLock` is dropped.
50    type FileLock;
51
52    /// Try to obtain an exclusive lock on `path`. If `path` doesn't exist, this method will try to
53    /// first create an empty file at `path`. If a file exists at `path` and can be opened, or if
54    /// new a file can be created at `path`, then this method will return `Ok`. Then, if an
55    /// exclusive file lock can be obtained, `Ok(Some)` will be returned, otherwise `Ok(None)`. If
56    /// an other error is encountered, then `Err` will be returned.
57    fn possibly_create_file_and_try_exclusive_lock(
58        &self,
59        path: &Path,
60    ) -> Result<Option<Self::FileLock>, Self::Error>;
61
62    /// Create a symlink at `link` that points to `target`. There must not be any file or directory
63    /// at `link`, but its parent directory must exist.
64    fn symlink(&self, target: &Path, link: &Path) -> Result<(), Self::Error>;
65
66    /// Create an empty directory at `path`. There must not be any file or directory at `path`, but
67    /// its parent directory must exist.
68    fn mkdir(&self, path: &Path) -> Result<(), Self::Error>;
69
70    /// Attempt to create a directory at `path` by creating any missing ancestor directories. Don't
71    /// error if there is already a directory at `path`.
72    fn mkdir_recursively(&self, path: &Path) -> Result<(), Self::Error>;
73
74    /// Remove an existing, non-directory entry at `path`. If `path` resolves to a symlink, the
75    /// symlink will be removed, not the target of the symlink.
76    fn remove(&self, path: &Path) -> Result<(), Self::Error>;
77
78    /// Remove an existing directory, and all of its descendants, on a background thread. `path`
79    /// must exist, and it must be a directory. This function will return an error immediately if
80    /// `path` doesn't exist, can't be resolved, or doesn't point to a directory. Otherwise, the
81    /// removal will happen in the background on another thread. If an error occurs there, the
82    /// calling function won't be notified.
83    fn rmdir_recursively_on_thread(&self, path: PathBuf) -> Result<(), Self::Error>;
84
85    /// Rename `source` to `destination`. There are a bunch of rules that must be satisfied for
86    /// this to succeed:
87    ///   - `source` must exist.
88    ///   - Either `destination` exists, or its parent exists and is a directory.
89    ///   - If `destination` exists and `source` is not a directory, then `destination` must also
90    ///     no be a directory. It will be removed as part of the rename.
91    ///   - If `destination` exists and `source` is a directory, then `destination` must be an
92    ///     empty directory.
93    ///   - If `source` is a directory, it cannot be moved into one of its descendants, as this
94    ///     would create a disconnected cycle.
95    fn rename(&self, source: &Path, destination: &Path) -> Result<(), Self::Error>;
96
97    /// The type returned by the [`Self::temp_file`] method. Some implementations may make this
98    /// type [`Drop`] so that the temporary file can be cleaned up when it is closed.
99    type TempFile: TempFile;
100
101    /// Create a new temporary file in the directory `parent`.
102    fn temp_file(&self, parent: &Path) -> Result<Self::TempFile, Self::Error>;
103
104    /// Rename `temp_file` to `target` while consuming `temp_file`. This is different than the
105    /// caller just doing the rename itself in that it consumes `temp_file` without dropping it.
106    fn persist_temp_file(
107        &self,
108        temp_file: Self::TempFile,
109        target: &Path,
110    ) -> Result<(), Self::Error>;
111
112    /// The type returned by the [`Self::temp_dir`] method. Some implementations may make this
113    /// type [`Drop`] so that the temporary directory can be cleaned up when it is closed.
114    type TempDir: TempDir;
115
116    /// Create a new temporary directory in the directory `parent`.
117    fn temp_dir(&self, parent: &Path) -> Result<Self::TempDir, Self::Error>;
118
119    /// Rename `temp_dir` to `target` while consuming `temp_dir`. This is different than the
120    /// caller just doing the rename itself in that it consumes `temp_dir` without dropping it.
121    fn persist_temp_dir(&self, temp_dir: Self::TempDir, target: &Path) -> Result<(), Self::Error>;
122}
123
124/// A type used to represent a temporary file. The assumption is that the implementer may want to
125/// make the type [`Drop`] so that the temporary file is cleaned up if it isn't consumed.
126pub trait TempFile: Debug {
127    /// Return the path to the temporary file. Can be used to open the file to write into it.
128    fn path(&self) -> &Path;
129}
130
131/// A type used to represent a temporary directory. The assumption is that the implementer may want
132/// to make the type [`Drop`] so that the temporary directory is cleaned up if it isn't consumed.
133pub trait TempDir: Debug {
134    /// Return the path to the temporary directory. Can be used to create files in the directory
135    /// before it is made persistent.
136    fn path(&self) -> &Path;
137}
138
139/// The file metadata returned from [`Fs`].
140#[derive(Clone, Copy, Debug, PartialEq)]
141pub struct Metadata {
142    pub type_: FileType,
143    pub size: u64,
144}
145
146impl Metadata {
147    /// Create a new [`Metadata`] for a directory of size `size`.
148    pub fn directory(size: u64) -> Self {
149        Self {
150            type_: FileType::Directory,
151            size,
152        }
153    }
154
155    /// Create a new [`Metadata`] for a file of size `size`.
156    pub fn file(size: u64) -> Self {
157        Self {
158            type_: FileType::File,
159            size,
160        }
161    }
162
163    /// Create a new [`Metadata`] for a symlink of size `size`.
164    pub fn symlink(size: u64) -> Self {
165        Self {
166            type_: FileType::Symlink,
167            size,
168        }
169    }
170
171    pub fn is_directory(&self) -> bool {
172        matches!(
173            self,
174            Self {
175                type_: FileType::Directory,
176                ..
177            }
178        )
179    }
180
181    pub fn is_file(&self) -> bool {
182        matches!(
183            self,
184            Self {
185                type_: FileType::File,
186                ..
187            }
188        )
189    }
190
191    pub fn is_symlink(&self) -> bool {
192        matches!(
193            self,
194            Self {
195                type_: FileType::Symlink,
196                ..
197            }
198        )
199    }
200}
201
202impl From<fs::Metadata> for Metadata {
203    fn from(metadata: fs::Metadata) -> Self {
204        Self {
205            type_: metadata.file_type().into(),
206            size: metadata.len(),
207        }
208    }
209}
210
211/// The file type returned from [`Fs`].
212#[derive(Clone, Copy, Debug, Display, PartialEq)]
213pub enum FileType {
214    Directory,
215    File,
216    Symlink,
217    Other,
218}
219
220impl From<fs::FileType> for FileType {
221    fn from(file_type: fs::FileType) -> Self {
222        if file_type.is_dir() {
223            FileType::Directory
224        } else if file_type.is_file() {
225            FileType::File
226        } else if file_type.is_symlink() {
227            FileType::Symlink
228        } else {
229            FileType::Other
230        }
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn constructors() {
240        assert_eq!(
241            Metadata::file(3),
242            Metadata {
243                type_: FileType::File,
244                size: 3
245            }
246        );
247        assert_eq!(
248            Metadata::directory(3),
249            Metadata {
250                type_: FileType::Directory,
251                size: 3
252            }
253        );
254        assert_eq!(
255            Metadata::symlink(3),
256            Metadata {
257                type_: FileType::Symlink,
258                size: 3
259            }
260        );
261    }
262
263    #[test]
264    fn is_file() {
265        assert!(Metadata::file(3).is_file());
266        assert!(!Metadata::directory(3).is_file());
267        assert!(!Metadata::symlink(3).is_file());
268        assert!(!Metadata {
269            type_: FileType::Other,
270            size: 3
271        }
272        .is_file());
273    }
274
275    #[test]
276    fn is_directory() {
277        assert!(!Metadata::file(3).is_directory());
278        assert!(Metadata::directory(3).is_directory());
279        assert!(!Metadata::symlink(3).is_directory());
280        assert!(!Metadata {
281            type_: FileType::Other,
282            size: 3
283        }
284        .is_directory());
285    }
286
287    #[test]
288    fn is_symlink() {
289        assert!(!Metadata::file(3).is_symlink());
290        assert!(!Metadata::directory(3).is_symlink());
291        assert!(Metadata::symlink(3).is_symlink());
292        assert!(!Metadata {
293            type_: FileType::Other,
294            size: 3
295        }
296        .is_symlink());
297    }
298}