Skip to main content

turtle_build/infrastructure/file_system/
os.rs

1use super::Metadata;
2use crate::infrastructure::FileSystem;
3use async_trait::async_trait;
4use core::error::Error;
5use std::{
6    io,
7    path::{Path, PathBuf},
8};
9use tokio::{
10    fs::{self, File},
11    io::AsyncReadExt,
12    sync::Semaphore,
13};
14
15/// A file system backed by an operating system.
16#[derive(Debug)]
17pub struct OsFileSystem {
18    semaphore: Semaphore,
19}
20
21impl OsFileSystem {
22    /// Creates a file system.
23    pub fn new(open_file_limit: usize) -> Self {
24        Self {
25            semaphore: Semaphore::new(open_file_limit.min(Semaphore::MAX_PERMITS)),
26        }
27    }
28
29    fn error(error: io::Error, path: &Path) -> String {
30        format!("{}: {}", error, path.display())
31    }
32}
33
34#[async_trait]
35impl FileSystem for OsFileSystem {
36    async fn read_file(&self, path: &Path, buffer: &mut Vec<u8>) -> Result<(), Box<dyn Error>> {
37        let _permit = self.semaphore.acquire().await?;
38
39        File::open(path)
40            .await
41            .map_err(|error| Self::error(error, path))?
42            .read_to_end(buffer)
43            .await
44            .map_err(|error| Self::error(error, path))?;
45
46        Ok(())
47    }
48
49    async fn read_file_to_string(
50        &self,
51        path: &Path,
52        buffer: &mut String,
53    ) -> Result<(), Box<dyn Error>> {
54        let _permit = self.semaphore.acquire().await?;
55
56        File::open(path)
57            .await
58            .map_err(|error| Self::error(error, path))?
59            .read_to_string(buffer)
60            .await
61            .map_err(|error| Self::error(error, path))?;
62
63        Ok(())
64    }
65
66    async fn exists(&self, path: &Path) -> Result<bool, Box<dyn Error>> {
67        Ok(fs::try_exists(path)
68            .await
69            .map_err(|error| Self::error(error, path))?)
70    }
71
72    async fn metadata(&self, path: &Path) -> Result<Metadata, Box<dyn Error>> {
73        Ok(fs::metadata(path)
74            .await
75            .map_err(|error| Self::error(error, path))?
76            .try_into()?)
77    }
78
79    async fn create_directory(&self, path: &Path) -> Result<(), Box<dyn Error>> {
80        fs::create_dir_all(path)
81            .await
82            .map_err(|error| Self::error(error, path))?;
83
84        Ok(())
85    }
86
87    async fn canonicalize_path(&self, path: &Path) -> Result<PathBuf, Box<dyn Error>> {
88        Ok(fs::canonicalize(path)
89            .await
90            .map_err(|error| Self::error(error, path))?)
91    }
92
93    async fn remove_file(&self, path: &Path) -> Result<(), Box<dyn Error>> {
94        fs::remove_file(path)
95            .await
96            .map_err(|error| Self::error(error, path))?;
97
98        Ok(())
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use pretty_assertions::assert_eq;
106    use std::fs;
107    use tempfile::tempdir;
108
109    #[test]
110    fn limit_open_files() {
111        assert_eq!(OsFileSystem::new(42).semaphore.available_permits(), 42);
112    }
113
114    #[test]
115    fn limit_open_files_to_maximum_permits() {
116        assert_eq!(
117            OsFileSystem::new(usize::MAX).semaphore.available_permits(),
118            Semaphore::MAX_PERMITS
119        );
120    }
121
122    #[tokio::test]
123    async fn read_file_to_string() {
124        let directory = tempdir().unwrap();
125        let path = directory.path().join("foo");
126        let mut buffer = "foo".to_owned();
127
128        fs::write(&path, "bar").unwrap();
129
130        OsFileSystem::new(1)
131            .read_file_to_string(&path, &mut buffer)
132            .await
133            .unwrap();
134
135        assert_eq!(buffer, "foobar");
136    }
137
138    #[tokio::test]
139    async fn fail_to_read_missing_file_to_string() {
140        let directory = tempdir().unwrap();
141        let path = directory.path().join("foo");
142
143        assert!(
144            OsFileSystem::new(1)
145                .read_file_to_string(&path, &mut String::new())
146                .await
147                .unwrap_err()
148                .to_string()
149                .contains(&path.display().to_string())
150        );
151    }
152
153    #[tokio::test]
154    async fn check_file_existence() {
155        let directory = tempdir().unwrap();
156        let path = directory.path().join("foo");
157        let file_system = OsFileSystem::new(1);
158
159        assert!(!file_system.exists(&path).await.unwrap());
160
161        fs::write(&path, "").unwrap();
162
163        assert!(file_system.exists(&path).await.unwrap());
164    }
165
166    #[tokio::test]
167    async fn create_directory() {
168        let directory = tempdir().unwrap();
169        let path = directory.path().join("foo").join("bar");
170
171        OsFileSystem::new(1).create_directory(&path).await.unwrap();
172
173        assert!(fs::metadata(&path).unwrap().is_dir());
174    }
175
176    #[tokio::test]
177    async fn remove_file() {
178        let directory = tempdir().unwrap();
179        let path = directory.path().join("foo");
180
181        fs::write(&path, "").unwrap();
182
183        OsFileSystem::new(1).remove_file(&path).await.unwrap();
184
185        assert!(!path.try_exists().unwrap());
186    }
187}