turtle_build/infrastructure/file_system/
os.rs1use super::Metadata;
2use crate::infrastructure::{FileError, FileSystem};
3use async_trait::async_trait;
4use std::{
5 fs::{exists, metadata},
6 io,
7 path::{Path, PathBuf},
8};
9use tokio::{
10 fs::{canonicalize, create_dir_all, read, read_to_string, remove_file},
11 sync::Semaphore,
12};
13
14#[derive(Debug)]
16pub struct OsFileSystem {
17 semaphore: Semaphore,
18}
19
20impl OsFileSystem {
21 pub fn new(open_file_limit: usize) -> Self {
23 Self {
24 semaphore: Semaphore::new(open_file_limit.min(Semaphore::MAX_PERMITS)),
25 }
26 }
27
28 fn error(error: io::Error, path: &Path) -> FileError {
29 FileError::new(format!("{}: {}", error, path.display()))
30 }
31}
32
33#[async_trait]
34impl FileSystem for OsFileSystem {
35 async fn read_file(&self, path: &Path) -> Result<Vec<u8>, FileError> {
36 let _permit = self.semaphore.acquire().await?;
37
38 read(path).await.map_err(|error| Self::error(error, path))
39 }
40
41 async fn read_file_to_string(&self, path: &Path) -> Result<String, FileError> {
42 let _permit = self.semaphore.acquire().await?;
43
44 read_to_string(path)
45 .await
46 .map_err(|error| Self::error(error, path))
47 }
48
49 async fn exists(&self, path: &Path) -> Result<bool, FileError> {
52 exists(path).map_err(|error| Self::error(error, path))
53 }
54
55 async fn metadata(&self, path: &Path) -> Result<Option<Metadata>, FileError> {
57 match metadata(path) {
58 Ok(metadata) => Ok(Some(metadata.try_into()?)),
59 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
60 Err(error) => Err(Self::error(error, path)),
61 }
62 }
63
64 async fn create_directory(&self, path: &Path) -> Result<(), FileError> {
65 create_dir_all(path)
66 .await
67 .map_err(|error| Self::error(error, path))?;
68
69 Ok(())
70 }
71
72 async fn canonicalize_path(&self, path: &Path) -> Result<PathBuf, FileError> {
73 canonicalize(path)
74 .await
75 .map_err(|error| Self::error(error, path))
76 }
77
78 async fn remove_file(&self, path: &Path) -> Result<(), FileError> {
79 remove_file(path)
80 .await
81 .map_err(|error| Self::error(error, path))?;
82
83 Ok(())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use pretty_assertions::assert_eq;
91 use std::fs::{metadata, write};
92 use tempfile::tempdir;
93
94 #[test]
95 fn limit_open_files() {
96 assert_eq!(OsFileSystem::new(42).semaphore.available_permits(), 42);
97 }
98
99 #[test]
100 fn limit_open_files_to_maximum_permits() {
101 assert_eq!(
102 OsFileSystem::new(usize::MAX).semaphore.available_permits(),
103 Semaphore::MAX_PERMITS
104 );
105 }
106
107 #[tokio::test]
108 async fn read_file() {
109 let directory = tempdir().unwrap();
110 let path = directory.path().join("foo");
111
112 write(&path, "bar").unwrap();
113
114 assert_eq!(OsFileSystem::new(1).read_file(&path).await.unwrap(), b"bar");
115 }
116
117 #[tokio::test]
118 async fn fail_to_read_missing_file() {
119 let directory = tempdir().unwrap();
120 let path = directory.path().join("foo");
121
122 assert!(
123 OsFileSystem::new(1)
124 .read_file(&path)
125 .await
126 .unwrap_err()
127 .to_string()
128 .contains(&path.display().to_string())
129 );
130 }
131
132 #[tokio::test]
133 async fn read_file_to_string() {
134 let directory = tempdir().unwrap();
135 let path = directory.path().join("foo");
136
137 write(&path, "bar").unwrap();
138
139 assert_eq!(
140 OsFileSystem::new(1)
141 .read_file_to_string(&path)
142 .await
143 .unwrap(),
144 "bar"
145 );
146 }
147
148 #[tokio::test]
149 async fn fail_to_read_missing_file_to_string() {
150 let directory = tempdir().unwrap();
151 let path = directory.path().join("foo");
152
153 assert!(
154 OsFileSystem::new(1)
155 .read_file_to_string(&path)
156 .await
157 .unwrap_err()
158 .to_string()
159 .contains(&path.display().to_string())
160 );
161 }
162
163 #[tokio::test]
164 async fn check_file_existence() {
165 let directory = tempdir().unwrap();
166 let path = directory.path().join("foo");
167 let file_system = OsFileSystem::new(1);
168
169 assert!(!file_system.exists(&path).await.unwrap());
170
171 write(&path, "").unwrap();
172
173 assert!(file_system.exists(&path).await.unwrap());
174 }
175
176 #[tokio::test]
177 async fn get_metadata_of_file() {
178 let directory = tempdir().unwrap();
179 let path = directory.path().join("foo");
180
181 write(&path, "").unwrap();
182
183 assert_eq!(
184 OsFileSystem::new(1).metadata(&path).await.unwrap(),
185 Some(Metadata::new(
186 metadata(&path).unwrap().modified().unwrap(),
187 false
188 ))
189 );
190 }
191
192 #[tokio::test]
193 async fn get_metadata_of_directory() {
194 let directory = tempdir().unwrap();
195
196 assert_eq!(
197 OsFileSystem::new(1)
198 .metadata(directory.path())
199 .await
200 .unwrap(),
201 Some(Metadata::new(
202 metadata(directory.path()).unwrap().modified().unwrap(),
203 true
204 ))
205 );
206 }
207
208 #[tokio::test]
209 async fn get_metadata_of_missing_file() {
210 let directory = tempdir().unwrap();
211
212 assert_eq!(
213 OsFileSystem::new(1)
214 .metadata(&directory.path().join("foo"))
215 .await,
216 Ok(None)
217 );
218 }
219
220 #[tokio::test]
221 async fn fail_to_get_metadata_under_file() {
222 let directory = tempdir().unwrap();
223 let path = directory.path().join("foo");
224
225 write(&path, "").unwrap();
226
227 assert!(
228 OsFileSystem::new(1)
229 .metadata(&path.join("bar"))
230 .await
231 .unwrap_err()
232 .to_string()
233 .contains(&path.join("bar").display().to_string())
234 );
235 }
236
237 #[tokio::test]
238 async fn create_directory() {
239 let directory = tempdir().unwrap();
240 let path = directory.path().join("foo").join("bar");
241
242 OsFileSystem::new(1).create_directory(&path).await.unwrap();
243
244 assert!(metadata(&path).unwrap().is_dir());
245 }
246
247 #[tokio::test]
248 async fn remove_file() {
249 let directory = tempdir().unwrap();
250 let path = directory.path().join("foo");
251
252 write(&path, "").unwrap();
253
254 OsFileSystem::new(1).remove_file(&path).await.unwrap();
255
256 assert!(!path.try_exists().unwrap());
257 }
258}