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    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/// A file system backed by an operating system.
14#[derive(Debug)]
15pub struct OsFileSystem {
16    semaphore: Semaphore,
17}
18
19impl OsFileSystem {
20    /// Creates a file system.
21    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}