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 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 assert!(dir.stat(Path::new("test")).is_err());
106
107 assert!(dir.open_read(Path::new("/etc/hosts")).is_err());
109 }
110
111 {
112 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 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 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 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 #[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}