use crate::commands;
use crate::core::{Cmd, Message};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub enum FileError {
NotFound(PathBuf),
PermissionDenied(PathBuf),
IoError(String),
Utf8Error(String),
}
impl std::fmt::Display for FileError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotFound(path) => write!(f, "File not found: {}", path.display()),
Self::PermissionDenied(path) => write!(f, "Permission denied: {}", path.display()),
Self::IoError(e) => write!(f, "I/O error: {e}"),
Self::Utf8Error(e) => write!(f, "UTF-8 error: {e}"),
}
}
}
impl std::error::Error for FileError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FileEvent {
Created(PathBuf),
Modified(PathBuf),
Deleted(PathBuf),
Renamed {
from: PathBuf,
to: PathBuf,
},
}
pub fn read_file<M, F, P>(path: P, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<String, FileError>) -> M + Send + 'static,
P: AsRef<Path> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
commands::spawn(async move {
let result = tokio::fs::read_to_string(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
FileError::NotFound(path.clone())
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
FileError::PermissionDenied(path.clone())
} else {
FileError::IoError(e.to_string())
}
});
Some(handler(result))
})
}
pub fn read_file_bytes<M, F, P>(path: P, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<Vec<u8>, FileError>) -> M + Send + 'static,
P: AsRef<Path> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
commands::spawn(async move {
let result = tokio::fs::read(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
FileError::NotFound(path.clone())
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
FileError::PermissionDenied(path.clone())
} else {
FileError::IoError(e.to_string())
}
});
Some(handler(result))
})
}
pub fn write_file<M, F, P, C>(path: P, content: C, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<(), FileError>) -> M + Send + 'static,
P: AsRef<Path> + Send + 'static,
C: AsRef<[u8]> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
let content = content.as_ref().to_vec();
commands::spawn(async move {
let result = tokio::fs::write(&path, content).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
FileError::PermissionDenied(path.clone())
} else {
FileError::IoError(e.to_string())
}
});
Some(handler(result))
})
}
pub fn watch_file<M, F, P>(path: P, mut handler: F) -> Cmd<M>
where
M: Message,
F: FnMut(FileEvent) -> Option<M> + Send + 'static,
P: AsRef<Path> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
commands::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
handler(FileEvent::Modified(path))
})
}
pub fn list_dir<M, F, P>(path: P, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<Vec<PathBuf>, FileError>) -> M + Send + 'static,
P: AsRef<Path> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
commands::spawn(async move {
let result = async {
let mut entries = Vec::new();
let mut dir = tokio::fs::read_dir(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
FileError::NotFound(path.clone())
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
FileError::PermissionDenied(path.clone())
} else {
FileError::IoError(e.to_string())
}
})?;
while let Some(entry) = dir
.next_entry()
.await
.map_err(|e| FileError::IoError(e.to_string()))?
{
entries.push(entry.path());
}
Ok(entries)
}
.await;
Some(handler(result))
})
}
pub fn create_dir<M, F, P>(path: P, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<(), FileError>) -> M + Send + 'static,
P: AsRef<Path> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
commands::spawn(async move {
let result = tokio::fs::create_dir_all(&path).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
FileError::PermissionDenied(path.clone())
} else {
FileError::IoError(e.to_string())
}
});
Some(handler(result))
})
}
pub fn delete<M, F, P>(path: P, handler: F) -> Cmd<M>
where
M: Message,
F: FnOnce(Result<(), FileError>) -> M + Send + 'static,
P: AsRef<Path> + Send + 'static,
{
let path = path.as_ref().to_path_buf();
commands::spawn(async move {
let result = if path.is_dir() {
tokio::fs::remove_dir_all(&path).await
} else {
tokio::fs::remove_file(&path).await
}
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
FileError::NotFound(path.clone())
} else if e.kind() == std::io::ErrorKind::PermissionDenied {
FileError::PermissionDenied(path.clone())
} else {
FileError::IoError(e.to_string())
}
});
Some(handler(result))
})
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_file_error_consistency(error_msg in ".*") {
let file_error = FileError::IoError(error_msg.clone());
let error_string = file_error.to_string();
prop_assert!(error_string.contains(&error_msg));
}
}
}