Skip to main content

turtle_build/infrastructure/file_system/
os.rs

1use 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/// A file system backed by an operating system.
15#[derive(Debug)]
16pub struct OsFileSystem {
17    semaphore: Semaphore,
18}
19
20impl OsFileSystem {
21    /// Creates a file system.
22    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    // Existence checks run inline because a `stat` call is cheaper than a round
50    // trip through the blocking thread pool.
51    async fn exists(&self, path: &Path) -> Result<bool, FileError> {
52        exists(path).map_err(|error| Self::error(error, path))
53    }
54
55    // Metadata queries run inline for the same reason as existence checks.
56    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    // Windows reports a path under a file as a missing file.
221    #[cfg(unix)]
222    #[tokio::test]
223    async fn fail_to_get_metadata_under_file() {
224        let directory = tempdir().unwrap();
225        let path = directory.path().join("foo");
226
227        write(&path, "").unwrap();
228
229        assert!(
230            OsFileSystem::new(1)
231                .metadata(&path.join("bar"))
232                .await
233                .unwrap_err()
234                .to_string()
235                .contains(&path.join("bar").display().to_string())
236        );
237    }
238
239    #[tokio::test]
240    async fn create_directory() {
241        let directory = tempdir().unwrap();
242        let path = directory.path().join("foo").join("bar");
243
244        OsFileSystem::new(1).create_directory(&path).await.unwrap();
245
246        assert!(metadata(&path).unwrap().is_dir());
247    }
248
249    #[tokio::test]
250    async fn remove_file() {
251        let directory = tempdir().unwrap();
252        let path = directory.path().join("foo");
253
254        write(&path, "").unwrap();
255
256        OsFileSystem::new(1).remove_file(&path).await.unwrap();
257
258        assert!(!path.try_exists().unwrap());
259    }
260}