turtle_build/infrastructure/file_system/
os.rs1use 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#[derive(Debug)]
17pub struct OsFileSystem {
18 semaphore: Semaphore,
19}
20
21impl OsFileSystem {
22 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}