use std::fs::Metadata; use std::path::{Path, PathBuf}; use tokio::fs; use tokio::sync::mpsc; pub type Result = std::result::Result; pub use tokio::fs::ReadDir; #[derive(Debug, thiserror::Error)] pub enum Error { #[error("{0}: read dir: {1}")] ReadDir(PathBuf, std::io::Error), #[error("{0}: exists: {1}")] Exists(PathBuf, std::io::Error), #[error("{0}: read: {1}")] Read(PathBuf, std::io::Error), #[error("{0}: stat: {1}")] Stat(PathBuf, std::io::Error), #[error("{0}: create dir: {1}")] CreateDir(PathBuf, std::io::Error), #[error("{0}: write: {1}")] Write(PathBuf, std::io::Error), #[error("{0}: remove file: {1}")] RemoveFile(PathBuf, std::io::Error), #[error("{0}: symlink: {1}")] Symlink(PathBuf, std::io::Error), #[error("{0}: {1}")] Other(PathBuf, String), } macro_rules! wrap_path { ($fn:ident $( ( $( $pname:ident : $ptype:ty ),* ) )? -> $result:ty, $err:ident) => { pub async fn $fn(path: impl AsRef$($(, $pname: $ptype)*)?) -> Result<$result> { let path = path.as_ref(); fs::$fn(path $($(, $pname)*)?).await.map_err(|e| Error::$err(path.into(), e)) } }; } wrap_path!(read_dir -> ReadDir, ReadDir); wrap_path!(try_exists -> bool, Exists); wrap_path!(read -> Vec, Read); wrap_path!(read_to_string -> String, Read); wrap_path!(create_dir -> (), CreateDir); wrap_path!(create_dir_all -> (), CreateDir); wrap_path!(remove_file -> (), RemoveFile); wrap_path!(symlink(link_src: impl AsRef) -> (), Symlink); wrap_path!(write(content: impl AsRef<[u8]>) -> (), Write); pub fn spawn_walk_dir( dir: impl Into + Send + 'static, ) -> mpsc::Receiver> { let (tx, rx) = mpsc::channel(1); tokio::spawn(walk_dir(dir, tx)); rx } pub async fn walk_dir(dir: impl Into, tx: mpsc::Sender>) { let dir: PathBuf = dir.into(); let mut todo = std::collections::LinkedList::new(); if let Ok(rd) = read_dir(&dir).await { todo.push_front(rd); } while let Some(rd) = todo.front_mut() { let entry = match rd.next_entry().await { Ok(v) => v, Err(e) => { if tx.send(Err(Error::ReadDir(dir.clone(), e))).await.is_err() { return; } todo.pop_front(); // skip dir on error continue; } }; let Some(entry) = entry else { todo.pop_front(); continue; }; let Ok(md) = entry.metadata().await else { continue; }; let is_dir = md.is_dir(); let Ok(path) = entry.path().strip_prefix(&dir).map(|p| p.to_path_buf()) else { continue; // sub-entry not in dir, weird but semantically, we ignore }; if tx.send(Ok((path, md))).await.is_err() { return; } // recurse in sub directories if is_dir { if let Ok(rd) = read_dir(entry.path()).await { todo.push_front(rd); } } } }