1#![allow(dead_code)] use 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
14pub 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
22pub 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
33pub 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
44pub 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
70pub 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
94pub 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
106pub 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
120pub 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
132pub 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
141pub 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
164pub 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
197pub 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
224pub 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
238pub 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
252pub 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
263pub 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 super::create_dir_all(&fs, "/really/nested/directory").unwrap();
344
345 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}