Skip to main content

turtle_build/infrastructure/
file_system.rs

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