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}