turtle_build/infrastructure/
file_system.rs1#[cfg(test)]
2mod fake;
3mod metadata;
4
5#[cfg(test)]
6pub use self::fake::FakeFileSystem;
7use async_trait::async_trait;
8use core::{error::Error, fmt::Debug};
9use metadata::Metadata;
10use std::{
11 io,
12 path::{Path, PathBuf},
13};
14use tokio::{
15 fs::{self, File},
16 io::AsyncReadExt,
17 sync::Semaphore,
18};
19
20#[async_trait]
21pub trait FileSystem {
22 async fn read_file(&self, path: &Path, buffer: &mut Vec<u8>) -> Result<(), Box<dyn Error>>;
23 async fn read_file_to_string(
24 &self,
25 path: &Path,
26 buffer: &mut String,
27 ) -> Result<(), Box<dyn Error>>;
28 async fn exists(&self, path: &Path) -> Result<bool, Box<dyn Error>>;
29 async fn metadata(&self, path: &Path) -> Result<Metadata, Box<dyn Error>>;
30 async fn create_directory(&self, path: &Path) -> Result<(), Box<dyn Error>>;
31 async fn canonicalize_path(&self, path: &Path) -> Result<PathBuf, Box<dyn Error>>;
32 async fn remove_file(&self, path: &Path) -> Result<(), Box<dyn Error>>;
33}
34
35#[derive(Debug)]
37pub struct OsFileSystem {
38 semaphore: Semaphore,
39}
40
41impl OsFileSystem {
42 pub fn new(open_file_limit: usize) -> Self {
44 Self {
45 semaphore: Semaphore::new(open_file_limit.min(Semaphore::MAX_PERMITS)),
46 }
47 }
48
49 async fn read_file(&self, path: &Path, buffer: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
50 File::open(path)
51 .await
52 .map_err(|error| Self::error(error, path))?
53 .read_to_end(buffer)
54 .await
55 .map_err(|error| Self::error(error, path))?;
56
57 Ok(())
58 }
59
60 async fn read_file_to_string(
61 &self,
62 path: &Path,
63 buffer: &mut String,
64 ) -> Result<(), Box<dyn Error>> {
65 File::open(path)
66 .await
67 .map_err(|error| Self::error(error, path))?
68 .read_to_string(buffer)
69 .await
70 .map_err(|error| Self::error(error, path))?;
71
72 Ok(())
73 }
74
75 fn error(error: io::Error, path: &Path) -> String {
76 format!("{}: {}", error, path.display())
77 }
78}
79
80#[async_trait]
81impl FileSystem for OsFileSystem {
82 async fn read_file(&self, path: &Path, buffer: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
83 let _permit = self.semaphore.acquire().await?;
84
85 self.read_file(path, buffer).await
86 }
87
88 async fn read_file_to_string(
89 &self,
90 path: &Path,
91 buffer: &mut String,
92 ) -> Result<(), Box<dyn Error>> {
93 let _permit = self.semaphore.acquire().await?;
94
95 self.read_file_to_string(path, buffer).await
96 }
97
98 async fn exists(&self, path: &Path) -> Result<bool, Box<dyn Error>> {
99 Ok(fs::try_exists(path)
100 .await
101 .map_err(|error| Self::error(error, path))?)
102 }
103
104 async fn metadata(&self, path: &Path) -> Result<Metadata, Box<dyn Error>> {
105 Ok(fs::metadata(path)
106 .await
107 .map_err(|error| Self::error(error, path))?
108 .try_into()?)
109 }
110
111 async fn create_directory(&self, path: &Path) -> Result<(), Box<dyn Error>> {
112 fs::create_dir_all(path)
113 .await
114 .map_err(|error| Self::error(error, path))?;
115
116 Ok(())
117 }
118
119 async fn canonicalize_path(&self, path: &Path) -> Result<PathBuf, Box<dyn Error>> {
120 Ok(fs::canonicalize(path)
121 .await
122 .map_err(|error| Self::error(error, path))?)
123 }
124
125 async fn remove_file(&self, path: &Path) -> Result<(), Box<dyn Error>> {
126 fs::remove_file(path)
127 .await
128 .map_err(|error| Self::error(error, path))?;
129
130 Ok(())
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn limit_open_files() {
140 assert_eq!(OsFileSystem::new(42).semaphore.available_permits(), 42);
141 }
142
143 #[test]
144 fn limit_open_files_to_maximum_permits() {
145 assert_eq!(
146 OsFileSystem::new(usize::MAX).semaphore.available_permits(),
147 Semaphore::MAX_PERMITS
148 );
149 }
150}