virtual_fs/
ops.rs

1//! Common [`FileSystem`] operations.
2#![allow(dead_code)] // Most of these helpers are used during testing
3
4use std::{
5    collections::VecDeque,
6    path::{Path, PathBuf},
7};
8
9use futures::future::BoxFuture;
10use tokio::io::{AsyncReadExt, AsyncWriteExt};
11
12use crate::{DirEntry, FileSystem, FsError};
13
14/// Does this item exists?
15pub fn exists<F>(fs: &F, path: impl AsRef<Path>) -> bool
16where
17    F: FileSystem + ?Sized,
18{
19    fs.metadata(path.as_ref()).is_ok()
20}
21
22/// Does this path refer to a directory?
23pub fn is_dir<F>(fs: &F, path: impl AsRef<Path>) -> bool
24where
25    F: FileSystem + ?Sized,
26{
27    match fs.metadata(path.as_ref()) {
28        Ok(meta) => meta.is_dir(),
29        Err(_) => false,
30    }
31}
32
33/// Does this path refer to a file?
34pub fn is_file<F>(fs: &F, path: impl AsRef<Path>) -> bool
35where
36    F: FileSystem + ?Sized,
37{
38    match fs.metadata(path.as_ref()) {
39        Ok(meta) => meta.is_file(),
40        Err(_) => false,
41    }
42}
43
44/// Make sure a directory (and all its parents) exist.
45///
46/// This is analogous to [`std::fs::create_dir_all()`].
47pub fn create_dir_all<F>(fs: &F, path: impl AsRef<Path>) -> Result<(), FsError>
48where
49    F: FileSystem + ?Sized,
50{
51    let path = path.as_ref();
52    if let Some(parent) = path.parent() {
53        create_dir_all(fs, parent)?;
54    }
55
56    if let Ok(metadata) = fs.metadata(path) {
57        if metadata.is_dir() {
58            return Ok(());
59        }
60        if metadata.is_file() {
61            return Err(FsError::BaseNotDirectory);
62        }
63    }
64
65    fs.create_dir(path)
66}
67
68static WHITEOUT_PREFIX: &str = ".wh.";
69
70/// Creates a white out file which hides it from secondary file systems
71pub fn create_white_out<F>(fs: &F, path: impl AsRef<Path>) -> Result<(), FsError>
72where
73    F: FileSystem + ?Sized,
74{
75    if let Some(filename) = path.as_ref().file_name() {
76        let mut path = path.as_ref().to_owned();
77        path.set_file_name(format!("{}{}", WHITEOUT_PREFIX, filename.to_string_lossy()));
78
79        if let Some(parent) = path.parent() {
80            create_dir_all(fs, parent).ok();
81        }
82
83        fs.new_open_options()
84            .create_new(true)
85            .truncate(true)
86            .write(true)
87            .open(path)?;
88        Ok(())
89    } else {
90        Err(FsError::EntryNotFound)
91    }
92}
93
94/// Removes a white out file from the primary
95pub fn remove_white_out<F>(fs: &F, path: impl AsRef<Path>)
96where
97    F: FileSystem + ?Sized,
98{
99    if let Some(filename) = path.as_ref().file_name() {
100        let mut path = path.as_ref().to_owned();
101        path.set_file_name(format!("{}{}", WHITEOUT_PREFIX, filename.to_string_lossy()));
102        fs.remove_file(&path).ok();
103    }
104}
105
106/// Returns true if the path has been hidden by a whiteout file
107pub fn has_white_out<F>(fs: &F, path: impl AsRef<Path>) -> bool
108where
109    F: FileSystem + ?Sized,
110{
111    if let Some(filename) = path.as_ref().file_name() {
112        let mut path = path.as_ref().to_owned();
113        path.set_file_name(format!("{}{}", WHITEOUT_PREFIX, filename.to_string_lossy()));
114        fs.metadata(&path).is_ok()
115    } else {
116        false
117    }
118}
119
120/// Returns true if the path is a whiteout file
121pub fn is_white_out(path: impl AsRef<Path>) -> Option<PathBuf> {
122    if let Some(filename) = path.as_ref().file_name()
123        && let Some(filename) = filename.to_string_lossy().strip_prefix(WHITEOUT_PREFIX)
124    {
125        let mut path = path.as_ref().to_owned();
126        path.set_file_name(filename);
127        return Some(path);
128    }
129    None
130}
131
132/// Copies the reference of a file from one file system to another
133pub fn copy_reference<'a>(
134    source: &'a (impl FileSystem + ?Sized),
135    destination: &'a (impl FileSystem + ?Sized),
136    path: &'a Path,
137) -> BoxFuture<'a, Result<(), std::io::Error>> {
138    Box::pin(async { copy_reference_ext(source, destination, path, path).await })
139}
140
141/// Copies the reference of a file from one file system to another
142pub fn copy_reference_ext<'a>(
143    source: &'a (impl FileSystem + ?Sized),
144    destination: &'a (impl FileSystem + ?Sized),
145    from: &Path,
146    to: &Path,
147) -> BoxFuture<'a, Result<(), std::io::Error>> {
148    let from = from.to_owned();
149    let to = to.to_owned();
150    Box::pin(async move {
151        let src = source.new_open_options().read(true).open(from)?;
152        let mut dst = destination
153            .new_open_options()
154            .create(true)
155            .write(true)
156            .truncate(true)
157            .open(to)?;
158
159        dst.copy_reference(src).await?;
160        Ok(())
161    })
162}
163
164/// Move a file across filesystem boundaries by copying it into the destination
165/// and then removing the original.
166pub fn move_across_filesystems<'a>(
167    source: &'a (impl FileSystem + ?Sized),
168    destination: &'a (impl FileSystem + ?Sized),
169    from: &'a Path,
170    to: &'a Path,
171) -> BoxFuture<'a, crate::Result<()>> {
172    let from = from.to_owned();
173    let to = to.to_owned();
174    Box::pin(async move {
175        let metadata = source.metadata(&from)?;
176        if metadata.is_dir() {
177            return Err(FsError::InvalidInput);
178        }
179
180        copy_reference_ext(source, destination, &from, &to)
181            .await
182            .map_err(FsError::from)?;
183
184        if let Err(error) = source.remove_file(&from) {
185            tracing::warn!(
186                ?from,
187                ?to,
188                ?error,
189                "Failed to remove file after cross-filesystem rename"
190            );
191        }
192
193        Ok(())
194    })
195}
196
197/// Asynchronously write some bytes to a file.
198///
199/// This is analogous to [`std::fs::write()`].
200pub async fn write<F>(
201    fs: &F,
202    path: impl AsRef<Path> + Send,
203    data: impl AsRef<[u8]> + Send,
204) -> Result<(), FsError>
205where
206    F: FileSystem + ?Sized,
207{
208    let path = path.as_ref();
209    let data = data.as_ref();
210
211    let mut f = fs
212        .new_open_options()
213        .create(true)
214        .truncate(true)
215        .write(true)
216        .open(path)?;
217
218    f.write_all(data).await?;
219    f.flush().await?;
220
221    Ok(())
222}
223
224/// Asynchronously read a file's contents into memory.
225///
226/// This is analogous to [`std::fs::read()`].
227pub async fn read<F>(fs: &F, path: impl AsRef<Path> + Send) -> Result<Vec<u8>, FsError>
228where
229    F: FileSystem + ?Sized,
230{
231    let mut f = fs.new_open_options().read(true).open(path.as_ref())?;
232    let mut buffer = Vec::new();
233    f.read_to_end(&mut buffer).await?;
234
235    Ok(buffer)
236}
237
238/// Asynchronously read a file's contents into memory as a string.
239///
240/// This is analogous to [`std::fs::read_to_string()`].
241pub async fn read_to_string<F>(fs: &F, path: impl AsRef<Path> + Send) -> Result<String, FsError>
242where
243    F: FileSystem + ?Sized,
244{
245    let mut f = fs.new_open_options().read(true).open(path.as_ref())?;
246    let mut buffer = String::new();
247    f.read_to_string(&mut buffer).await?;
248
249    Ok(buffer)
250}
251
252/// Update a file's modification and access times, creating the file if it
253/// doesn't already exist.
254pub fn touch<F>(fs: &F, path: impl AsRef<Path> + Send) -> Result<(), FsError>
255where
256    F: FileSystem + ?Sized,
257{
258    let _ = fs.new_open_options().create(true).write(true).open(path)?;
259
260    Ok(())
261}
262
263/// Recursively iterate over all paths inside a directory, ignoring any
264/// errors that may occur along the way.
265pub fn walk<F>(fs: &F, path: impl AsRef<Path>) -> Box<dyn Iterator<Item = DirEntry> + '_>
266where
267    F: FileSystem + ?Sized,
268{
269    let path = path.as_ref();
270    let mut dirs_to_visit: VecDeque<_> = fs
271        .read_dir(path)
272        .ok()
273        .into_iter()
274        .flatten()
275        .filter_map(|result| result.ok())
276        .collect();
277
278    Box::new(std::iter::from_fn(move || {
279        let next = dirs_to_visit.pop_back()?;
280
281        if let Ok(children) = fs.read_dir(&next.path) {
282            dirs_to_visit.extend(children.flatten());
283        }
284
285        Some(next)
286    }))
287}
288
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use crate::mem_fs::FileSystem as MemFS;
293    use tokio::io::AsyncReadExt;
294
295    #[tokio::test]
296    async fn write() {
297        let fs = MemFS::default();
298
299        super::write(&fs, "/file.txt", b"Hello, World!")
300            .await
301            .unwrap();
302
303        let mut contents = String::new();
304        fs.new_open_options()
305            .read(true)
306            .open("/file.txt")
307            .unwrap()
308            .read_to_string(&mut contents)
309            .await
310            .unwrap();
311        assert_eq!(contents, "Hello, World!");
312    }
313
314    #[tokio::test]
315    async fn read() {
316        let fs = MemFS::default();
317        fs.new_open_options()
318            .create(true)
319            .write(true)
320            .open("/file.txt")
321            .unwrap()
322            .write_all(b"Hello, World!")
323            .await
324            .unwrap();
325
326        let contents = super::read_to_string(&fs, "/file.txt").await.unwrap();
327        assert_eq!(contents, "Hello, World!");
328
329        let contents = super::read(&fs, "/file.txt").await.unwrap();
330        assert_eq!(contents, b"Hello, World!");
331    }
332
333    #[tokio::test]
334    async fn create_dir_all() {
335        let fs = MemFS::default();
336        super::write(&fs, "/file.txt", b"").await.unwrap();
337
338        assert!(!super::exists(&fs, "/really/nested/directory"));
339        super::create_dir_all(&fs, "/really/nested/directory").unwrap();
340        assert!(super::exists(&fs, "/really/nested/directory"));
341
342        // It's okay to create the same directory multiple times
343        super::create_dir_all(&fs, "/really/nested/directory").unwrap();
344
345        // You can't create a directory on top of a file
346        assert_eq!(
347            super::create_dir_all(&fs, "/file.txt").unwrap_err(),
348            FsError::BaseNotDirectory
349        );
350        assert_eq!(
351            super::create_dir_all(&fs, "/file.txt/invalid/path").unwrap_err(),
352            FsError::BaseNotDirectory
353        );
354    }
355
356    #[tokio::test]
357    async fn touch() {
358        let fs = MemFS::default();
359
360        super::touch(&fs, "/file.txt").unwrap();
361
362        assert_eq!(super::read(&fs, "/file.txt").await.unwrap(), b"");
363    }
364}