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