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