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, 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/// 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    // Text files are read inline because they are small enough for a read to be
42    // cheaper than a round trip through the blocking thread pool.
43    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    // Existence checks run inline because a `stat` call is cheaper than a round
48    // trip through the blocking thread pool.
49    async fn exists(&self, path: &Path) -> Result<bool, FileError> {
50        exists(path).map_err(|error| Self::error(error, path))
51    }
52
53    // Metadata queries run inline for the same reason as existence checks.
54    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    // Windows reports a path under a file as a missing file.
219    #[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}