From c5fea26c66c21592488e68cb20f885b6c4759a90 Mon Sep 17 00:00:00 2001 From: Jordan Danford Date: Tue, 2 Apr 2024 07:40:53 -0700 Subject: [PATCH] New error type that includes path information * Wraps std::io::error & includes the path. * Use thiserror to reduce code maintenance. * Use a wrapped error to maintain API compatibility in further evolutions. --------- Co-authored-by: Ririsoft --- Cargo.toml | 5 +-- src/error.rs | 42 +++++++++++++++++++++++ src/lib.rs | 96 +++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 125 insertions(+), 18 deletions(-) create mode 100644 src/error.rs diff --git a/Cargo.toml b/Cargo.toml index 4073b56..f28fd67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-walkdir" -version = "1.0.0" +version = "2.0.0" authors = ["Ririsoft "] edition = "2021" description = "Asynchronous directory traversal for Rust." @@ -16,6 +16,7 @@ readme = "README.md" [dependencies] futures-lite = "2.1.0" async-fs = "2.1.0" +thiserror = "1.0.58" [dev-dependencies] -tempfile = "3.9.0" \ No newline at end of file +tempfile = "3.9.0" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..909b75b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2020-2024 Ririsoft +// SPDX-FileCopyrightText: 2024 Jordan Danford +// SPDX-License-Identifier: Apache-2.0 + +use std::{ + io, + path::{Path, PathBuf}, +}; + +use thiserror::Error; + +#[derive(Debug, Error)] +#[error(transparent)] +/// An error produced during a directory tree traversal. +pub struct Error(#[from] InnerError); + +impl Error { + /// Returns the path where the error occured if it applies, + /// for instance during IO operations. + pub fn path(&self) -> Option<&Path> { + let InnerError::Io { ref path, .. } = self.0; + Some(path) + } + + /// Returns the original [`io::Error`] if any. + pub fn io(&self) -> Option<&io::Error> { + let InnerError::Io { ref source, .. } = self.0; + Some(source) + } +} + +#[derive(Debug, Error)] +pub enum InnerError { + #[error("IO error at '{path}': {source}")] + /// A error produced during an IO operation. + Io { + /// The path in the directory tree where the IO error occured. + path: PathBuf, + /// The IO error. + source: io::Error, + }, +} diff --git a/src/lib.rs b/src/lib.rs index 6c42cf9..da598ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ // Copyright 2020 Ririsoft +// Copyright 2024 Jordan Danford // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -78,6 +79,8 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] +mod error; + use std::future::Future; use std::path::{Path, PathBuf}; use std::pin::Pin; @@ -90,8 +93,12 @@ use futures_lite::stream::{self, Stream, StreamExt}; #[doc(no_inline)] pub use async_fs::DirEntry; -#[doc(no_inline)] -pub use std::io::Result; + +pub use error::Error; +use error::InnerError; + +/// A specialized [`Result`][`std::result::Result`] type. +pub type Result = std::result::Result; type BoxStream = futures_lite::stream::Boxed>; @@ -165,9 +172,12 @@ where State::Start((root.as_ref().to_owned(), filter)), move |state| async move { match state { - State::Start((root, filter)) => match read_dir(root).await { - Err(e) => Some((Err(e), State::Done)), - Ok(rd) => walk(vec![rd], filter).await, + State::Start((root, filter)) => match read_dir(&root).await { + Err(source) => Some(( + Err(InnerError::Io { path: root, source }.into()), + State::Done, + )), + Ok(rd) => walk(vec![(root, rd)], filter).await, }, State::Walk((dirs, filter)) => walk(dirs, filter).await, State::Done => None, @@ -179,22 +189,32 @@ where enum State { Start((PathBuf, Option)), - Walk((Vec, Option)), + Walk((Vec<(PathBuf, ReadDir)>, Option)), Done, } type UnfoldState = (Result, State); -fn walk(mut dirs: Vec, filter: Option) -> BoxedFut>> +fn walk( + mut dirs: Vec<(PathBuf, ReadDir)>, + filter: Option, +) -> BoxedFut>> where F: FnMut(DirEntry) -> Fut + Send + 'static, Fut: Future + Send, { async move { - if let Some(dir) = dirs.last_mut() { + if let Some((path, dir)) = dirs.last_mut() { match dir.next().await { Some(Ok(entry)) => walk_entry(entry, dirs, filter).await, - Some(Err(e)) => Some((Err(e), State::Walk((dirs, filter)))), + Some(Err(source)) => Some(( + Err(InnerError::Io { + path: path.to_path_buf(), + source, + } + .into()), + State::Walk((dirs, filter)), + )), None => { dirs.pop(); walk(dirs, filter).await @@ -209,7 +229,7 @@ where fn walk_entry( entry: DirEntry, - mut dirs: Vec, + mut dirs: Vec<(PathBuf, ReadDir)>, mut filter: Option, ) -> BoxedFut>> where @@ -218,19 +238,32 @@ where { async move { match entry.file_type().await { - Err(e) => Some((Err(e), State::Walk((dirs, filter)))), + Err(source) => Some(( + Err(InnerError::Io { + path: entry.path(), + source, + } + .into()), + State::Walk((dirs, filter)), + )), Ok(ft) => { let filtering = match filter.as_mut() { Some(filter) => filter(entry.clone()).await, None => Filtering::Continue, }; if ft.is_dir() { - let rd = match read_dir(entry.path()).await { - Err(e) => return Some((Err(e), State::Walk((dirs, filter)))), + let path = entry.path(); + let rd = match read_dir(&path).await { + Err(source) => { + return Some(( + Err(InnerError::Io { path, source }.into()), + State::Walk((dirs, filter)), + )) + } Ok(rd) => rd, }; if filtering != Filtering::IgnoreDir { - dirs.push(rd); + dirs.push((path, rd)); } } match filtering { @@ -267,8 +300,11 @@ mod tests { block_on(async { let mut wd = WalkDir::new("foobar"); match wd.next().await.unwrap() { - Ok(_) => panic!("want error"), - Err(e) => assert_eq!(e.kind(), ErrorKind::NotFound), + Err(e) => { + assert_eq!(wd.root, e.path().unwrap()); + assert_eq!(e.io().unwrap().kind(), ErrorKind::NotFound); + } + _ => panic!("want IO error"), } }) } @@ -385,3 +421,31 @@ mod tests { }) } } + +#[cfg(all(unix, test))] +mod test_unix { + use async_fs::unix::PermissionsExt; + use std::io::Result; + + use futures_lite::future::block_on; + use futures_lite::stream::StreamExt; + + use super::WalkDir; + #[test] + fn walk_dir_error_path() -> Result<()> { + block_on(async { + let root = tempfile::tempdir()?; + let d1 = root.path().join("d1"); + async_fs::create_dir_all(&d1).await?; + let mut perms = async_fs::metadata(&d1).await?.permissions(); + perms.set_mode(0o222); + async_fs::set_permissions(&d1, perms).await?; + let mut wd = WalkDir::new(&root); + match wd.next().await.unwrap() { + Err(e) => assert_eq!(e.path().unwrap(), d1.as_path()), + _ => panic!("want IO error"), + } + Ok(()) + }) + } +}