virtual_fs/
union_fs.rs

1//! Another implementation of the union that uses paths,
2//! its not as simple as TmpFs. not currently used but was used by
3//! the previoulsy implementation of Deploy - now using TmpFs
4
5use 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/// Allows different filesystems of different types
33/// to be mounted at various mount points
34#[derive(Debug, Default)]
35pub struct UnionFileSystem {
36    pub mounts: DashMap<PathBuf, MountPoint>,
37}
38
39/// Defines how to handle conflicts when merging two UnionFileSystems
40#[derive(Clone, Copy, Debug)]
41pub enum UnionMergeMode {
42    /// Replace existing nodes with the new ones.
43    Replace,
44    /// Skip conflicting nodes, and keep the existing ones.
45    Skip,
46    /// Return an error if a conflict is found.
47    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    /// Merge another UnionFileSystem into this one.
74    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    /// Duplicate this UnionFileSystem.
100    ///
101    /// This differs from the Clone implementation in that it creates a new
102    /// underlying shared map.
103    /// Clone just does a shallow copy.
104    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    /*
1021    #[tokio::test]
1022    async fn test_canonicalize() {
1023        let fs = gen_filesystem();
1024
1025        let root_dir = env!("CARGO_MANIFEST_DIR");
1026
1027        let _ = fs_extra::remove_items(&["./test_canonicalize"]);
1028
1029        assert_eq!(
1030            fs.create_dir(Path::new("./test_canonicalize")),
1031            Ok(()),
1032            "creating `test_canonicalize`"
1033        );
1034
1035        assert_eq!(
1036            fs.create_dir(Path::new("./test_canonicalize/foo")),
1037            Ok(()),
1038            "creating `foo`"
1039        );
1040        assert_eq!(
1041            fs.create_dir(Path::new("./test_canonicalize/foo/bar")),
1042            Ok(()),
1043            "creating `bar`"
1044        );
1045        assert_eq!(
1046            fs.create_dir(Path::new("./test_canonicalize/foo/bar/baz")),
1047            Ok(()),
1048            "creating `baz`",
1049        );
1050        assert_eq!(
1051            fs.create_dir(Path::new("./test_canonicalize/foo/bar/baz/qux")),
1052            Ok(()),
1053            "creating `qux`",
1054        );
1055        assert!(
1056            matches!(
1057                fs.new_open_options()
1058                    .write(true)
1059                    .create_new(true)
1060                    .open(Path::new("./test_canonicalize/foo/bar/baz/qux/hello.txt")),
1061                Ok(_)
1062            ),
1063            "creating `hello.txt`",
1064        );
1065
1066        assert_eq!(
1067            fs.canonicalize(Path::new("./test_canonicalize")),
1068            Ok(Path::new(&format!("{root_dir}/test_canonicalize")).to_path_buf()),
1069            "canonicalizing `/`",
1070        );
1071        assert_eq!(
1072            fs.canonicalize(Path::new("foo")),
1073            Err(FsError::InvalidInput),
1074            "canonicalizing `foo`",
1075        );
1076        assert_eq!(
1077            fs.canonicalize(Path::new("./test_canonicalize/././././foo/")),
1078            Ok(Path::new(&format!("{root_dir}/test_canonicalize/foo")).to_path_buf()),
1079            "canonicalizing `/././././foo/`",
1080        );
1081        assert_eq!(
1082            fs.canonicalize(Path::new("./test_canonicalize/foo/bar//")),
1083            Ok(Path::new(&format!("{root_dir}/test_canonicalize/foo/bar")).to_path_buf()),
1084            "canonicalizing `/foo/bar//`",
1085        );
1086        assert_eq!(
1087            fs.canonicalize(Path::new("./test_canonicalize/foo/bar/../bar")),
1088            Ok(Path::new(&format!("{root_dir}/test_canonicalize/foo/bar")).to_path_buf()),
1089            "canonicalizing `/foo/bar/../bar`",
1090        );
1091        assert_eq!(
1092            fs.canonicalize(Path::new("./test_canonicalize/foo/bar/../..")),
1093            Ok(Path::new(&format!("{root_dir}/test_canonicalize")).to_path_buf()),
1094            "canonicalizing `/foo/bar/../..`",
1095        );
1096        assert_eq!(
1097            fs.canonicalize(Path::new("/foo/bar/../../..")),
1098            Err(FsError::InvalidInput),
1099            "canonicalizing `/foo/bar/../../..`",
1100        );
1101        assert_eq!(
1102            fs.canonicalize(Path::new("C:/foo/")),
1103            Err(FsError::InvalidInput),
1104            "canonicalizing `C:/foo/`",
1105        );
1106        assert_eq!(
1107            fs.canonicalize(Path::new(
1108                "./test_canonicalize/foo/./../foo/bar/../../foo/bar/./baz/./../baz/qux/../../baz/./qux/hello.txt"
1109            )),
1110            Ok(Path::new(&format!("{root_dir}/test_canonicalize/foo/bar/baz/qux/hello.txt")).to_path_buf()),
1111            "canonicalizing a crazily stupid path name",
1112        );
1113
1114        let _ = fs_extra::remove_items(&["./test_canonicalize"]);
1115    }
1116    */
1117}