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