1use dashmap::DashMap;
6
7use crate::*;
8
9use std::{path::Path, sync::Arc};
10
11#[derive(Debug)]
12pub struct MountPoint {
13 pub path: PathBuf,
14 pub name: String,
15 pub fs: Arc<Box<dyn FileSystem + Send + Sync>>,
16}
17
18impl MountPoint {
19 pub fn fs(&self) -> &(dyn FileSystem + Send + Sync) {
20 self.fs.as_ref()
21 }
22
23 pub fn mount_point_ref(&self) -> MountPointRef<'_> {
24 MountPointRef {
25 path: self.path.clone(),
26 name: self.name.clone(),
27 fs: &self.fs,
28 }
29 }
30}
31
32#[derive(Debug, Default)]
35pub struct UnionFileSystem {
36 pub mounts: DashMap<PathBuf, MountPoint>,
37}
38
39impl UnionFileSystem {
40 pub fn new() -> Self {
41 Self::default()
42 }
43
44 pub fn clear(&mut self) {
45 self.mounts.clear();
46 }
47
48 fn is_root(&self) -> bool {
49 self.mounts.len() == 1 && self.mounts.contains_key(&PathBuf::from("/"))
50 }
51
52 fn prepare_path(&self, path: &Path) -> PathBuf {
53 if self.is_root() {
54 path.to_owned()
55 } else {
56 path.strip_prefix(PathBuf::from("/"))
57 .unwrap_or(path)
58 .to_owned()
59 }
60 }
61}
62
63impl UnionFileSystem {
64 #[allow(clippy::type_complexity)]
65 fn find_mount(
66 &self,
67 path: PathBuf,
68 ) -> Option<(PathBuf, PathBuf, Arc<Box<dyn FileSystem + Send + Sync>>)> {
69 let mut components = path.components().collect::<Vec<_>>();
70
71 if let Some(c) = components.first().copied() {
72 components.remove(0);
73
74 let sub_path = components.into_iter().collect::<PathBuf>();
75
76 if let Some(mount) = self.mounts.get(&PathBuf::from(c.as_os_str())) {
77 return Some((
78 PathBuf::from(c.as_os_str()),
79 PathBuf::from("/").join(sub_path),
80 mount.fs.clone(),
81 ));
82 }
83 }
84
85 None
86 }
87}
88
89impl FileSystem for UnionFileSystem {
90 fn readlink(&self, path: &Path) -> Result<PathBuf> {
91 let path = self.prepare_path(path);
92
93 if path.as_os_str().is_empty() {
94 Err(FsError::NotAFile)
95 } else {
96 match self.find_mount(path.to_owned()) {
97 Some((_, path, fs)) => fs.readlink(&path),
98 _ => Err(FsError::EntryNotFound),
99 }
100 }
101 }
102
103 fn read_dir(&self, path: &Path) -> Result<ReadDir> {
104 let path = self.prepare_path(path);
105
106 if path.as_os_str().is_empty() {
107 let entries = self
108 .mounts
109 .iter()
110 .map(|i| DirEntry {
111 path: PathBuf::from("/").join(i.key()),
112 metadata: Ok(Metadata {
113 ft: FileType::new_dir(),
114 accessed: 0,
115 created: 0,
116 modified: 0,
117 len: 0,
118 }),
119 })
120 .collect::<Vec<_>>();
121
122 Ok(ReadDir::new(entries))
123 } else {
124 match self.find_mount(path.to_owned()) {
125 Some((prefix, path, fs)) => {
126 let mut entries = fs.read_dir(&path)?;
127
128 for entry in &mut entries.data {
129 let path: PathBuf = entry.path.components().skip(1).collect();
130 entry.path = PathBuf::from("/").join(PathBuf::from(&prefix).join(path));
131 }
132
133 Ok(entries)
134 }
135 _ => Err(FsError::EntryNotFound),
136 }
137 }
138 }
139
140 fn create_dir(&self, path: &Path) -> Result<()> {
141 let path = self.prepare_path(path);
142
143 if path.as_os_str().is_empty() {
144 Ok(())
145 } else {
146 match self.find_mount(path.to_owned()) {
147 Some((_, path, fs)) => {
148 let result = fs.create_dir(&path);
149
150 if let Err(e) = result {
151 if e == FsError::AlreadyExists {
152 return Ok(());
153 }
154 }
155
156 result
157 }
158 _ => Err(FsError::EntryNotFound),
159 }
160 }
161 }
162 fn remove_dir(&self, path: &Path) -> Result<()> {
163 let path = self.prepare_path(path);
164
165 if path.as_os_str().is_empty() {
166 Err(FsError::PermissionDenied)
167 } else {
168 match self.find_mount(path.to_owned()) {
169 Some((_, path, fs)) => fs.remove_dir(&path),
170 _ => Err(FsError::EntryNotFound),
171 }
172 }
173 }
174 fn rename<'a>(&'a self, from: &'a Path, to: &'a Path) -> BoxFuture<'a, Result<()>> {
175 Box::pin(async move {
176 let from = self.prepare_path(from);
177 let to = self.prepare_path(to);
178
179 if from.as_os_str().is_empty() {
180 Err(FsError::PermissionDenied)
181 } else {
182 match self.find_mount(from.to_owned()) {
183 Some((prefix, path, fs)) => {
184 let to = to.strip_prefix(prefix).map_err(|_| FsError::InvalidInput)?;
185
186 let to = PathBuf::from("/").join(to);
187
188 fs.rename(&path, &to).await
189 }
190 _ => Err(FsError::EntryNotFound),
191 }
192 }
193 })
194 }
195 fn metadata(&self, path: &Path) -> Result<Metadata> {
196 let path = self.prepare_path(path);
197
198 if path.as_os_str().is_empty() {
199 Ok(Metadata {
200 ft: FileType::new_dir(),
201 accessed: 0,
202 created: 0,
203 modified: 0,
204 len: 0,
205 })
206 } else {
207 match self.find_mount(path.to_owned()) {
208 Some((_, path, fs)) => fs.metadata(&path),
209 _ => Err(FsError::EntryNotFound),
210 }
211 }
212 }
213 fn symlink_metadata(&self, path: &Path) -> Result<Metadata> {
214 let path = self.prepare_path(path);
215
216 if path.as_os_str().is_empty() {
217 Ok(Metadata {
218 ft: FileType::new_dir(),
219 accessed: 0,
220 created: 0,
221 modified: 0,
222 len: 0,
223 })
224 } else {
225 match self.find_mount(path.to_owned()) {
226 Some((_, path, fs)) => fs.symlink_metadata(&path),
227 _ => Err(FsError::EntryNotFound),
228 }
229 }
230 }
231 fn remove_file(&self, path: &Path) -> Result<()> {
232 let path = self.prepare_path(path);
233
234 if path.as_os_str().is_empty() {
235 Err(FsError::NotAFile)
236 } else {
237 match self.find_mount(path.to_owned()) {
238 Some((_, path, fs)) => fs.remove_file(&path),
239 _ => Err(FsError::EntryNotFound),
240 }
241 }
242 }
243 fn new_open_options(&self) -> OpenOptions<'_> {
244 OpenOptions::new(self)
245 }
246
247 fn mount(
248 &self,
249 name: String,
250 path: &Path,
251 fs: Box<dyn FileSystem + Send + Sync>,
252 ) -> Result<()> {
253 let mut components = path.components().collect::<Vec<_>>();
254 if let Some(c) = components.first().copied() {
255 components.remove(0);
256
257 let sub_path = components.into_iter().collect::<PathBuf>();
258
259 if let Some(mount) = self.mounts.get(&PathBuf::from(c.as_os_str())) {
260 return mount.fs.mount(name, sub_path.as_path(), fs);
261 }
262
263 let fs = if sub_path.components().next().is_none() {
264 fs
265 } else {
266 let union = UnionFileSystem::new();
267 union.mount(name.clone(), sub_path.as_path(), fs)?;
268
269 Box::new(union)
270 };
271
272 let fs = Arc::new(fs);
273
274 let mount = MountPoint {
275 path: PathBuf::from(c.as_os_str()),
276 name,
277 fs,
278 };
279
280 self.mounts.insert(PathBuf::from(c.as_os_str()), mount);
281 } else {
282 return Err(FsError::EntryNotFound);
283 }
284
285 Ok(())
286 }
287}
288
289#[derive(Debug)]
290pub struct MountPointRef<'a> {
291 pub path: PathBuf,
292 pub name: String,
293 pub fs: &'a dyn FileSystem,
294}
295
296impl FileOpener for UnionFileSystem {
297 fn open(
298 &self,
299 path: &Path,
300 conf: &OpenOptionsConfig,
301 ) -> Result<Box<dyn VirtualFile + Send + Sync>> {
302 let path = self.prepare_path(path);
303
304 if path.as_os_str().is_empty() {
305 Err(FsError::NotAFile)
306 } else {
307 let parent = path.parent().unwrap();
308 let file_name = path.file_name().unwrap();
309 match self.find_mount(parent.to_owned()) {
310 Some((_, path, fs)) => fs
311 .new_open_options()
312 .options(conf.clone())
313 .open(path.join(file_name)),
314 _ => Err(FsError::EntryNotFound),
315 }
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use std::{
323 collections::HashSet,
324 path::{Path, PathBuf},
325 };
326
327 use tokio::io::AsyncWriteExt;
328
329 use crate::{FileSystem as FileSystemTrait, FsError, UnionFileSystem, mem_fs};
330
331 use super::{FileOpener, OpenOptionsConfig};
332
333 fn gen_filesystem() -> UnionFileSystem {
334 let union = UnionFileSystem::new();
335 let a = mem_fs::FileSystem::default();
336 let b = mem_fs::FileSystem::default();
337 let c = mem_fs::FileSystem::default();
338 let d = mem_fs::FileSystem::default();
339 let e = mem_fs::FileSystem::default();
340 let f = mem_fs::FileSystem::default();
341 let g = mem_fs::FileSystem::default();
342 let h = mem_fs::FileSystem::default();
343
344 union
345 .mount(
346 "mem_fs_1".to_string(),
347 PathBuf::from("/test_new_filesystem").as_path(),
348 Box::new(a),
349 )
350 .unwrap();
351 union
352 .mount(
353 "mem_fs_2".to_string(),
354 PathBuf::from("/test_create_dir").as_path(),
355 Box::new(b),
356 )
357 .unwrap();
358 union
359 .mount(
360 "mem_fs_3".to_string(),
361 PathBuf::from("/test_remove_dir").as_path(),
362 Box::new(c),
363 )
364 .unwrap();
365 union
366 .mount(
367 "mem_fs_4".to_string(),
368 PathBuf::from("/test_rename").as_path(),
369 Box::new(d),
370 )
371 .unwrap();
372 union
373 .mount(
374 "mem_fs_5".to_string(),
375 PathBuf::from("/test_metadata").as_path(),
376 Box::new(e),
377 )
378 .unwrap();
379 union
380 .mount(
381 "mem_fs_6".to_string(),
382 PathBuf::from("/test_remove_file").as_path(),
383 Box::new(f),
384 )
385 .unwrap();
386 union
387 .mount(
388 "mem_fs_6".to_string(),
389 PathBuf::from("/test_readdir").as_path(),
390 Box::new(g),
391 )
392 .unwrap();
393 union
394 .mount(
395 "mem_fs_6".to_string(),
396 PathBuf::from("/test_canonicalize").as_path(),
397 Box::new(h),
398 )
399 .unwrap();
400
401 union
402 }
403
404 fn gen_nested_filesystem() -> UnionFileSystem {
405 let union = UnionFileSystem::new();
406 let a = mem_fs::FileSystem::default();
407 a.open(
408 &PathBuf::from("/data-a.txt"),
409 &OpenOptionsConfig {
410 read: true,
411 write: true,
412 create_new: false,
413 create: true,
414 append: false,
415 truncate: false,
416 },
417 )
418 .unwrap();
419 let b = mem_fs::FileSystem::default();
420 b.open(
421 &PathBuf::from("/data-b.txt"),
422 &OpenOptionsConfig {
423 read: true,
424 write: true,
425 create_new: false,
426 create: true,
427 append: false,
428 truncate: false,
429 },
430 )
431 .unwrap();
432
433 union
434 .mount(
435 "mem_fs_1".to_string(),
436 PathBuf::from("/app/a").as_path(),
437 Box::new(a),
438 )
439 .unwrap();
440 union
441 .mount(
442 "mem_fs_2".to_string(),
443 PathBuf::from("/app/b").as_path(),
444 Box::new(b),
445 )
446 .unwrap();
447
448 union
449 }
450
451 #[tokio::test]
452 async fn test_nested_read_dir() {
453 let fs = gen_nested_filesystem();
454
455 let root_contents: Vec<PathBuf> = fs
456 .read_dir(&PathBuf::from("/"))
457 .unwrap()
458 .map(|e| e.unwrap().path.clone())
459 .collect();
460 assert_eq!(root_contents, vec![PathBuf::from("/app")]);
461
462 let app_contents: HashSet<PathBuf> = fs
463 .read_dir(&PathBuf::from("/app"))
464 .unwrap()
465 .map(|e| e.unwrap().path)
466 .collect();
467 assert_eq!(
468 app_contents,
469 HashSet::from_iter([PathBuf::from("/app/a"), PathBuf::from("/app/b")].into_iter())
470 );
471
472 let a_contents: Vec<PathBuf> = fs
473 .read_dir(&PathBuf::from("/app/a"))
474 .unwrap()
475 .map(|e| e.unwrap().path.clone())
476 .collect();
477 assert_eq!(a_contents, vec![PathBuf::from("/app/a/data-a.txt")]);
478
479 let b_contents: Vec<PathBuf> = fs
480 .read_dir(&PathBuf::from("/app/b"))
481 .unwrap()
482 .map(|e| e.unwrap().path)
483 .collect();
484 assert_eq!(b_contents, vec![PathBuf::from("/app/b/data-b.txt")]);
485 }
486
487 #[tokio::test]
488 async fn test_nested_metadata() {
489 let fs = gen_nested_filesystem();
490
491 assert!(fs.metadata(&PathBuf::from("/")).is_ok());
492 assert!(fs.metadata(&PathBuf::from("/app")).is_ok());
493 assert!(fs.metadata(&PathBuf::from("/app/a")).is_ok());
494 assert!(fs.metadata(&PathBuf::from("/app/b")).is_ok());
495 assert!(fs.metadata(&PathBuf::from("/app/a/data-a.txt")).is_ok());
496 assert!(fs.metadata(&PathBuf::from("/app/b/data-b.txt")).is_ok());
497 }
498
499 #[tokio::test]
500 async fn test_nested_symlink_metadata() {
501 let fs = gen_nested_filesystem();
502
503 assert!(fs.symlink_metadata(&PathBuf::from("/")).is_ok());
504 assert!(fs.symlink_metadata(&PathBuf::from("/app")).is_ok());
505 assert!(fs.symlink_metadata(&PathBuf::from("/app/a")).is_ok());
506 assert!(fs.symlink_metadata(&PathBuf::from("/app/b")).is_ok());
507 assert!(
508 fs.symlink_metadata(&PathBuf::from("/app/a/data-a.txt"))
509 .is_ok()
510 );
511 assert!(
512 fs.symlink_metadata(&PathBuf::from("/app/b/data-b.txt"))
513 .is_ok()
514 );
515 }
516
517 #[tokio::test]
518 async fn test_new_filesystem() {
519 let fs = gen_filesystem();
520 assert!(
521 fs.read_dir(Path::new("/test_new_filesystem")).is_ok(),
522 "hostfs can read root"
523 );
524 let mut file_write = fs
525 .new_open_options()
526 .read(true)
527 .write(true)
528 .create_new(true)
529 .open(Path::new("/test_new_filesystem/foo2.txt"))
530 .unwrap();
531 file_write.write_all(b"hello").await.unwrap();
532 let _ = std::fs::remove_file("/test_new_filesystem/foo2.txt");
533 }
534
535 #[tokio::test]
536 async fn test_create_dir() {
537 let fs = gen_filesystem();
538
539 assert_eq!(fs.create_dir(Path::new("/")), Ok(()));
540
541 assert_eq!(fs.create_dir(Path::new("/test_create_dir")), Ok(()));
542
543 assert_eq!(
544 fs.create_dir(Path::new("/test_create_dir/foo")),
545 Ok(()),
546 "creating a directory",
547 );
548
549 let cur_dir = read_dir_names(&fs, "/test_create_dir");
550
551 if !cur_dir.contains(&"foo".to_string()) {
552 panic!("cur_dir does not contain foo: {cur_dir:#?}");
553 }
554
555 assert!(
556 cur_dir.contains(&"foo".to_string()),
557 "the root is updated and well-defined"
558 );
559
560 assert_eq!(
561 fs.create_dir(Path::new("/test_create_dir/foo/bar")),
562 Ok(()),
563 "creating a sub-directory",
564 );
565
566 let foo_dir = read_dir_names(&fs, "/test_create_dir/foo");
567
568 assert!(
569 foo_dir.contains(&"bar".to_string()),
570 "the foo directory is updated and well-defined"
571 );
572
573 let bar_dir = read_dir_names(&fs, "/test_create_dir/foo/bar");
574
575 assert!(
576 bar_dir.is_empty(),
577 "the foo directory is updated and well-defined"
578 );
579 let _ = fs_extra::remove_items(&["/test_create_dir"]);
580 }
581
582 #[tokio::test]
583 async fn test_remove_dir() {
584 let fs = gen_filesystem();
585
586 assert_eq!(
587 fs.remove_dir(Path::new("/")),
588 Err(FsError::PermissionDenied),
589 "cannot remove the root directory",
590 );
591
592 assert_eq!(
593 fs.remove_dir(Path::new("/foo")),
594 Err(FsError::EntryNotFound),
595 "cannot remove a directory that doesn't exist",
596 );
597
598 assert_eq!(fs.create_dir(Path::new("/test_remove_dir")), Ok(()));
599
600 assert_eq!(
601 fs.create_dir(Path::new("/test_remove_dir/foo")),
602 Ok(()),
603 "creating a directory",
604 );
605
606 assert_eq!(
607 fs.create_dir(Path::new("/test_remove_dir/foo/bar")),
608 Ok(()),
609 "creating a sub-directory",
610 );
611
612 assert!(
613 read_dir_names(&fs, "/test_remove_dir/foo").contains(&"bar".to_string()),
614 "./foo/bar exists"
615 );
616
617 assert_eq!(
618 fs.remove_dir(Path::new("/test_remove_dir/foo")),
619 Err(FsError::DirectoryNotEmpty),
620 "removing a directory that has children",
621 );
622
623 assert_eq!(
624 fs.remove_dir(Path::new("/test_remove_dir/foo/bar")),
625 Ok(()),
626 "removing a sub-directory",
627 );
628
629 assert_eq!(
630 fs.remove_dir(Path::new("/test_remove_dir/foo")),
631 Ok(()),
632 "removing a directory",
633 );
634
635 assert!(
636 !read_dir_names(&fs, "/test_remove_dir").contains(&"foo".to_string()),
637 "the foo directory still exists"
638 );
639 }
640
641 fn read_dir_names(fs: &dyn crate::FileSystem, path: &str) -> Vec<String> {
642 fs.read_dir(Path::new(path))
643 .unwrap()
644 .filter_map(|entry| Some(entry.ok()?.file_name().to_str()?.to_string()))
645 .collect::<Vec<_>>()
646 }
647
648 #[tokio::test]
649 async fn test_rename() {
650 let fs = gen_filesystem();
651
652 assert_eq!(
653 fs.rename(Path::new("/"), Path::new("/bar")).await,
654 Err(FsError::PermissionDenied),
655 "renaming a directory that has no parent",
656 );
657 assert_eq!(
658 fs.rename(Path::new("/foo"), Path::new("/")).await,
659 Err(FsError::EntryNotFound),
660 "renaming to a directory that has no parent",
661 );
662
663 assert_eq!(fs.create_dir(Path::new("/test_rename")), Ok(()));
664 assert_eq!(fs.create_dir(Path::new("/test_rename/foo")), Ok(()));
665 assert_eq!(fs.create_dir(Path::new("/test_rename/foo/qux")), Ok(()));
666
667 assert_eq!(
668 fs.rename(
669 Path::new("/test_rename/foo"),
670 Path::new("/test_rename/bar/baz")
671 )
672 .await,
673 Err(FsError::EntryNotFound),
674 "renaming to a directory that has parent that doesn't exist",
675 );
676
677 assert_eq!(fs.create_dir(Path::new("/test_rename/bar")), Ok(()));
678
679 assert_eq!(
680 fs.rename(Path::new("/test_rename/foo"), Path::new("/test_rename/bar"))
681 .await,
682 Ok(()),
683 "renaming to a directory that has parent that exists",
684 );
685
686 assert!(
687 fs.new_open_options()
688 .write(true)
689 .create_new(true)
690 .open(Path::new("/test_rename/bar/hello1.txt"))
691 .is_ok(),
692 "creating a new file (`hello1.txt`)",
693 );
694 assert!(
695 fs.new_open_options()
696 .write(true)
697 .create_new(true)
698 .open(Path::new("/test_rename/bar/hello2.txt"))
699 .is_ok(),
700 "creating a new file (`hello2.txt`)",
701 );
702
703 let cur_dir = read_dir_names(&fs, "/test_rename");
704
705 assert!(
706 !cur_dir.contains(&"foo".to_string()),
707 "the foo directory still exists"
708 );
709
710 assert!(
711 cur_dir.contains(&"bar".to_string()),
712 "the bar directory still exists"
713 );
714
715 let bar_dir = read_dir_names(&fs, "/test_rename/bar");
716
717 if !bar_dir.contains(&"qux".to_string()) {
718 println!("qux does not exist: {bar_dir:?}")
719 }
720
721 let qux_dir = read_dir_names(&fs, "/test_rename/bar/qux");
722
723 assert!(qux_dir.is_empty(), "the qux directory is empty");
724
725 assert!(
726 read_dir_names(&fs, "/test_rename/bar").contains(&"hello1.txt".to_string()),
727 "the /bar/hello1.txt file exists"
728 );
729
730 assert!(
731 read_dir_names(&fs, "/test_rename/bar").contains(&"hello2.txt".to_string()),
732 "the /bar/hello2.txt file exists"
733 );
734
735 assert_eq!(
736 fs.create_dir(Path::new("/test_rename/foo")),
737 Ok(()),
738 "create ./foo again",
739 );
740
741 assert_eq!(
742 fs.rename(
743 Path::new("/test_rename/bar/hello2.txt"),
744 Path::new("/test_rename/foo/world2.txt")
745 )
746 .await,
747 Ok(()),
748 "renaming (and moving) a file",
749 );
750
751 assert_eq!(
752 fs.rename(
753 Path::new("/test_rename/foo"),
754 Path::new("/test_rename/bar/baz")
755 )
756 .await,
757 Ok(()),
758 "renaming a directory",
759 );
760
761 assert_eq!(
762 fs.rename(
763 Path::new("/test_rename/bar/hello1.txt"),
764 Path::new("/test_rename/bar/world1.txt")
765 )
766 .await,
767 Ok(()),
768 "renaming a file (in the same directory)",
769 );
770
771 assert!(
772 read_dir_names(&fs, "/test_rename").contains(&"bar".to_string()),
773 "./bar exists"
774 );
775
776 assert!(
777 read_dir_names(&fs, "/test_rename/bar").contains(&"baz".to_string()),
778 "/bar/baz exists"
779 );
780 assert!(
781 !read_dir_names(&fs, "/test_rename").contains(&"foo".to_string()),
782 "foo does not exist anymore"
783 );
784 assert!(
785 read_dir_names(&fs, "/test_rename/bar/baz").contains(&"world2.txt".to_string()),
786 "/bar/baz/world2.txt exists"
787 );
788 assert!(
789 read_dir_names(&fs, "/test_rename/bar").contains(&"world1.txt".to_string()),
790 "/bar/world1.txt (ex hello1.txt) exists"
791 );
792 assert!(
793 !read_dir_names(&fs, "/test_rename/bar").contains(&"hello1.txt".to_string()),
794 "hello1.txt was moved"
795 );
796 assert!(
797 !read_dir_names(&fs, "/test_rename/bar").contains(&"hello2.txt".to_string()),
798 "hello2.txt was moved"
799 );
800 assert!(
801 read_dir_names(&fs, "/test_rename/bar/baz").contains(&"world2.txt".to_string()),
802 "world2.txt was moved to the correct place"
803 );
804
805 let _ = fs_extra::remove_items(&["/test_rename"]);
806 }
807
808 #[tokio::test]
809 async fn test_metadata() {
810 use std::thread::sleep;
811 use std::time::Duration;
812
813 let fs = gen_filesystem();
814
815 let root_metadata = fs.metadata(Path::new("/test_metadata")).unwrap();
816
817 assert!(root_metadata.ft.dir);
818 assert_eq!(root_metadata.accessed, root_metadata.created);
819 assert_eq!(root_metadata.modified, root_metadata.created);
820 assert!(root_metadata.modified > 0);
821
822 assert_eq!(fs.create_dir(Path::new("/test_metadata/foo")), Ok(()));
823
824 let foo_metadata = fs.metadata(Path::new("/test_metadata/foo"));
825 assert!(foo_metadata.is_ok());
826 let foo_metadata = foo_metadata.unwrap();
827
828 assert!(foo_metadata.ft.dir);
829 assert!(foo_metadata.accessed == foo_metadata.created);
830 assert!(foo_metadata.modified == foo_metadata.created);
831 assert!(foo_metadata.modified > 0);
832
833 sleep(Duration::from_secs(3));
834
835 assert_eq!(
836 fs.rename(
837 Path::new("/test_metadata/foo"),
838 Path::new("/test_metadata/bar")
839 )
840 .await,
841 Ok(())
842 );
843
844 let bar_metadata = fs.metadata(Path::new("/test_metadata/bar")).unwrap();
845 assert!(bar_metadata.ft.dir);
846 assert!(bar_metadata.accessed == foo_metadata.accessed);
847 assert!(bar_metadata.created == foo_metadata.created);
848 assert!(bar_metadata.modified > foo_metadata.modified);
849
850 let root_metadata = fs.metadata(Path::new("/test_metadata/bar")).unwrap();
851 assert!(
852 root_metadata.modified > foo_metadata.modified,
853 "the parent modified time was updated"
854 );
855
856 let _ = fs_extra::remove_items(&["/test_metadata"]);
857 }
858
859 #[tokio::test]
860 async fn test_remove_file() {
861 let fs = gen_filesystem();
862
863 assert!(
864 fs.new_open_options()
865 .write(true)
866 .create_new(true)
867 .open(Path::new("/test_remove_file/foo.txt"))
868 .is_ok(),
869 "creating a new file",
870 );
871
872 assert!(read_dir_names(&fs, "/test_remove_file").contains(&"foo.txt".to_string()));
873
874 assert_eq!(
875 fs.remove_file(Path::new("/test_remove_file/foo.txt")),
876 Ok(()),
877 "removing a file that exists",
878 );
879
880 assert!(!read_dir_names(&fs, "/test_remove_file").contains(&"foo.txt".to_string()));
881
882 assert_eq!(
883 fs.remove_file(Path::new("/test_remove_file/foo.txt")),
884 Err(FsError::EntryNotFound),
885 "removing a file that doesn't exists",
886 );
887
888 let _ = fs_extra::remove_items(&["./test_remove_file"]);
889 }
890
891 #[tokio::test]
892 async fn test_readdir() {
893 let fs = gen_filesystem();
894
895 assert_eq!(
896 fs.create_dir(Path::new("/test_readdir/foo")),
897 Ok(()),
898 "creating `foo`"
899 );
900 assert_eq!(
901 fs.create_dir(Path::new("/test_readdir/foo/sub")),
902 Ok(()),
903 "creating `sub`"
904 );
905 assert_eq!(
906 fs.create_dir(Path::new("/test_readdir/bar")),
907 Ok(()),
908 "creating `bar`"
909 );
910 assert_eq!(
911 fs.create_dir(Path::new("/test_readdir/baz")),
912 Ok(()),
913 "creating `bar`"
914 );
915 assert!(
916 fs.new_open_options()
917 .write(true)
918 .create_new(true)
919 .open(Path::new("/test_readdir/a.txt"))
920 .is_ok(),
921 "creating `a.txt`",
922 );
923 assert!(
924 fs.new_open_options()
925 .write(true)
926 .create_new(true)
927 .open(Path::new("/test_readdir/b.txt"))
928 .is_ok(),
929 "creating `b.txt`",
930 );
931
932 println!("fs: {fs:?}");
933
934 let readdir = fs.read_dir(Path::new("/test_readdir"));
935
936 assert!(readdir.is_ok(), "reading the directory `/test_readdir/`");
937
938 let mut readdir = readdir.unwrap();
939
940 let next = readdir.next().unwrap().unwrap();
941 assert!(next.path.ends_with("foo"), "checking entry #1");
942 println!("entry 1: {next:#?}");
943 assert!(next.file_type().unwrap().is_dir(), "checking entry #1");
944
945 let next = readdir.next().unwrap().unwrap();
946 assert!(next.path.ends_with("bar"), "checking entry #2");
947 assert!(next.file_type().unwrap().is_dir(), "checking entry #2");
948
949 let next = readdir.next().unwrap().unwrap();
950 assert!(next.path.ends_with("baz"), "checking entry #3");
951 assert!(next.file_type().unwrap().is_dir(), "checking entry #3");
952
953 let next = readdir.next().unwrap().unwrap();
954 assert!(next.path.ends_with("a.txt"), "checking entry #2");
955 assert!(next.file_type().unwrap().is_file(), "checking entry #4");
956
957 let next = readdir.next().unwrap().unwrap();
958 assert!(next.path.ends_with("b.txt"), "checking entry #2");
959 assert!(next.file_type().unwrap().is_file(), "checking entry #5");
960
961 if let Some(s) = readdir.next() {
962 panic!("next: {s:?}");
963 }
964
965 let _ = fs_extra::remove_items(&["./test_readdir"]);
966 }
967
968 }