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::{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/// 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    // Canonicalization runs inline for the same reason as existence checks.
71    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    // Windows reports a path under a file as a missing file.
218    #[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}