exocore_core/dir/
mod.rs

1use std::{
2    ops::Deref,
3    path::{Path, PathBuf},
4};
5
6use self::scoped::ScopedDirectory;
7
8pub mod os;
9pub mod ram;
10pub mod scoped;
11#[cfg(feature = "web")]
12pub mod web;
13
14pub trait Directory: Send + Sync {
15    fn open_read(&self, path: &Path) -> Result<Box<dyn FileRead>, Error>;
16    fn open_write(&self, path: &Path) -> Result<Box<dyn FileWrite>, Error>;
17    fn open_create(&self, path: &Path) -> Result<Box<dyn FileWrite>, Error>;
18    fn list(&self, prefix: Option<&Path>) -> Result<Vec<Box<dyn FileStat>>, Error>;
19    fn stat(&self, path: &Path) -> Result<Box<dyn FileStat>, Error>;
20    fn exists(&self, path: &Path) -> bool;
21    fn delete(&self, path: &Path) -> Result<(), Error>;
22    fn clone(&self) -> DynDirectory;
23    fn as_os_path(&self) -> Result<PathBuf, Error>;
24
25    fn scope(&self, path: PathBuf) -> DynDirectory {
26        let dir = self.clone();
27        ScopedDirectory::new(dir, path).into()
28    }
29
30    fn copy_to(&self, to: DynDirectory) -> Result<(), Error> {
31        let file_stats = self.list(None)?;
32        for file_stat in file_stats {
33            let mut src_file = self.open_read(file_stat.path())?;
34            let mut dst_file = to.open_create(file_stat.path())?;
35            std::io::copy(&mut src_file, &mut dst_file)?;
36        }
37        Ok(())
38    }
39}
40
41pub struct DynDirectory(pub Box<dyn Directory>);
42
43impl Clone for DynDirectory {
44    fn clone(&self) -> Self {
45        self.0.clone()
46    }
47}
48
49impl Deref for DynDirectory {
50    type Target = dyn Directory;
51
52    fn deref(&self) -> &Self::Target {
53        self.0.as_ref()
54    }
55}
56
57impl<D: Directory + 'static> From<D> for DynDirectory {
58    fn from(dir: D) -> Self {
59        DynDirectory(Box::new(dir))
60    }
61}
62
63pub trait FileRead: std::io::Read + std::io::Seek + Send {}
64
65pub trait FileWrite: std::io::Write + std::io::Read + std::io::Seek + Send {}
66
67pub trait FileStat {
68    fn path(&self) -> &Path;
69    fn size(&self) -> u64;
70}
71
72#[derive(thiserror::Error, Debug)]
73pub enum Error {
74    #[error("IO error: {0}")]
75    IO(#[from] std::io::Error),
76
77    #[error("File not found: {0}")]
78    NotFound(PathBuf),
79
80    #[error("Path error: {0}")]
81    Path(#[source] anyhow::Error),
82
83    #[error("Not a OsDirectory")]
84    NotOsDirectory,
85
86    #[error("Other: {0}")]
87    Other(#[from] anyhow::Error),
88}
89
90#[cfg(test)]
91mod tests {
92    use std::io::SeekFrom;
93
94    use super::*;
95
96    pub fn test_write_read_file(dir: impl Into<DynDirectory>) {
97        let dir = dir.into();
98        {
99            // cannot create empty path
100            assert!(dir.open_write(Path::new("")).is_err());
101            assert!(dir.open_read(Path::new("")).is_err());
102            assert!(dir.stat(Path::new("")).is_err());
103
104            // inexistent file
105            assert!(dir.stat(Path::new("test")).is_err());
106
107            // not under path
108            assert!(dir.open_read(Path::new("/etc/hosts")).is_err());
109        }
110
111        {
112            // can create file
113            assert!(!dir.exists(Path::new("file1")));
114
115            let mut file = dir.open_create(Path::new("file1")).unwrap();
116            file.write_all(b"Hello ").unwrap();
117            file.write_all(b"world").unwrap();
118
119            let stat = dir.stat(Path::new("file1")).unwrap();
120            assert_eq!(stat.path(), Path::new("file1"));
121            assert_eq!(stat.size(), 11);
122
123            assert!(dir.exists(Path::new("file1")));
124        }
125
126        {
127            // can read the file
128            let mut file = dir.open_read(Path::new("file1")).unwrap();
129            let mut buf = String::new();
130            file.read_to_string(&mut buf).unwrap();
131            assert_eq!("Hello world", buf);
132
133            buf.clear();
134            file.read_to_string(&mut buf).unwrap();
135            assert_eq!("", buf);
136        }
137
138        {
139            // can seek
140            let mut file = dir.open_write(Path::new("file1")).unwrap();
141
142            file.seek(SeekFrom::Start(6)).unwrap();
143            file.write_all(b"monde").unwrap();
144
145            let mut buf = String::new();
146            file.read_to_string(&mut buf).unwrap();
147            assert_eq!("", buf);
148
149            file.rewind().unwrap();
150            file.read_to_string(&mut buf).unwrap();
151            assert_eq!("Hello monde", buf);
152
153            file.seek(SeekFrom::End(-5)).unwrap();
154            buf.clear();
155            file.read_to_string(&mut buf).unwrap();
156            assert_eq!("monde", buf);
157
158            file.seek(SeekFrom::Current(-5)).unwrap();
159            buf.clear();
160            file.read_to_string(&mut buf).unwrap();
161            assert_eq!("monde", buf);
162        }
163
164        {
165            // can create / overwrite a file
166            let mut file = dir.open_create(Path::new("file1")).unwrap();
167            file.write_all(b"Yo").unwrap();
168            drop(file);
169
170            let stat = dir.stat(Path::new("file1")).unwrap();
171            assert_eq!(stat.path(), Path::new("file1"));
172            assert_eq!(stat.size(), 2);
173        }
174
175        {
176            // can clone
177            #[allow(clippy::redundant_clone)]
178            let dir = dir.clone();
179            assert!(dir.exists(Path::new("file1")));
180        }
181    }
182
183    pub fn test_list(dir: impl Into<DynDirectory>) {
184        let dir = dir.into();
185        assert!(dir.list(None).unwrap().is_empty());
186        assert!(dir.list(Some(Path::new(""))).unwrap().is_empty());
187
188        {
189            dir.open_write(Path::new("dir1/file1")).unwrap();
190            dir.open_write(Path::new("dir1/file2")).unwrap();
191            dir.open_write(Path::new("dir1/file3")).unwrap();
192            dir.open_write(Path::new("dir2/file1")).unwrap();
193            dir.open_write(Path::new("dir2/file2")).unwrap();
194            dir.open_write(Path::new("file1")).unwrap();
195        }
196
197        assert_eq!(dir.list(Some(Path::new("dir1"))).unwrap().len(), 3);
198        assert_eq!(dir.list(Some(Path::new("dir2"))).unwrap().len(), 2);
199        assert_eq!(dir.list(Some(Path::new("file1"))).unwrap().len(), 1);
200        assert_eq!(dir.list(Some(Path::new(""))).unwrap().len(), 6);
201        assert_eq!(dir.list(None).unwrap().len(), 6);
202        assert_eq!(dir.list(Some(Path::new("not/found"))).unwrap().len(), 0);
203    }
204
205    pub fn test_delete(dir: impl Into<DynDirectory>) {
206        let dir = dir.into();
207        {
208            let mut file = dir.open_write(Path::new("test")).unwrap();
209            file.write_all(b"Hello").unwrap();
210        }
211
212        assert!(dir.exists(Path::new("test")));
213
214        dir.delete(Path::new("test")).unwrap();
215
216        assert!(!dir.exists(Path::new("test")));
217    }
218
219    #[test]
220    pub fn test_copy_directory() {
221        let src = super::ram::RamDirectory::new();
222
223        {
224            let mut f = src.open_create(Path::new("file1")).unwrap();
225            f.write_all(b"file1").unwrap();
226
227            let mut f = src.open_create(Path::new("dir1/file1")).unwrap();
228            f.write_all(b"dir1/file1").unwrap();
229
230            let mut f = src.open_create(Path::new("dir1/file2")).unwrap();
231            f.write_all(b"dir1/file2").unwrap();
232
233            let mut f = src.open_create(Path::new("dir2/file1")).unwrap();
234            f.write_all(b"dir2/file1").unwrap();
235        }
236
237        let dst = super::ram::RamDirectory::new();
238        src.copy_to(dst.clone()).unwrap();
239
240        let files = dst.list(None).unwrap();
241        assert_eq!(files.len(), 4);
242
243        assert!(dst.exists(Path::new("file1")));
244        assert!(dst.exists(Path::new("dir1/file1")));
245        assert!(dst.exists(Path::new("dir1/file2")));
246        assert!(dst.exists(Path::new("dir2/file1")));
247
248        let stat = dst.stat(Path::new("dir1/file1")).unwrap();
249        assert_eq!(stat.size(), 10);
250    }
251}