hojicha_core/async_helpers/
file_io.rs1use crate::commands;
4use crate::core::{Cmd, Message};
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone)]
9pub enum FileError {
10 NotFound(PathBuf),
12 PermissionDenied(PathBuf),
14 IoError(String),
16 Utf8Error(String),
18}
19
20impl std::fmt::Display for FileError {
21 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22 match self {
23 Self::NotFound(path) => write!(f, "File not found: {}", path.display()),
24 Self::PermissionDenied(path) => write!(f, "Permission denied: {}", path.display()),
25 Self::IoError(e) => write!(f, "I/O error: {e}"),
26 Self::Utf8Error(e) => write!(f, "UTF-8 error: {e}"),
27 }
28 }
29}
30
31impl std::error::Error for FileError {}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum FileEvent {
36 Created(PathBuf),
38 Modified(PathBuf),
40 Deleted(PathBuf),
42 Renamed {
44 from: PathBuf,
46 to: PathBuf,
48 },
49}
50
51pub fn read_file<M, F, P>(path: P, handler: F) -> Cmd<M>
71where
72 M: Message,
73 F: FnOnce(Result<String, FileError>) -> M + Send + 'static,
74 P: AsRef<Path> + Send + 'static,
75{
76 let path = path.as_ref().to_path_buf();
77
78 commands::spawn(async move {
79 let result = tokio::fs::read_to_string(&path).await.map_err(|e| {
80 if e.kind() == std::io::ErrorKind::NotFound {
81 FileError::NotFound(path.clone())
82 } else if e.kind() == std::io::ErrorKind::PermissionDenied {
83 FileError::PermissionDenied(path.clone())
84 } else {
85 FileError::IoError(e.to_string())
86 }
87 });
88
89 Some(handler(result))
90 })
91}
92
93pub fn read_file_bytes<M, F, P>(path: P, handler: F) -> Cmd<M>
95where
96 M: Message,
97 F: FnOnce(Result<Vec<u8>, FileError>) -> M + Send + 'static,
98 P: AsRef<Path> + Send + 'static,
99{
100 let path = path.as_ref().to_path_buf();
101
102 commands::spawn(async move {
103 let result = tokio::fs::read(&path).await.map_err(|e| {
104 if e.kind() == std::io::ErrorKind::NotFound {
105 FileError::NotFound(path.clone())
106 } else if e.kind() == std::io::ErrorKind::PermissionDenied {
107 FileError::PermissionDenied(path.clone())
108 } else {
109 FileError::IoError(e.to_string())
110 }
111 });
112
113 Some(handler(result))
114 })
115}
116
117pub fn write_file<M, F, P, C>(path: P, content: C, handler: F) -> Cmd<M>
137where
138 M: Message,
139 F: FnOnce(Result<(), FileError>) -> M + Send + 'static,
140 P: AsRef<Path> + Send + 'static,
141 C: AsRef<[u8]> + Send + 'static,
142{
143 let path = path.as_ref().to_path_buf();
144 let content = content.as_ref().to_vec();
145
146 commands::spawn(async move {
147 let result = tokio::fs::write(&path, content).await.map_err(|e| {
148 if e.kind() == std::io::ErrorKind::PermissionDenied {
149 FileError::PermissionDenied(path.clone())
150 } else {
151 FileError::IoError(e.to_string())
152 }
153 });
154
155 Some(handler(result))
156 })
157}
158
159pub fn watch_file<M, F, P>(path: P, mut handler: F) -> Cmd<M>
184where
185 M: Message,
186 F: FnMut(FileEvent) -> Option<M> + Send + 'static,
187 P: AsRef<Path> + Send + 'static,
188{
189 let path = path.as_ref().to_path_buf();
190
191 commands::spawn(async move {
192 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
195 handler(FileEvent::Modified(path))
196 })
197}
198
199pub fn list_dir<M, F, P>(path: P, handler: F) -> Cmd<M>
201where
202 M: Message,
203 F: FnOnce(Result<Vec<PathBuf>, FileError>) -> M + Send + 'static,
204 P: AsRef<Path> + Send + 'static,
205{
206 let path = path.as_ref().to_path_buf();
207
208 commands::spawn(async move {
209 let result = async {
210 let mut entries = Vec::new();
211 let mut dir = tokio::fs::read_dir(&path).await.map_err(|e| {
212 if e.kind() == std::io::ErrorKind::NotFound {
213 FileError::NotFound(path.clone())
214 } else if e.kind() == std::io::ErrorKind::PermissionDenied {
215 FileError::PermissionDenied(path.clone())
216 } else {
217 FileError::IoError(e.to_string())
218 }
219 })?;
220
221 while let Some(entry) = dir
222 .next_entry()
223 .await
224 .map_err(|e| FileError::IoError(e.to_string()))?
225 {
226 entries.push(entry.path());
227 }
228
229 Ok(entries)
230 }
231 .await;
232
233 Some(handler(result))
234 })
235}
236
237pub fn create_dir<M, F, P>(path: P, handler: F) -> Cmd<M>
239where
240 M: Message,
241 F: FnOnce(Result<(), FileError>) -> M + Send + 'static,
242 P: AsRef<Path> + Send + 'static,
243{
244 let path = path.as_ref().to_path_buf();
245
246 commands::spawn(async move {
247 let result = tokio::fs::create_dir_all(&path).await.map_err(|e| {
248 if e.kind() == std::io::ErrorKind::PermissionDenied {
249 FileError::PermissionDenied(path.clone())
250 } else {
251 FileError::IoError(e.to_string())
252 }
253 });
254
255 Some(handler(result))
256 })
257}
258
259pub fn delete<M, F, P>(path: P, handler: F) -> Cmd<M>
261where
262 M: Message,
263 F: FnOnce(Result<(), FileError>) -> M + Send + 'static,
264 P: AsRef<Path> + Send + 'static,
265{
266 let path = path.as_ref().to_path_buf();
267
268 commands::spawn(async move {
269 let result = if path.is_dir() {
270 tokio::fs::remove_dir_all(&path).await
271 } else {
272 tokio::fs::remove_file(&path).await
273 }
274 .map_err(|e| {
275 if e.kind() == std::io::ErrorKind::NotFound {
276 FileError::NotFound(path.clone())
277 } else if e.kind() == std::io::ErrorKind::PermissionDenied {
278 FileError::PermissionDenied(path.clone())
279 } else {
280 FileError::IoError(e.to_string())
281 }
282 });
283
284 Some(handler(result))
285 })
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291 use proptest::prelude::*;
292
293 proptest! {
294 #[test]
295 fn prop_file_error_consistency(error_msg in ".*") {
296 let file_error = FileError::IoError(error_msg.clone());
297 let error_string = file_error.to_string();
298 prop_assert!(error_string.contains(&error_msg));
299 }
300 }
301}