turtle_build/infrastructure/file_system/
os.rs1use super::Metadata;
2use crate::infrastructure::{FileError, FileSystem};
3use async_trait::async_trait;
4use std::{
5 io,
6 path::{Path, PathBuf},
7};
8use tokio::{
9 fs::{canonicalize, create_dir_all, metadata, read, read_to_string, remove_file, try_exists},
10 sync::Semaphore,
11};
12
13#[derive(Debug)]
15pub struct OsFileSystem {
16 semaphore: Semaphore,
17}
18
19impl OsFileSystem {
20 pub fn new(open_file_limit: usize) -> Self {
22 Self {
23 semaphore: Semaphore::new(open_file_limit.min(Semaphore::MAX_PERMITS)),
24 }
25 }
26
27 fn error(error: io::Error, path: &Path) -> FileError {
28 FileError::new(format!("{}: {}", error, path.display()))
29 }
30}
31
32#[async_trait]
33impl FileSystem for OsFileSystem {
34 async fn read_file(&self, path: &Path) -> Result<Vec<u8>, FileError> {
35 let _permit = self.semaphore.acquire().await?;
36
37 read(path).await.map_err(|error| Self::error(error, path))
38 }
39
40 async fn read_file_to_string(&self, path: &Path) -> Result<String, FileError> {
41 let _permit = self.semaphore.acquire().await?;
42
43 read_to_string(path)
44 .await
45 .map_err(|error| Self::error(error, path))
46 }
47
48 async fn exists(&self, path: &Path) -> Result<bool, FileError> {
49 try_exists(path)
50 .await
51 .map_err(|error| Self::error(error, path))
52 }
53
54 async fn metadata(&self, path: &Path) -> Result<Metadata, FileError> {
55 Ok(metadata(path)
56 .await
57 .map_err(|error| Self::error(error, path))?
58 .try_into()?)
59 }
60
61 async fn create_directory(&self, path: &Path) -> Result<(), FileError> {
62 create_dir_all(path)
63 .await
64 .map_err(|error| Self::error(error, path))?;
65
66 Ok(())
67 }
68
69 async fn canonicalize_path(&self, path: &Path) -> Result<PathBuf, FileError> {
70 canonicalize(path)
71 .await
72 .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 create_directory() {
175 let directory = tempdir().unwrap();
176 let path = directory.path().join("foo").join("bar");
177
178 OsFileSystem::new(1).create_directory(&path).await.unwrap();
179
180 assert!(metadata(&path).unwrap().is_dir());
181 }
182
183 #[tokio::test]
184 async fn remove_file() {
185 let directory = tempdir().unwrap();
186 let path = directory.path().join("foo");
187
188 write(&path, "").unwrap();
189
190 OsFileSystem::new(1).remove_file(&path).await.unwrap();
191
192 assert!(!path.try_exists().unwrap());
193 }
194}