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        if let Some(filename) = filename.to_string_lossy().strip_prefix(WHITEOUT_PREFIX) {
124            let mut path = path.as_ref().to_owned();
125            path.set_file_name(filename);
126            return Some(path);
127        }
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/// Asynchronously write some bytes to a file.
165///
166/// This is analogous to [`std::fs::write()`].
167pub async fn write<F>(
168    fs: &F,
169    path: impl AsRef<Path> + Send,
170    data: impl AsRef<[u8]> + Send,
171) -> Result<(), FsError>
172where
173    F: FileSystem + ?Sized,
174{
175    let path = path.as_ref();
176    let data = data.as_ref();
177
178    let mut f = fs
179        .new_open_options()
180        .create(true)
181        .truncate(true)
182        .write(true)
183        .open(path)?;
184
185    f.write_all(data).await?;
186    f.flush().await?;
187
188    Ok(())
189}
190
191/// Asynchronously read a file's contents into memory.
192///
193/// This is analogous to [`std::fs::read()`].
194pub async fn read<F>(fs: &F, path: impl AsRef<Path> + Send) -> Result<Vec<u8>, FsError>
195where
196    F: FileSystem + ?Sized,
197{
198    let mut f = fs.new_open_options().read(true).open(path.as_ref())?;
199    let mut buffer = Vec::new();
200    f.read_to_end(&mut buffer).await?;
201
202    Ok(buffer)
203}
204
205/// Asynchronously read a file's contents into memory as a string.
206///
207/// This is analogous to [`std::fs::read_to_string()`].
208pub async fn read_to_string<F>(fs: &F, path: impl AsRef<Path> + Send) -> Result<String, FsError>
209where
210    F: FileSystem + ?Sized,
211{
212    let mut f = fs.new_open_options().read(true).open(path.as_ref())?;
213    let mut buffer = String::new();
214    f.read_to_string(&mut buffer).await?;
215
216    Ok(buffer)
217}
218
219/// Update a file's modification and access times, creating the file if it
220/// doesn't already exist.
221pub fn touch<F>(fs: &F, path: impl AsRef<Path> + Send) -> Result<(), FsError>
222where
223    F: FileSystem + ?Sized,
224{
225    let _ = fs.new_open_options().create(true).write(true).open(path)?;
226
227    Ok(())
228}
229
230/// Recursively iterate over all paths inside a directory, ignoring any
231/// errors that may occur along the way.
232pub fn walk<F>(fs: &F, path: impl AsRef<Path>) -> Box<dyn Iterator<Item = DirEntry> + '_>
233where
234    F: FileSystem + ?Sized,
235{
236    let path = path.as_ref();
237    let mut dirs_to_visit: VecDeque<_> = fs
238        .read_dir(path)
239        .ok()
240        .into_iter()
241        .flatten()
242        .filter_map(|result| result.ok())
243        .collect();
244
245    Box::new(std::iter::from_fn(move || {
246        let next = dirs_to_visit.pop_back()?;
247
248        if let Ok(children) = fs.read_dir(&next.path) {
249            dirs_to_visit.extend(children.flatten());
250        }
251
252        Some(next)
253    }))
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259    use crate::mem_fs::FileSystem as MemFS;
260    use tokio::io::AsyncReadExt;
261
262    #[tokio::test]
263    async fn write() {
264        let fs = MemFS::default();
265
266        super::write(&fs, "/file.txt", b"Hello, World!")
267            .await
268            .unwrap();
269
270        let mut contents = String::new();
271        fs.new_open_options()
272            .read(true)
273            .open("/file.txt")
274            .unwrap()
275            .read_to_string(&mut contents)
276            .await
277            .unwrap();
278        assert_eq!(contents, "Hello, World!");
279    }
280
281    #[tokio::test]
282    async fn read() {
283        let fs = MemFS::default();
284        fs.new_open_options()
285            .create(true)
286            .write(true)
287            .open("/file.txt")
288            .unwrap()
289            .write_all(b"Hello, World!")
290            .await
291            .unwrap();
292
293        let contents = super::read_to_string(&fs, "/file.txt").await.unwrap();
294        assert_eq!(contents, "Hello, World!");
295
296        let contents = super::read(&fs, "/file.txt").await.unwrap();
297        assert_eq!(contents, b"Hello, World!");
298    }
299
300    #[tokio::test]
301    async fn create_dir_all() {
302        let fs = MemFS::default();
303        super::write(&fs, "/file.txt", b"").await.unwrap();
304
305        assert!(!super::exists(&fs, "/really/nested/directory"));
306        super::create_dir_all(&fs, "/really/nested/directory").unwrap();
307        assert!(super::exists(&fs, "/really/nested/directory"));
308
309        // It's okay to create the same directory multiple times
310        super::create_dir_all(&fs, "/really/nested/directory").unwrap();
311
312        // You can't create a directory on top of a file
313        assert_eq!(
314            super::create_dir_all(&fs, "/file.txt").unwrap_err(),
315            FsError::BaseNotDirectory
316        );
317        assert_eq!(
318            super::create_dir_all(&fs, "/file.txt/invalid/path").unwrap_err(),
319            FsError::BaseNotDirectory
320        );
321    }
322
323    #[tokio::test]
324    async fn touch() {
325        let fs = MemFS::default();
326
327        super::touch(&fs, "/file.txt").unwrap();
328
329        assert_eq!(super::read(&fs, "/file.txt").await.unwrap(), b"");
330    }
331}