volaris_tools/
storage.rs

1use rand::distributions::{Alphanumeric, DistString};
2use std::cell::RefCell;
3use std::fs;
4use std::io::{Read, Seek, Write};
5use std::path::{Path, PathBuf};
6
7#[cfg(test)]
8use std::collections::HashMap;
9#[cfg(test)]
10use std::io;
11#[cfg(test)]
12use std::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
13#[cfg(test)]
14use std::thread;
15
16#[derive(Debug)]
17pub enum FileMode {
18    Read,
19    Write,
20}
21
22#[derive(Debug)]
23pub enum Error {
24    CreateDir,
25    CreateFile,
26    OpenFile(FileMode),
27    RemoveFile,
28    RemoveDir,
29    DirEntries,
30    FlushFile,
31    FileAccess,
32    FileLen,
33}
34
35impl std::fmt::Display for Error {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            Error::CreateDir => f.write_str("Unable to create a new directory"),
39            Error::CreateFile => f.write_str("Unable to create a new file"),
40            Error::OpenFile(mode) => write!(f, "Unable to read the file in {mode:?} mode"),
41            Error::FlushFile => f.write_str("Unable to flush the file"),
42            Error::RemoveFile => f.write_str("Unable to remove the file"),
43            Error::RemoveDir => f.write_str("Unable to remove dir"),
44            Error::DirEntries => f.write_str("Unable to read directory"),
45            Error::FileAccess => f.write_str("Permission denied"),
46            Error::FileLen => f.write_str("Unable to get file length"),
47        }
48    }
49}
50
51impl std::error::Error for Error {}
52
53pub trait Storage<RW>: Send + Sync
54where
55    RW: Read + Write + Seek,
56{
57    // TODO(pleshevskiy): return a new struct that will be removed on drop.
58    fn create_temp_file(&self) -> Result<Entry<RW>, Error> {
59        let mut path = std::env::temp_dir();
60        let file_name = Alphanumeric.sample_string(&mut rand::thread_rng(), 16);
61        path.push(file_name);
62
63        self.create_file(path)
64    }
65
66    fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> Result<(), Error>;
67    fn create_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<RW>, Error>;
68    fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<RW>, Error>;
69    fn write_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<RW>, Error>;
70    fn flush_file(&self, file: &Entry<RW>) -> Result<(), Error>;
71    fn file_len(&self, file: &Entry<RW>) -> Result<usize, Error>;
72    fn remove_file(&self, file: Entry<RW>) -> Result<(), Error>;
73    fn remove_dir_all(&self, file: Entry<RW>) -> Result<(), Error>;
74    // TODO(pleshevskiy): return iterator instead of Vector
75    fn read_dir(&self, file: &Entry<RW>) -> Result<Vec<Entry<RW>>, Error>;
76}
77
78pub struct FileStorage;
79
80impl Storage<fs::File> for FileStorage {
81    fn create_dir_all<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
82        fs::create_dir_all(&path).map_err(|_| Error::CreateDir)
83    }
84
85    fn create_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<fs::File>, Error> {
86        let path = path.as_ref().to_path_buf();
87        let file = fs::File::options()
88            .create_new(true)
89            .read(true)
90            .write(true)
91            .open(&path)
92            .map_err(|_| Error::CreateFile)?;
93        Ok(Entry::File(FileData {
94            path,
95            stream: RefCell::new(file),
96        }))
97    }
98
99    fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<fs::File>, Error> {
100        let path = path.as_ref().to_path_buf();
101        if path.is_dir() {
102            Ok(Entry::Dir(path))
103        } else {
104            let file = fs::File::open(&path).map_err(|_| Error::OpenFile(FileMode::Read))?;
105            Ok(Entry::File(FileData {
106                path,
107                stream: RefCell::new(file),
108            }))
109        }
110    }
111
112    fn write_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<fs::File>, Error> {
113        let path = path.as_ref().to_path_buf();
114        let file = fs::File::options()
115            .write(true)
116            .read(true)
117            .truncate(true)
118            .open(&path)
119            .map_err(|_| Error::OpenFile(FileMode::Write))?;
120
121        Ok(Entry::File(FileData {
122            path,
123            stream: RefCell::new(file),
124        }))
125    }
126
127    fn flush_file(&self, file: &Entry<fs::File>) -> Result<(), Error> {
128        file.try_writer()?
129            .borrow_mut()
130            .flush()
131            .map_err(|_| Error::FlushFile)
132    }
133
134    fn file_len(&self, file: &Entry<fs::File>) -> Result<usize, Error> {
135        let fs_file = match file {
136            Entry::File(FileData { stream, .. }) => stream.borrow(),
137            Entry::Dir(_) => return Err(Error::FileAccess),
138        };
139        let file_meta = fs::File::metadata(&fs_file).map_err(|_| Error::FileLen)?;
140        file_meta.len().try_into().map_err(|_| Error::FileLen)
141    }
142
143    fn remove_file(&self, file: Entry<fs::File>) -> Result<(), Error> {
144        if let Entry::File(FileData { stream, .. }) = &file {
145            let mut stream = stream.borrow_mut();
146            stream.set_len(0).map_err(|_| Error::RemoveFile)?;
147            stream.flush().map_err(|_| Error::FlushFile)?;
148        }
149
150        fs::remove_file(file.path()).map_err(|_| Error::RemoveFile)
151    }
152
153    fn remove_dir_all(&self, file: Entry<fs::File>) -> Result<(), Error> {
154        if !file.is_dir() {
155            return Err(Error::RemoveDir);
156        }
157
158        fs::remove_dir_all(file.path()).map_err(|_| Error::RemoveDir)
159    }
160
161    fn read_dir(&self, file: &Entry<fs::File>) -> Result<Vec<Entry<fs::File>>, Error> {
162        if !file.is_dir() {
163            return Err(Error::FileAccess);
164        }
165
166        walkdir::WalkDir::new(file.path())
167            .into_iter()
168            .map(|res| {
169                res.map(|e| e.path().to_owned())
170                    .map_err(|_| Error::DirEntries)
171            })
172            .map(|path| path.and_then(|path| self.read_file(path)))
173            .collect()
174    }
175}
176
177#[cfg(test)]
178#[derive(Default)]
179pub struct InMemoryStorage {
180    pub files: RwLock<HashMap<PathBuf, IMFile>>,
181}
182
183#[cfg(test)]
184impl InMemoryStorage {
185    fn save_text_file<P: AsRef<Path>>(&self, path: P, content: &str) {
186        let buf = content.bytes().collect::<Vec<_>>();
187        self.save_file(
188            path,
189            IMFile::File(InMemoryFile {
190                len: buf.len(),
191                buf,
192            }),
193        );
194    }
195
196    fn save_file<P: AsRef<Path>>(&self, path: P, im_file: IMFile) {
197        self.mut_files().insert(path.as_ref().to_owned(), im_file);
198    }
199
200    pub(crate) fn files(&self) -> RwLockReadGuard<'_, HashMap<PathBuf, IMFile>> {
201        loop {
202            match self.files.try_read() {
203                Ok(files) => break files,
204                _ => thread::sleep(std::time::Duration::from_micros(100)),
205            }
206        }
207    }
208
209    pub(crate) fn mut_files(&self) -> RwLockWriteGuard<'_, HashMap<PathBuf, IMFile>> {
210        loop {
211            match self.files.try_write() {
212                Ok(files) => break files,
213                _ => thread::sleep(std::time::Duration::from_micros(100)),
214            }
215        }
216    }
217
218    // --------------------------------
219    // TEST DATA
220    // -------------------------------
221
222    pub(crate) fn add_hello_txt(&self) {
223        self.save_text_file("hello.txt", "hello world");
224    }
225
226    pub(crate) fn add_bar_foo_folder(&self) {
227        self.save_file("bar/", IMFile::Dir);
228        self.save_text_file("bar/hello.txt", "hello");
229        self.save_text_file("bar/world.txt", "world");
230        self.save_file("bar/foo/", IMFile::Dir);
231        self.save_text_file("bar/foo/hello.txt", "hello");
232        self.save_text_file("bar/foo/world.txt", "world");
233    }
234
235    pub(crate) fn add_bar_foo_folder_with_hidden(&self) {
236        self.save_file("bar/", IMFile::Dir);
237        self.save_text_file("bar/.hello.txt", "hello");
238        self.save_text_file("bar/world.txt", "world");
239        self.save_file("bar/.foo/", IMFile::Dir);
240        self.save_text_file("bar/.foo/hello.txt", "hello");
241        self.save_text_file("bar/.foo/world.txt", "world");
242    }
243}
244
245#[cfg(test)]
246impl Storage<io::Cursor<Vec<u8>>> for InMemoryStorage {
247    fn create_dir_all<P: AsRef<Path>>(&self, _path: P) -> Result<(), Error> {
248        todo!();
249    }
250
251    fn create_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<io::Cursor<Vec<u8>>>, Error> {
252        let file_path = path.as_ref().to_path_buf();
253
254        #[allow(clippy::significant_drop_in_scrutinee)]
255        let im_file = match self.files().get(&file_path) {
256            Some(_) => Err(Error::CreateFile),
257            None => Ok(IMFile::File(InMemoryFile::default())),
258        }?;
259
260        let cursor = io::Cursor::new(im_file.inner().buf.clone());
261
262        self.save_file(file_path.clone(), im_file);
263
264        Ok(Entry::File(FileData {
265            path: file_path,
266            stream: RefCell::new(cursor),
267        }))
268    }
269
270    fn read_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<io::Cursor<Vec<u8>>>, Error> {
271        let in_file = self
272            .files()
273            .get(path.as_ref())
274            .cloned()
275            .ok_or(Error::OpenFile(FileMode::Read))?;
276
277        let file_path = path.as_ref().to_path_buf();
278
279        match in_file {
280            IMFile::Dir => Ok(Entry::Dir(file_path)),
281            IMFile::File(f) => {
282                let cursor = io::Cursor::new(f.buf);
283                Ok(Entry::File(FileData {
284                    path: file_path,
285                    stream: RefCell::new(cursor),
286                }))
287            }
288        }
289    }
290
291    fn write_file<P: AsRef<Path>>(&self, path: P) -> Result<Entry<io::Cursor<Vec<u8>>>, Error> {
292        let file_path = path.as_ref().to_path_buf();
293
294        let file = self
295            .files()
296            .get(&file_path)
297            .cloned()
298            .ok_or(Error::OpenFile(FileMode::Write))?;
299        if matches!(file, IMFile::Dir) {
300            return Err(Error::FileAccess);
301        }
302
303        let cursor = io::Cursor::new(file.inner().buf.clone());
304
305        Ok(Entry::File(FileData {
306            path: file_path,
307            stream: RefCell::new(cursor),
308        }))
309    }
310
311    fn flush_file(&self, file: &Entry<io::Cursor<Vec<u8>>>) -> Result<(), Error> {
312        if file.is_dir() {
313            return Err(Error::FileAccess);
314        }
315
316        let file_path = file.path();
317        let writer = file.try_writer()?;
318        writer.borrow_mut().flush().map_err(|_| Error::FlushFile)?;
319
320        let vec = writer.borrow().get_ref().clone();
321        let len = vec.len();
322        let new_file = IMFile::File(InMemoryFile { buf: vec, len });
323
324        self.save_file(file_path, new_file);
325
326        Ok(())
327    }
328
329    fn file_len(&self, file: &Entry<io::Cursor<Vec<u8>>>) -> Result<usize, Error> {
330        let cur = match file {
331            Entry::File(FileData { stream, .. }) => stream.borrow(),
332            Entry::Dir(_) => return Err(Error::FileAccess),
333        };
334
335        Ok(cur.get_ref().len())
336    }
337
338    fn remove_file(&self, file: Entry<io::Cursor<Vec<u8>>>) -> Result<(), Error> {
339        self.mut_files()
340            .remove(file.path())
341            .ok_or(Error::RemoveFile)?;
342        Ok(())
343    }
344
345    fn remove_dir_all(&self, file: Entry<io::Cursor<Vec<u8>>>) -> Result<(), Error> {
346        if !file.is_dir() {
347            return Err(Error::FileAccess);
348        }
349
350        let file_path = file.path();
351
352        #[allow(clippy::needless_collect)] // 🚫 we have to collect to close read lock guard!
353        let file_paths = self
354            .files()
355            .keys()
356            .filter(|k| k.starts_with(file_path))
357            .cloned()
358            .collect::<Vec<_>>();
359
360        file_paths.into_iter().try_for_each(|k| {
361            self.mut_files()
362                .remove(&k)
363                .map(|_| ())
364                .ok_or(Error::RemoveDir)?;
365            Ok(())
366        })
367    }
368
369    fn read_dir(
370        &self,
371        file: &Entry<io::Cursor<Vec<u8>>>,
372    ) -> Result<Vec<Entry<io::Cursor<Vec<u8>>>>, Error> {
373        if !file.is_dir() {
374            return Err(Error::FileAccess);
375        }
376
377        let file_path = file.path();
378
379        self.files()
380            .iter()
381            .filter(|(k, _)| k.starts_with(file_path))
382            .map(|(k, _)| self.read_file(k))
383            .collect()
384    }
385}
386
387#[cfg(test)]
388#[derive(Debug, Clone, Default, PartialEq, Eq)]
389pub struct InMemoryFile {
390    pub buf: Vec<u8>,
391    pub len: usize,
392}
393
394#[cfg(test)]
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub enum IMFile {
397    File(InMemoryFile),
398    Dir,
399}
400
401#[cfg(test)]
402impl IMFile {
403    fn inner(&self) -> &InMemoryFile {
404        match self {
405            IMFile::File(inner) => inner,
406            IMFile::Dir => unreachable!(),
407        }
408    }
409}
410
411pub struct FileData<RW>
412where
413    RW: Read + Write + Seek,
414{
415    path: PathBuf,
416    stream: RefCell<RW>,
417}
418
419pub enum Entry<RW>
420where
421    RW: Read + Write + Seek,
422{
423    File(FileData<RW>),
424    Dir(PathBuf),
425}
426
427impl<RW> Entry<RW>
428where
429    RW: Read + Write + Seek,
430{
431    pub fn path(&self) -> &Path {
432        match self {
433            Entry::File(FileData { path, .. }) | Entry::Dir(path) => path,
434        }
435    }
436
437    pub fn is_dir(&self) -> bool {
438        matches!(self, Entry::Dir(_))
439    }
440
441    pub fn try_reader(&self) -> Result<&RefCell<RW>, Error> {
442        match self {
443            Entry::File(file) => Ok(&file.stream),
444            Entry::Dir(_) => Err(Error::FileAccess),
445        }
446    }
447
448    pub fn try_writer(&self) -> Result<&RefCell<RW>, Error> {
449        match self {
450            Entry::File(file) => Ok(&file.stream),
451            Entry::Dir(_) => Err(Error::FileAccess),
452        }
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459
460    fn sorted_file_names(file_names: &[PathBuf]) -> Vec<&str> {
461        let mut keys = file_names
462            .iter()
463            .map(|k| k.to_str().unwrap())
464            .collect::<Vec<_>>();
465        keys.sort_unstable();
466        keys
467    }
468
469    #[test]
470    fn should_create_a_new_file() {
471        let stor = InMemoryStorage::default();
472
473        match stor.create_file("hello.txt") {
474            Ok(file) => {
475                let im_file = stor.files().get(file.path()).cloned();
476                assert_eq!(im_file, Some(IMFile::File(InMemoryFile::default())));
477            }
478            _ => unreachable!(),
479        }
480    }
481
482    #[test]
483    fn should_throw_an_error_if_file_already_exist() {
484        let stor = InMemoryStorage::default();
485        stor.add_hello_txt();
486
487        match stor.create_file("hello.txt") {
488            Err(Error::CreateFile) => {}
489            _ => unreachable!(),
490        }
491    }
492
493    #[test]
494    fn should_not_open_file_to_read() {
495        let stor = InMemoryStorage::default();
496
497        match stor.read_file("hello.txt") {
498            Err(Error::OpenFile(FileMode::Read)) => {}
499            _ => unreachable!(),
500        }
501    }
502
503    #[test]
504    fn should_not_open_file_to_write() {
505        let stor = InMemoryStorage::default();
506
507        match stor.write_file("hello.txt") {
508            Err(Error::OpenFile(FileMode::Write)) => {}
509            _ => unreachable!(),
510        }
511    }
512
513    #[test]
514    fn should_open_exist_file_in_read_mode() {
515        let stor = InMemoryStorage::default();
516        stor.add_hello_txt();
517
518        match stor.read_file("hello.txt") {
519            Ok(file) => {
520                if let Some(IMFile::File(InMemoryFile { buf, len })) = stor.files().get(file.path())
521                {
522                    let content = b"hello world".to_vec();
523                    assert_eq!(len, &content.len());
524                    assert_eq!(buf, &content);
525                } else {
526                    unreachable!();
527                }
528            }
529            _ => unreachable!(),
530        }
531    }
532
533    #[test]
534    fn should_open_exist_file_in_write_mode() {
535        let stor = InMemoryStorage::default();
536        stor.add_hello_txt();
537
538        match stor.write_file("hello.txt") {
539            Ok(file) => {
540                if let Some(IMFile::File(InMemoryFile { buf, len })) = stor.files().get(file.path())
541                {
542                    let content = b"hello world".to_vec();
543                    assert_eq!(len, &content.len());
544                    assert_eq!(buf, &content);
545                } else {
546                    unreachable!();
547                }
548            }
549            _ => unreachable!(),
550        }
551    }
552
553    #[test]
554    fn should_write_content_to_file() {
555        let stor = InMemoryStorage::default();
556        let content = "hello world";
557
558        let file = stor.create_file("hello.txt").unwrap();
559        file.try_writer()
560            .unwrap()
561            .borrow_mut()
562            .write_all(content.as_bytes())
563            .unwrap();
564
565        match stor.flush_file(&file) {
566            Ok(_) => {
567                let im_file = stor.files().get(file.path()).cloned();
568                assert_eq!(
569                    im_file,
570                    Some(IMFile::File(InMemoryFile {
571                        buf: content.as_bytes().to_vec(),
572                        len: content.len()
573                    }))
574                );
575            }
576            _ => unreachable!(),
577        }
578    }
579
580    #[test]
581    fn should_remove_a_file_in_read_mode() {
582        let stor = InMemoryStorage::default();
583        stor.add_hello_txt();
584
585        let file = stor.write_file("hello.txt").unwrap();
586        let file_path = file.path().to_path_buf();
587
588        match stor.remove_file(file) {
589            Ok(_) => {
590                let im_file = stor.files().get(&file_path).cloned();
591                assert_eq!(im_file, None);
592            }
593            _ => unreachable!(),
594        }
595    }
596
597    #[test]
598    fn should_remove_a_file_in_write_mode() {
599        let stor = InMemoryStorage::default();
600        stor.add_hello_txt();
601
602        let file = stor.write_file("hello.txt").unwrap();
603        let file_path = file.path().to_path_buf();
604
605        match stor.remove_file(file) {
606            Ok(_) => {
607                let im_file = stor.files().get(&file_path).cloned();
608                assert_eq!(im_file, None);
609            }
610            _ => unreachable!(),
611        }
612    }
613
614    #[test]
615    fn should_get_file_length() {
616        let stor = InMemoryStorage::default();
617        stor.add_hello_txt();
618
619        let file = stor.read_file("hello.txt").unwrap();
620
621        match stor.file_len(&file) {
622            Ok(len) => {
623                let content = b"hello world".to_vec();
624                assert_eq!(len, content.len());
625            }
626            _ => unreachable!(),
627        }
628    }
629
630    #[test]
631    fn should_open_dir() {
632        let stor = InMemoryStorage::default();
633        stor.add_bar_foo_folder();
634
635        match stor.read_file("bar/foo/") {
636            Ok(Entry::Dir(path)) => assert_eq!(path, PathBuf::from("bar/foo/")),
637            _ => unreachable!(),
638        }
639    }
640
641    #[test]
642    fn should_remove_dir_with_subfiles() {
643        let stor = InMemoryStorage::default();
644        stor.add_hello_txt();
645        stor.add_bar_foo_folder();
646
647        let file = stor.read_file("bar/foo/").unwrap();
648        let file_path = file.path().to_path_buf();
649
650        match stor.remove_dir_all(file) {
651            Ok(()) => {
652                assert_eq!(stor.files().get(&file_path).cloned(), None);
653                let files = stor.files();
654                let keys = files.keys().cloned().collect::<Vec<_>>();
655                assert_eq!(
656                    sorted_file_names(&keys),
657                    vec!["bar/", "bar/hello.txt", "bar/world.txt", "hello.txt"]
658                );
659            }
660            _ => unreachable!(),
661        }
662    }
663
664    #[test]
665    fn should_remove_dir_recursively_with_subfiles() {
666        let stor = InMemoryStorage::default();
667        stor.add_hello_txt();
668        stor.add_bar_foo_folder();
669
670        let file = stor.read_file("bar/").unwrap();
671        let file_path = file.path().to_path_buf();
672
673        match stor.remove_dir_all(file) {
674            Ok(()) => {
675                assert_eq!(stor.files().get(&file_path).cloned(), None);
676                let files = stor.files();
677                let keys = files.keys().cloned().collect::<Vec<PathBuf>>();
678                assert_eq!(sorted_file_names(&keys), vec!["hello.txt"]);
679            }
680            _ => unreachable!(),
681        }
682    }
683
684    #[test]
685    fn should_return_file_names_of_dir_subfiles() {
686        let stor = InMemoryStorage::default();
687        stor.add_hello_txt();
688        stor.add_bar_foo_folder();
689
690        let file = stor.read_file("bar/").unwrap();
691
692        match stor.read_dir(&file) {
693            Ok(files) => {
694                let file_names = files
695                    .iter()
696                    .map(|f| f.path().to_path_buf())
697                    .collect::<Vec<_>>();
698                assert_eq!(
699                    sorted_file_names(&file_names),
700                    vec![
701                        "bar/",
702                        "bar/foo/",
703                        "bar/foo/hello.txt",
704                        "bar/foo/world.txt",
705                        "bar/hello.txt",
706                        "bar/world.txt",
707                    ]
708                );
709            }
710            _ => unreachable!(),
711        }
712    }
713
714    #[test]
715    fn should_include_hidden_files_names() {
716        let stor = InMemoryStorage::default();
717        stor.add_hello_txt();
718        stor.add_bar_foo_folder_with_hidden();
719
720        let file = stor.read_file("bar/").unwrap();
721
722        match stor.read_dir(&file) {
723            Ok(files) => {
724                let file_names = files
725                    .into_iter()
726                    .map(|f| f.path().to_path_buf())
727                    .collect::<Vec<_>>();
728                assert_eq!(
729                    sorted_file_names(&file_names),
730                    vec![
731                        "bar/",
732                        "bar/.foo/",
733                        "bar/.foo/hello.txt",
734                        "bar/.foo/world.txt",
735                        "bar/.hello.txt",
736                        "bar/world.txt",
737                    ]
738                );
739            }
740            _ => unreachable!(),
741        }
742    }
743}