endbasic_std/storage/
fs.rs1use crate::storage::{Drive, DriveFactory, DriveFiles, Metadata};
19use async_trait::async_trait;
20use std::collections::BTreeMap;
21use std::fs::{self, File, OpenOptions};
22use std::io::{self, Read, Write};
23use std::path::PathBuf;
24use std::str;
25
26pub struct DirectoryDrive {
28 dir: PathBuf,
32}
33
34impl DirectoryDrive {
35 pub fn new<P: Into<PathBuf>>(dir: P) -> io::Result<Self> {
37 let dir = dir.into();
38
39 let dir = match dir.canonicalize() {
42 Ok(dir) => dir,
43 Err(e) if e.kind() == io::ErrorKind::NotFound => {
44 fs::create_dir_all(&dir)?;
45 dir.canonicalize()?
46 }
47 Err(e) => return Err(e),
48 };
49
50 Ok(Self { dir })
51 }
52}
53
54#[async_trait(?Send)]
55impl Drive for DirectoryDrive {
56 async fn delete(&mut self, name: &str) -> io::Result<()> {
57 let path = self.dir.join(name);
58 fs::remove_file(path)
59 }
60
61 async fn enumerate(&self) -> io::Result<DriveFiles> {
62 let mut entries = BTreeMap::default();
63 match fs::read_dir(&self.dir) {
64 Ok(dirents) => {
65 for de in dirents {
66 let de = de?;
67
68 let file_type = de.file_type()?;
69 if !file_type.is_file() && !file_type.is_symlink() {
70 continue;
72 }
73
74 let metadata = fs::metadata(de.path())?;
78 let offset = match time::UtcOffset::current_local_offset() {
79 Ok(offset) => offset,
80 Err(_) => time::UtcOffset::UTC,
81 };
82 let date = time::OffsetDateTime::from(metadata.modified()?).to_offset(offset);
83 let length = metadata.len();
84
85 entries.insert(
86 de.file_name().to_string_lossy().to_string(),
87 Metadata { date, length },
88 );
89 }
90 }
91 Err(e) => {
92 if e.kind() != io::ErrorKind::NotFound {
93 return Err(e);
94 }
95 }
96 }
97 Ok(DriveFiles::new(entries, None, None))
99 }
100
101 async fn get(&self, name: &str) -> io::Result<String> {
102 let path = self.dir.join(name);
103 let input = File::open(path)?;
104 let mut content = String::new();
105 io::BufReader::new(input).read_to_string(&mut content)?;
106 Ok(content)
107 }
108
109 async fn put(&mut self, name: &str, content: &str) -> io::Result<()> {
110 let path = self.dir.join(name);
111 let output = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
112 let mut writer = io::BufWriter::new(output);
113 writer.write_all(content.as_bytes())
114 }
115
116 fn system_path(&self, name: &str) -> Option<PathBuf> {
117 Some(self.dir.join(name))
118 }
119}
120
121#[derive(Default)]
123pub struct DirectoryDriveFactory {}
124
125impl DriveFactory for DirectoryDriveFactory {
126 fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
127 if !target.is_empty() {
128 Ok(Box::from(DirectoryDrive::new(target)?))
129 } else {
130 Err(io::Error::new(
131 io::ErrorKind::InvalidInput,
132 "Must specify a directory to mount a disk-backed drive",
133 ))
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use futures_lite::future::block_on;
142 use std::fs;
143 use std::io::{BufRead, Write};
144 use std::path::Path;
145
146 fn check_file(path: &Path, exp_lines: &[&str]) {
148 let file = File::open(path).unwrap();
149 let reader = io::BufReader::new(file);
150 let mut lines = vec![];
151 for line in reader.lines() {
152 lines.push(line.unwrap());
153 }
154 assert_eq!(exp_lines, lines.as_slice());
155 }
156
157 fn write_file(path: &Path, lines: &[&str]) {
159 let mut file = OpenOptions::new()
160 .create(true)
161 .truncate(false) .write(true)
163 .open(path)
164 .unwrap();
165 for line in lines {
166 file.write_fmt(format_args!("{}\n", line)).unwrap();
167 }
168 drop(file);
169
170 filetime::set_file_mtime(path, filetime::FileTime::from_unix_time(1_588_757_875, 0))
171 .unwrap();
172 }
173
174 #[test]
175 fn test_directorydrive_delete_ok() {
176 let dir = tempfile::tempdir().unwrap();
177 write_file(&dir.path().join("a.bas"), &[]);
178 write_file(&dir.path().join("a.bat"), &[]);
179
180 let mut drive = DirectoryDrive::new(dir.path()).unwrap();
181 block_on(drive.delete("a.bas")).unwrap();
182 assert!(!dir.path().join("a.bas").exists());
183 assert!(dir.path().join("a.bat").exists());
184 }
185
186 #[test]
187 fn test_directorydrive_delete_missing_file() {
188 let dir = tempfile::tempdir().unwrap();
189 let mut drive = DirectoryDrive::new(dir.path()).unwrap();
190 assert_eq!(io::ErrorKind::NotFound, block_on(drive.delete("a.bas")).unwrap_err().kind());
191 }
192
193 #[test]
194 fn test_directorydrive_enumerate_nothing() {
195 let dir = tempfile::tempdir().unwrap();
196
197 let drive = DirectoryDrive::new(dir.path()).unwrap();
198 assert!(block_on(drive.enumerate()).unwrap().dirents().is_empty());
199 }
200
201 #[test]
202 fn test_directorydrive_enumerate_some_files() {
203 let dir = tempfile::tempdir().unwrap();
204 write_file(&dir.path().join("empty.bas"), &[]);
205 write_file(&dir.path().join("some file.bas"), &["this is not empty"]);
206
207 let drive = DirectoryDrive::new(dir.path()).unwrap();
208 let files = block_on(drive.enumerate()).unwrap();
209 assert_eq!(2, files.dirents().len());
210 let date = time::OffsetDateTime::from_unix_timestamp(1_588_757_875).unwrap();
211 assert_eq!(&Metadata { date, length: 0 }, files.dirents().get("empty.bas").unwrap());
212 assert_eq!(&Metadata { date, length: 18 }, files.dirents().get("some file.bas").unwrap());
213 }
214
215 #[test]
216 fn test_directorydrive_enumerate_treats_missing_dir_as_empty() {
217 let dir = tempfile::tempdir().unwrap();
218 let drive = DirectoryDrive::new(dir.path().join("does-not-exist")).unwrap();
219 assert!(block_on(drive.enumerate()).unwrap().dirents().is_empty());
220 }
221
222 #[test]
223 fn test_directorydrive_enumerate_ignores_non_files() {
224 let dir = tempfile::tempdir().unwrap();
225 fs::create_dir(dir.path().join("will-be-ignored")).unwrap();
226 let drive = DirectoryDrive::new(dir.path()).unwrap();
227 assert!(block_on(drive.enumerate()).unwrap().dirents().is_empty());
228 }
229
230 #[cfg(not(target_os = "windows"))]
231 #[test]
232 fn test_directorydrive_enumerate_follows_symlinks() {
233 use std::os::unix::fs as unix_fs;
234
235 let dir = tempfile::tempdir().unwrap();
236 write_file(&dir.path().join("some file.bas"), &["this is not empty"]);
237 unix_fs::symlink(Path::new("some file.bas"), dir.path().join("a link.bas")).unwrap();
238
239 let drive = DirectoryDrive::new(dir.path()).unwrap();
240 let files = block_on(drive.enumerate()).unwrap();
241 assert_eq!(2, files.dirents().len());
242 let metadata = Metadata {
243 date: time::OffsetDateTime::from_unix_timestamp(1_588_757_875).unwrap(),
244 length: 18,
245 };
246 assert_eq!(&metadata, files.dirents().get("some file.bas").unwrap());
247 assert_eq!(&metadata, files.dirents().get("a link.bas").unwrap());
248 }
249
250 #[test]
251 fn test_directorydrive_enumerate_fails_on_non_directory() {
252 let dir = tempfile::tempdir().unwrap();
253 let file = dir.path().join("not-a-dir");
254 write_file(&file, &[]);
255 let drive = DirectoryDrive::new(&file).unwrap();
256 block_on(drive.enumerate()).unwrap_err();
261 }
262
263 #[test]
264 fn test_directorydrive_get() {
265 let dir = tempfile::tempdir().unwrap();
266 write_file(&dir.path().join("some file.bas"), &["one line", "two lines"]);
267
268 let drive = DirectoryDrive::new(dir.path()).unwrap();
269 assert_eq!("one line\ntwo lines\n", block_on(drive.get("some file.bas")).unwrap());
270 }
271
272 #[test]
273 fn test_directorydrive_put() {
274 let dir = tempfile::tempdir().unwrap();
275
276 let mut drive = DirectoryDrive::new(dir.path()).unwrap();
277 block_on(drive.put("some file.bas", "a b c\nd e\n")).unwrap();
278 check_file(&dir.path().join("some file.bas"), &["a b c", "d e"]);
279 }
280
281 #[test]
282 fn test_directorydrive_system_path() {
283 let dir = tempfile::tempdir().unwrap();
284
285 let drive = DirectoryDrive::new(dir.path()).unwrap();
286 assert_eq!(
287 dir.path().canonicalize().unwrap().join("foo"),
288 drive.system_path("foo").unwrap()
289 );
290 }
291}