Skip to content

Commit

Permalink
New error type that includes path information
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
jdanford and ririsoft authored Apr 2, 2024
1 parent 38696b9 commit c5fea26
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 18 deletions.
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "async-walkdir"
version = "1.0.0"
version = "2.0.0"
authors = ["Ririsoft <[email protected]>"]
edition = "2021"
description = "Asynchronous directory traversal for Rust."
Expand All @@ -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"
tempfile = "3.9.0"
42 changes: 42 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2020-2024 Ririsoft <[email protected]>
// SPDX-FileCopyrightText: 2024 Jordan Danford <[email protected]>
// 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,
},
}
96 changes: 80 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright 2020 Ririsoft <[email protected]>
// Copyright 2024 Jordan Danford <[email protected]>
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -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;
Expand All @@ -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<T> = std::result::Result<T, Error>;

type BoxStream = futures_lite::stream::Boxed<Result<DirEntry>>;

Expand Down Expand Up @@ -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,
Expand All @@ -179,22 +189,32 @@ where

enum State<F> {
Start((PathBuf, Option<F>)),
Walk((Vec<ReadDir>, Option<F>)),
Walk((Vec<(PathBuf, ReadDir)>, Option<F>)),
Done,
}

type UnfoldState<F> = (Result<DirEntry>, State<F>);

fn walk<F, Fut>(mut dirs: Vec<ReadDir>, filter: Option<F>) -> BoxedFut<Option<UnfoldState<F>>>
fn walk<F, Fut>(
mut dirs: Vec<(PathBuf, ReadDir)>,
filter: Option<F>,
) -> BoxedFut<Option<UnfoldState<F>>>
where
F: FnMut(DirEntry) -> Fut + Send + 'static,
Fut: Future<Output = Filtering> + 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
Expand All @@ -209,7 +229,7 @@ where

fn walk_entry<F, Fut>(
entry: DirEntry,
mut dirs: Vec<ReadDir>,
mut dirs: Vec<(PathBuf, ReadDir)>,
mut filter: Option<F>,
) -> BoxedFut<Option<UnfoldState<F>>>
where
Expand All @@ -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 {
Expand Down Expand Up @@ -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"),
}
})
}
Expand Down Expand Up @@ -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(())
})
}
}

0 comments on commit c5fea26

Please sign in to comment.