1mod dir;
2mod file;
3
4pub use dir::LocalDirEntry;
5pub use file::{
6 LocalFile, LocalFileError, LocalFileHandle, LocalFilePermissions,
7};
8
9use std::collections::{hash_map::Entry, HashMap};
10use std::io;
11use std::path::{Path, PathBuf};
12
13#[derive(Debug)]
14pub struct FileSystemManager {
15 files: HashMap<u32, LocalFile>,
16}
17
18impl Default for FileSystemManager {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl FileSystemManager {
25 pub fn new() -> Self {
26 Self {
27 files: HashMap::new(),
28 }
29 }
30
31 pub async fn create_dir(
33 &self,
34 path: impl AsRef<Path>,
35 create_components: bool,
36 ) -> io::Result<()> {
37 let path = clean_path(path.as_ref()).await;
38 dir::create(path, create_components).await
39 }
40
41 pub async fn rename_dir(
45 &mut self,
46 from: impl AsRef<Path>,
47 to: impl AsRef<Path>,
48 ) -> io::Result<()> {
49 let from = clean_path(from.as_ref()).await;
50 let to = clean_path(to.as_ref()).await;
51
52 self.check_no_open_files(from.as_path())?;
53
54 dir::rename(from.as_path(), to.as_path()).await?;
56
57 Ok(())
58 }
59
60 pub async fn remove_dir(
63 &mut self,
64 path: impl AsRef<Path>,
65 non_empty: bool,
66 ) -> io::Result<()> {
67 let path = clean_path(path.as_ref()).await;
68
69 self.check_no_open_files(path.as_path())?;
70
71 dir::remove(path, non_empty).await
73 }
74
75 pub async fn dir_entries(
84 &self,
85 path: impl AsRef<Path>,
86 ) -> io::Result<Vec<LocalDirEntry>> {
87 let path = clean_path(path.as_ref()).await;
88
89 dir::entries(path).await
90 }
91
92 pub async fn open_file(
101 &mut self,
102 path: impl AsRef<Path>,
103 create: bool,
104 write: bool,
105 read: bool,
106 ) -> io::Result<LocalFileHandle> {
107 let path = clean_path(path.as_ref()).await;
108
109 let mut new_permissions = LocalFilePermissions { read, write };
110 let mut maybe_id_and_sig = None;
111
112 let search =
115 self.files.values_mut().find(|f| f.path() == path.as_path());
116
117 if let Some(file) = search {
121 let id = file.id();
122 let sig = file.sig();
123 let permissions = file.permissions();
124
125 if (permissions.read || !read) && (permissions.write || !write) {
128 return Ok(file.handle());
129 } else {
130 new_permissions.read = permissions.read || read;
134 new_permissions.write = permissions.write || write;
135 maybe_id_and_sig = Some((id, sig));
136 }
137 }
138
139 let mut new_file = LocalFile::open(
141 path,
142 create,
143 new_permissions.write,
144 new_permissions.read,
145 )
146 .await?;
147
148 if let Some((id, sig)) = maybe_id_and_sig {
151 new_file.id = id;
152 new_file.sig = sig;
153 }
154
155 let handle = new_file.handle();
158 self.files.insert(new_file.id(), new_file);
159
160 Ok(handle)
161 }
162
163 pub fn close_file(
168 &mut self,
169 handle: LocalFileHandle,
170 ) -> io::Result<LocalFile> {
171 match self.files.entry(handle.id) {
172 Entry::Occupied(x) if x.get().sig == handle.sig => Ok(x.remove()),
173 Entry::Occupied(_) => Err(io::Error::new(
174 io::ErrorKind::InvalidInput,
175 format!("Signature invalid for file with id {}", handle.id),
176 )),
177 Entry::Vacant(_) => Err(io::Error::new(
178 io::ErrorKind::NotFound,
179 format!("No open file with id {}", handle.id),
180 )),
181 }
182 }
183
184 pub async fn rename_file(
188 &mut self,
189 from: impl AsRef<Path>,
190 to: impl AsRef<Path>,
191 ) -> io::Result<()> {
192 let from = clean_path(from.as_ref()).await;
193 let to = clean_path(to.as_ref()).await;
194
195 self.check_no_open_files(from.as_path())?;
196
197 file::rename(from.as_path(), to.as_path()).await
198 }
199
200 pub async fn remove_file(
202 &mut self,
203 path: impl AsRef<Path>,
204 ) -> io::Result<()> {
205 let path = clean_path(path.as_ref()).await;
206
207 self.check_no_open_files(path.as_path())?;
208
209 file::remove(path).await
210 }
211
212 pub fn file_cnt(&self) -> usize {
214 self.files.len()
215 }
216
217 pub fn get_mut(&mut self, id: impl Into<u32>) -> Option<&mut LocalFile> {
219 match self.files.get_mut(&id.into()) {
220 Some(file) => Some(file),
221 None => None,
222 }
223 }
224
225 pub fn get(&self, id: impl Into<u32>) -> Option<&LocalFile> {
227 match self.files.get(&id.into()) {
228 Some(file) => Some(file),
229 None => None,
230 }
231 }
232
233 pub fn exists(&self, id: impl Into<u32>) -> bool {
235 self.get(id).is_some()
236 }
237
238 fn check_no_open_files(&self, path: impl AsRef<Path>) -> io::Result<()> {
241 for f in self.files.values() {
242 if f.path().starts_with(path.as_ref()) {
243 return Err(io::Error::new(
244 io::ErrorKind::InvalidData,
245 format!(
246 "File at {:?} is open and must be closed",
247 f.path()
248 ),
249 ));
250 }
251 }
252
253 Ok(())
254 }
255}
256
257async fn clean_path(path: impl AsRef<Path>) -> PathBuf {
260 tokio::fs::canonicalize(path.as_ref())
261 .await
262 .ok()
263 .unwrap_or_else(|| path.as_ref().to_path_buf())
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use tokio::fs;
270
271 #[tokio::test]
272 async fn create_dir_should_yield_error_if_parent_dirs_missing_and_flag_not_set(
273 ) {
274 let root = tempfile::tempdir().unwrap();
275 let fsm = FileSystemManager::new();
276
277 let result =
278 fsm.create_dir(root.as_ref().join("some/dir"), false).await;
279 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
280 }
281
282 #[tokio::test]
283 async fn create_dir_should_return_success_if_created_the_path() {
284 let root = tempfile::tempdir().unwrap();
285 let fsm = FileSystemManager::new();
286
287 let path = root.as_ref().join("test-dir");
288 let result = fsm.create_dir(path.as_path(), false).await;
289 assert!(
290 result.is_ok(),
291 "Unexpectedly failed to create dir: {:?}",
292 result
293 );
294 assert!(fs::metadata(path).await.is_ok(), "Directory missing");
295
296 let path = root.as_ref().join("some/test-dir");
297 let result = fsm.create_dir(path.as_path(), true).await;
298 assert!(
299 result.is_ok(),
300 "Unexpectedly failed to create nested dir: {:?}",
301 result
302 );
303 assert!(fs::metadata(path).await.is_ok(), "Directory missing");
304 }
305
306 #[tokio::test]
307 async fn rename_dir_should_yield_error_if_origin_path_does_not_exist() {
308 let root = tempfile::tempdir().unwrap();
309 let mut fsm = FileSystemManager::new();
310
311 let origin = root.as_ref().join("origin");
312 let destination = root.as_ref().join("destination");
313
314 match fsm.rename_dir(origin, destination).await {
315 Err(x) => assert_eq!(x.kind(), io::ErrorKind::NotFound),
316 x => panic!("Unexpected result: {:?}", x),
317 }
318 }
319
320 #[tokio::test]
321 async fn rename_dir_should_yield_error_if_origin_path_is_not_a_directory() {
322 let root = tempfile::tempdir().unwrap();
323 let mut fsm = FileSystemManager::new();
324
325 let origin_file =
327 tempfile::NamedTempFile::new_in(root.as_ref()).unwrap();
328
329 let destination = root.as_ref().join("destination");
330
331 match fsm.rename_dir(origin_file.as_ref(), destination).await {
332 Err(_) => (),
333 x => panic!("Unexpected result: {:?}", x),
334 }
335 }
336
337 #[tokio::test]
338 async fn rename_dir_should_yield_error_if_contains_open_files() {
339 let root = tempfile::tempdir().unwrap();
340 let mut fsm = FileSystemManager::new();
341
342 let origin = root.as_ref().join("origin");
343 fs::create_dir(origin.as_path()).await.unwrap();
344
345 let _file1 = fsm
347 .open_file(origin.as_path().join("file1"), true, true, true)
348 .await
349 .unwrap();
350
351 let destination = root.as_ref().join("destination");
352
353 match fsm
354 .rename_dir(origin.as_path(), destination.as_path())
355 .await
356 {
357 Err(x) => assert_eq!(x.kind(), io::ErrorKind::InvalidData),
358 x => panic!("Unexpected result: {:?}", x),
359 }
360 }
361
362 #[tokio::test]
363 async fn rename_dir_should_return_success_if_renamed_directory() {
364 let root = tempfile::tempdir().unwrap();
365 let mut fsm = FileSystemManager::new();
366
367 let origin = root.as_ref().join("origin");
368 fs::create_dir(origin.as_path()).await.unwrap();
369
370 let destination = root.as_ref().join("destination");
371
372 match fsm.rename_dir(origin, destination).await {
373 Ok(_) => (),
374 x => panic!("Unexpected result: {:?}", x),
375 }
376 }
377
378 #[tokio::test]
379 async fn dir_entries_should_yield_error_if_path_not_a_directory() {
380 let root = tempfile::tempdir().unwrap();
381 let fsm = FileSystemManager::new();
382
383 let file = tempfile::NamedTempFile::new_in(root.as_ref()).unwrap();
384
385 match fsm.dir_entries(file.path()).await {
386 Err(x) if x.kind() == io::ErrorKind::Other => (),
387 x => panic!("Unexpected result: {:?}", x),
388 }
389 }
390
391 #[tokio::test]
392 async fn dir_entries_should_return_a_list_of_immediate_entries_in_a_directory(
393 ) {
394 let root = tempfile::tempdir().unwrap();
395 let fsm = FileSystemManager::new();
396
397 let file = tempfile::NamedTempFile::new_in(root.as_ref()).unwrap();
398 let dir = tempfile::tempdir_in(root.as_ref()).unwrap();
399 let inner_file = tempfile::NamedTempFile::new_in(dir.as_ref()).unwrap();
400
401 match fsm.dir_entries(root.as_ref()).await {
402 Ok(entries) => {
403 assert_eq!(
404 entries.len(),
405 2,
406 "Unexpected entry count: {}",
407 entries.len()
408 );
409 assert!(
410 entries.contains(&LocalDirEntry {
411 path: clean_path(file.as_ref()).await,
412 is_file: true,
413 is_dir: false,
414 is_symlink: false,
415 }),
416 "Missing file"
417 );
418 assert!(
419 entries.contains(&LocalDirEntry {
420 path: clean_path(dir.as_ref()).await,
421 is_file: false,
422 is_dir: true,
423 is_symlink: false,
424 }),
425 "Missing dir"
426 );
427 assert!(
428 !entries.contains(&LocalDirEntry {
429 path: clean_path(inner_file.as_ref()).await,
430 is_file: true,
431 is_dir: false,
432 is_symlink: false,
433 }),
434 "Unexpectedly found nested file"
435 );
436 }
437 x => panic!("Unexpected result: {:?}", x),
438 }
439 }
440
441 #[tokio::test]
442 async fn remove_dir_should_yield_error_if_directory_not_empty_and_flag_not_set(
443 ) {
444 let root = tempfile::tempdir().unwrap();
445 let mut fsm = FileSystemManager::new();
446
447 let _file = tempfile::NamedTempFile::new_in(root.as_ref()).unwrap();
449
450 match fsm.remove_dir(root.as_ref(), false).await {
451 Err(x) if x.kind() == io::ErrorKind::Other => (),
452 x => panic!("Unexpected result: {:?}", x),
453 }
454 }
455
456 #[tokio::test]
457 async fn remove_dir_should_yield_error_if_open_files_exist_in_directory() {
458 let root = tempfile::tempdir().unwrap();
459 let mut fsm = FileSystemManager::new();
460
461 fsm.open_file(root.as_ref().join("test-file"), true, true, true)
462 .await
463 .expect("Failed to open file with manager");
464
465 match fsm.remove_dir(root.as_ref(), true).await {
468 Err(x) if x.kind() == io::ErrorKind::InvalidData => (),
469 x => panic!("Unexpected result: {:?}", x),
470 }
471 }
472
473 #[tokio::test]
474 async fn remove_dir_should_return_success_if_removed_directory() {
475 let root = tempfile::tempdir().unwrap();
476 let mut fsm = FileSystemManager::new();
477
478 let _ = tempfile::tempfile_in(root.as_ref()).unwrap();
479
480 match fsm.remove_dir(root.as_ref(), true).await {
481 Ok(_) => (),
482 x => panic!("Unexpected result: {:?}", x),
483 }
484 }
485
486 #[tokio::test]
487 async fn open_file_should_yield_error_if_underlying_open_fails() {
488 let root = tempfile::tempdir().unwrap();
489 let mut fsm = FileSystemManager::new();
490
491 let not_a_file = tempfile::tempdir_in(root.as_ref()).unwrap();
492
493 match fsm.open_file(not_a_file.as_ref(), true, true, true).await {
494 Err(x) if x.kind() == io::ErrorKind::Other => (),
495 x => panic!("Unexpected result: {:?}", x),
496 }
497 }
498
499 #[tokio::test]
500 async fn open_file_should_return_existing_open_file_if_permissions_allow() {
501 let root = tempfile::tempdir().unwrap();
502 let mut fsm = FileSystemManager::new();
503
504 let handle = fsm
506 .open_file(root.as_ref().join("test-file"), true, true, true)
507 .await
508 .expect("Failed to create file");
509
510 assert_eq!(
511 fsm.file_cnt(),
512 1,
513 "Unexpected number of open files: {}",
514 fsm.file_cnt()
515 );
516 assert_eq!(
517 fsm.get(handle).map(|f| f.permissions()),
518 Some(LocalFilePermissions {
519 read: true,
520 write: true
521 })
522 );
523
524 let handle_2 = fsm
526 .open_file(
527 root.as_ref().join(".").join("test-file"),
528 false,
529 false,
530 true,
531 )
532 .await
533 .expect("Failed to open file for read");
534
535 assert_eq!(
536 fsm.file_cnt(),
537 1,
538 "Unexpected number of open files: {}",
539 fsm.file_cnt()
540 );
541
542 assert_eq!(handle, handle_2);
543
544 assert_eq!(
545 fsm.get(handle_2).map(|f| f.permissions()),
546 Some(LocalFilePermissions {
547 read: true,
548 write: true
549 })
550 );
551
552 let handle_3 = fsm
554 .open_file(root.as_ref().join("test-file"), false, true, false)
555 .await
556 .expect("Failed to open file for write");
557
558 assert_eq!(
559 fsm.file_cnt(),
560 1,
561 "Unexpected number of open files: {}",
562 fsm.file_cnt()
563 );
564
565 assert_eq!(handle, handle_3);
566
567 assert_eq!(
568 fsm.get(handle_3).map(|f| f.permissions()),
569 Some(LocalFilePermissions {
570 read: true,
571 write: true
572 })
573 );
574 }
575
576 #[tokio::test]
577 async fn open_file_should_reopen_an_open_file_if_permissions_need_merging()
578 {
579 let root = tempfile::tempdir().unwrap();
580 let mut fsm = FileSystemManager::new();
581
582 let handle = fsm
584 .open_file(root.as_ref().join("test-file"), true, true, false)
585 .await
586 .expect("Failed to create file");
587
588 assert_eq!(
589 fsm.file_cnt(),
590 1,
591 "Unexpected number of open files: {}",
592 fsm.file_cnt()
593 );
594
595 assert_eq!(
596 fsm.get(handle).map(|f| f.permissions()),
597 Some(LocalFilePermissions {
598 read: false,
599 write: true
600 })
601 );
602
603 let handle_2 = fsm
605 .open_file(root.as_ref().join("test-file"), false, false, true)
606 .await
607 .expect("Failed to open file");
608
609 assert_eq!(
610 fsm.file_cnt(),
611 1,
612 "Unexpected number of open files: {}",
613 fsm.file_cnt()
614 );
615
616 assert_eq!(handle, handle_2);
617
618 assert_eq!(
619 fsm.get(handle_2).map(|f| f.permissions()),
620 Some(LocalFilePermissions {
621 read: true,
622 write: true
623 })
624 );
625 }
626
627 #[tokio::test]
628 async fn open_file_should_return_a_newly_opened_file_if_none_already_open()
629 {
630 let root = tempfile::tempdir().unwrap();
631 let mut fsm = FileSystemManager::new();
632
633 let handle = fsm
634 .open_file(root.as_ref().join("test-file-1"), true, true, true)
635 .await
636 .expect("Failed to create file 1");
637
638 let handle_2 = fsm
639 .open_file(root.as_ref().join("test-file-2"), true, true, true)
640 .await
641 .expect("Failed to create file 2");
642
643 assert_eq!(
644 fsm.file_cnt(),
645 2,
646 "Unexpected number of open files: {}",
647 fsm.file_cnt()
648 );
649
650 assert_ne!(handle, handle_2, "Two open files have same handle");
651 }
652
653 #[tokio::test]
654 async fn close_file_should_yield_error_if_no_file_open_with_id() {
655 let root = tempfile::tempdir().unwrap();
656 let mut fsm = FileSystemManager::new();
657
658 let handle = fsm
659 .open_file(root.as_ref().join("test-file"), true, true, true)
660 .await
661 .expect("Failed to create file");
662
663 match fsm.close_file(LocalFileHandle {
664 id: handle.id + 1,
665 sig: handle.sig,
666 }) {
667 Err(x) if x.kind() == io::ErrorKind::NotFound => (),
668 x => panic!("Unexpected result: {:?}", x),
669 }
670 }
671
672 #[tokio::test]
673 async fn close_file_should_yield_error_if_file_has_different_signature() {
674 let root = tempfile::tempdir().unwrap();
675 let mut fsm = FileSystemManager::new();
676
677 let handle = fsm
678 .open_file(root.as_ref().join("test-file"), true, true, true)
679 .await
680 .expect("Failed to create file");
681
682 match fsm.close_file(LocalFileHandle {
683 id: handle.id,
684 sig: handle.sig + 1,
685 }) {
686 Err(x) if x.kind() == io::ErrorKind::InvalidInput => (),
687 x => panic!("Unexpected result: {:?}", x),
688 }
689 }
690
691 #[tokio::test]
692 async fn close_fould_should_remove_file_from_manager_if_successful() {
693 let root = tempfile::tempdir().unwrap();
694 let mut fsm = FileSystemManager::new();
695
696 let handle = fsm
697 .open_file(root.as_ref().join("test-file"), true, true, true)
698 .await
699 .expect("Failed to create file");
700
701 match fsm.close_file(handle) {
702 Ok(_) => (),
703 x => panic!("Unexpected result: {:?}", x),
704 }
705 }
706
707 #[tokio::test]
708 async fn rename_file_should_yield_error_if_origin_path_does_not_exist() {
709 let root = tempfile::tempdir().unwrap();
710 let mut fsm = FileSystemManager::new();
711
712 let origin = root.as_ref().join("origin");
713 let destination = root.as_ref().join("destination");
714
715 match fsm.rename_file(origin, destination).await {
716 Err(x) => assert_eq!(x.kind(), io::ErrorKind::NotFound),
717 x => panic!("Unexpected result: {:?}", x),
718 }
719 }
720
721 #[tokio::test]
722 async fn rename_file_should_yield_error_if_origin_path_is_not_a_file() {
723 let root = tempfile::tempdir().unwrap();
724 let mut fsm = FileSystemManager::new();
725
726 let origin_dir = tempfile::tempdir_in(root.as_ref()).unwrap();
728
729 let destination = root.as_ref().join("destination");
730
731 match fsm.rename_file(origin_dir.as_ref(), destination).await {
732 Err(x) => assert_eq!(x.kind(), io::ErrorKind::Other),
733 x => panic!("Unexpected result: {:?}", x),
734 }
735 }
736
737 #[tokio::test]
738 async fn rename_file_should_yield_error_if_file_is_open() {
739 let root = tempfile::tempdir().unwrap();
740 let mut fsm = FileSystemManager::new();
741
742 let origin = root.as_ref().join("file");
743 let _file = fsm
744 .open_file(origin.as_path(), true, true, true)
745 .await
746 .unwrap();
747
748 let destination = root.as_ref().join("destination");
749
750 match fsm
751 .rename_file(origin.as_path(), destination.as_path())
752 .await
753 {
754 Err(x) => assert_eq!(x.kind(), io::ErrorKind::InvalidData),
755 x => panic!("Unexpected result: {:?}", x),
756 }
757 }
758
759 #[tokio::test]
760 async fn rename_file_should_return_success_if_renamed_file() {
761 let root = tempfile::tempdir().unwrap();
762 let mut fsm = FileSystemManager::new();
763
764 let origin = tempfile::NamedTempFile::new_in(root.as_ref()).unwrap();
765
766 let destination = root.as_ref().join("destination");
767
768 match fsm.rename_file(origin, destination).await {
769 Ok(_) => (),
770 x => panic!("Unexpected result: {:?}", x),
771 }
772 }
773
774 #[tokio::test]
775 async fn remove_file_should_yield_error_if_file_open() {
776 let root = tempfile::tempdir().unwrap();
777 let mut fsm = FileSystemManager::new();
778
779 let path = root.as_ref().join("test-file");
780
781 fsm.open_file(path.as_path(), true, true, true)
782 .await
783 .expect("Failed to open file with manager");
784
785 match fsm.remove_file(path.as_path()).await {
788 Err(x) if x.kind() == io::ErrorKind::InvalidData => (),
789 x => panic!("Unexpected result: {:?}", x),
790 }
791 }
792
793 #[tokio::test]
794 async fn remove_file_should_return_success_if_removed_file() {
795 let root = tempfile::tempdir().unwrap();
796 let mut fsm = FileSystemManager::new();
797
798 let file = tempfile::NamedTempFile::new_in(root.as_ref()).unwrap();
799
800 match fsm.remove_file(file.as_ref()).await {
801 Ok(_) => (),
802 x => panic!("Unexpected result: {:?}", x),
803 }
804 }
805}