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, PathBuf},
8};
9use tokio::{
10 fs::{canonicalize, 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> {
71 canonicalize(path)
72 .await
73 .map_err(|error| Self::error(error, path))
74 }
75
76 async fn remove_file(&self, path: &Path) -> Result<(), FileError> {
77 remove_file(path)
78 .await
79 .map_err(|error| Self::error(error, path))?;
80
81 Ok(())
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use pretty_assertions::assert_eq;
89 use std::fs::{metadata, write};
90 use tempfile::tempdir;
91
92 #[test]
93 fn limit_open_files() {
94 assert_eq!(OsFileSystem::new(42).semaphore.available_permits(), 42);
95 }
96
97 #[test]
98 fn limit_open_files_to_maximum_permits() {
99 assert_eq!(
100 OsFileSystem::new(usize::MAX).semaphore.available_permits(),
101 Semaphore::MAX_PERMITS
102 );
103 }
104
105 #[tokio::test]
106 async fn read_file() {
107 let directory = tempdir().unwrap();
108 let path = directory.path().join("foo");
109
110 write(&path, "bar").unwrap();
111
112 assert_eq!(OsFileSystem::new(1).read_file(&path).await.unwrap(), b"bar");
113 }
114
115 #[tokio::test]
116 async fn fail_to_read_missing_file() {
117 let directory = tempdir().unwrap();
118 let path = directory.path().join("foo");
119
120 assert!(
121 OsFileSystem::new(1)
122 .read_file(&path)
123 .await
124 .unwrap_err()
125 .to_string()
126 .contains(&path.display().to_string())
127 );
128 }
129
130 #[tokio::test]
131 async fn read_file_to_string() {
132 let directory = tempdir().unwrap();
133 let path = directory.path().join("foo");
134
135 write(&path, "bar").unwrap();
136
137 assert_eq!(
138 OsFileSystem::new(1)
139 .read_file_to_string(&path)
140 .await
141 .unwrap(),
142 "bar"
143 );
144 }
145
146 #[tokio::test]
147 async fn fail_to_read_missing_file_to_string() {
148 let directory = tempdir().unwrap();
149 let path = directory.path().join("foo");
150
151 assert!(
152 OsFileSystem::new(1)
153 .read_file_to_string(&path)
154 .await
155 .unwrap_err()
156 .to_string()
157 .contains(&path.display().to_string())
158 );
159 }
160
161 #[tokio::test]
162 async fn check_file_existence() {
163 let directory = tempdir().unwrap();
164 let path = directory.path().join("foo");
165 let file_system = OsFileSystem::new(1);
166
167 assert!(!file_system.exists(&path).await.unwrap());
168
169 write(&path, "").unwrap();
170
171 assert!(file_system.exists(&path).await.unwrap());
172 }
173
174 #[tokio::test]
175 async fn get_metadata_of_file() {
176 let directory = tempdir().unwrap();
177 let path = directory.path().join("foo");
178
179 write(&path, "").unwrap();
180
181 assert_eq!(
182 OsFileSystem::new(1).metadata(&path).await.unwrap(),
183 Some(Metadata::new(
184 metadata(&path).unwrap().modified().unwrap(),
185 false
186 ))
187 );
188 }
189
190 #[tokio::test]
191 async fn get_metadata_of_directory() {
192 let directory = tempdir().unwrap();
193
194 assert_eq!(
195 OsFileSystem::new(1)
196 .metadata(directory.path())
197 .await
198 .unwrap(),
199 Some(Metadata::new(
200 metadata(directory.path()).unwrap().modified().unwrap(),
201 true
202 ))
203 );
204 }
205
206 #[tokio::test]
207 async fn get_metadata_of_missing_file() {
208 let directory = tempdir().unwrap();
209
210 assert_eq!(
211 OsFileSystem::new(1)
212 .metadata(&directory.path().join("foo"))
213 .await,
214 Ok(None)
215 );
216 }
217
218 #[cfg(unix)]
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}