├── .github └── workflows │ ├── release.yml │ └── rust.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml └── src ├── error.rs └── lib.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: [ "v[0-9]+.*" ] 5 | 6 | permissions: 7 | contents: write 8 | 9 | jobs: 10 | create-release: 11 | if: github.repository_owner == 'ririsoft' 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: dtolnay/rust-toolchain@stable 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | changelog: CHANGELOG.md 19 | title: $version 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | 22 | publish-release: 23 | if: github.repository_owner == 'ririsoft' 24 | needs: 25 | - create-release 26 | runs-on: ubuntu-latest 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: dtolnay/rust-toolchain@stable 30 | 31 | - name: Verify package can be published 32 | run: cargo publish --dry-run 33 | 34 | - name: Publish package to crates.io 35 | env: 36 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 37 | run: cargo publish 38 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | on: 3 | push: 4 | branches: 5 | - master 6 | tags-ignore: 7 | - "v[0-9]+.*" 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | rust-ci: 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest] 18 | 19 | steps: 20 | 21 | - name: Checkout sources 22 | uses: actions/checkout@v4 23 | 24 | - name: Install rust toolchain 25 | uses: dtolnay/rust-toolchain@stable 26 | with: 27 | toolchain: stable 28 | components: rustfmt, clippy, llvm-tools-preview 29 | 30 | - name: Setup rust smart caching 31 | uses: Swatinem/rust-cache@v2 32 | with: 33 | cache-on-failure: "true" 34 | 35 | - name: Run cargo fmt 36 | run: cargo fmt --all -- --check 37 | 38 | - name: Run cargo clippy 39 | run: cargo clippy --no-deps -- -D warnings 40 | 41 | - name: Run cargo build 42 | run: cargo build 43 | 44 | - name: Cargo tests 45 | run: cargo test 46 | 47 | - name: Setup cargo-llvm-cov 48 | run: cargo install cargo-llvm-cov 49 | 50 | - name: Run code coverage test 51 | run: | 52 | cargo llvm-cov test --fail-under-lines 90 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ### Added 10 | ### Fixed 11 | ### Changed 12 | ### Deprecated 13 | ### Removed 14 | ### Security 15 | 16 | ## [2.1.0] - 2025-01-27 17 | 18 | ### Added 19 | 20 | - New `into_io` and `From for io::Error` methods. 21 | 22 | ## [2.0.0] - 2024-06-15 23 | 24 | ### Added 25 | 26 | - New error type that allows to get the path in the filesystem where the error occured. 27 | 28 | ### Changed 29 | 30 | - Improved CI/CD with updated github actions and automated release notes. 31 | 32 | ## [1.0.0] - 2024-01-04 33 | 34 | ### Changed 35 | 36 | - Migration to Rust edition 2021. 37 | - Stabilize API to 1.0.0. 38 | 39 | ## [0.2.0] - 2020-09-07 40 | 41 | ### Changed 42 | 43 | - Update to futures-lite 1.2. 44 | 45 | ### Fixed 46 | 47 | - Docs typo. 48 | 49 | ## [0.1.0] - 2020-08-31 50 | 51 | ### Added 52 | 53 | - Initial release -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "async-walkdir" 3 | version = "2.1.0" 4 | authors = ["Ririsoft "] 5 | edition = "2021" 6 | description = "Asynchronous directory traversal for Rust." 7 | license = "Apache-2.0" 8 | keywords = ["async", "walk", "directory", "recursive", "stream"] 9 | categories = ["asynchronous", "filesystem"] 10 | homepage = "https://github.com/ririsoft/async-walkdir" 11 | repository = "https://github.com/ririsoft/async-walkdir" 12 | documentation = "https://docs.rs/async-walkdir/" 13 | exclude = [ "/.github/*" ] 14 | readme = "README.md" 15 | 16 | [dependencies] 17 | futures-lite = "2.1" 18 | async-fs = "2.1" 19 | thiserror = "2.0" 20 | 21 | [dev-dependencies] 22 | tempfile = "3.15" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Github CI](https://github.com/ririsoft/async-walkdir/workflows/Rust/badge.svg)](https://github.com/ririsoft/async-walkdir/actions) [![docs.rs](https://docs.rs/async-walkdir/badge.svg)](https://docs.rs/async-walkdir) 2 | # async-walkdir 3 | Asynchronous directory traversal for Rust. 4 | 5 | Based on [async-fs][2] and [blocking][3], 6 | it uses a thread pool to handle blocking IOs. Please refere to those crates for the rationale. 7 | This crate is compatible with async runtimes [tokio][5], [async-std][6], [smol][7] and potentially any runtime based on [futures 0.3][4] 8 | 9 | We do not plan to be as feature full as [Walkdir][1] crate in the synchronous world, but 10 | do not hesitate to open an issue or a PR. 11 | 12 | # Example 13 | 14 | ```rust 15 | use async_walkdir::WalkDir; 16 | use futures_lite::future::block_on; 17 | use futures_lite::stream::StreamExt; 18 | 19 | block_on(async { 20 | let mut entries = WalkDir::new("my_directory"); 21 | loop { 22 | match entries.next().await { 23 | Some(Ok(entry)) => println!("file: {}", entry.path().display()), 24 | Some(Err(e)) => { 25 | eprintln!("error: {}", e); 26 | break; 27 | }, 28 | None => break, 29 | } 30 | } 31 | }); 32 | ``` 33 | 34 | [1]: https://docs.rs/walkdir 35 | [2]: https://docs.rs/async-fs 36 | [3]: https://docs.rs/blocking 37 | [4]: https://docs.rs/futures-core 38 | [5]: https://docs.rs/tokio 39 | [6]: https://docs.rs/async-std 40 | [7]: https://docs.rs/smol 41 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | format_code_in_doc_comments = true -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2020-2024 Ririsoft 2 | // SPDX-FileCopyrightText: 2024 Jordan Danford 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | use std::{ 6 | io, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | use thiserror::Error; 11 | 12 | #[derive(Debug, Error)] 13 | #[error(transparent)] 14 | /// An error produced during a directory tree traversal. 15 | pub struct Error(#[from] InnerError); 16 | 17 | impl Error { 18 | /// Returns the path where the error occured if it applies, 19 | /// for instance during IO operations. 20 | pub fn path(&self) -> Option<&Path> { 21 | let InnerError::Io { ref path, .. } = self.0; 22 | Some(path) 23 | } 24 | 25 | /// Returns the original [`io::Error`] if any. 26 | pub fn io(&self) -> Option<&io::Error> { 27 | let InnerError::Io { ref source, .. } = self.0; 28 | Some(source) 29 | } 30 | 31 | /// Similar to [`io`][Self::io] except consumes self to convert to the original 32 | /// [`io::Error`] if one exists. 33 | pub fn into_io(self) -> Option { 34 | let InnerError::Io { source, .. } = self.0; 35 | Some(source) 36 | } 37 | } 38 | 39 | impl From for io::Error { 40 | /// Convert to an [`io::Error`], preserving the original [`struct@Error`] 41 | /// as the ["inner error"][io::Error::into_inner]. 42 | /// Note that this also makes the display of the error include the context. 43 | /// 44 | /// This is different from [`into_io`][Error::into_io] which returns 45 | /// the original [`io::Error`]. 46 | fn from(err: Error) -> io::Error { 47 | let InnerError::Io { ref source, .. } = err.0; 48 | io::Error::new(source.kind(), err) 49 | } 50 | } 51 | 52 | #[derive(Debug, Error)] 53 | pub enum InnerError { 54 | #[error("IO error at '{path}': {source}")] 55 | /// A error produced during an IO operation. 56 | Io { 57 | /// The path in the directory tree where the IO error occured. 58 | path: PathBuf, 59 | /// The IO error. 60 | source: io::Error, 61 | }, 62 | } 63 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Ririsoft 2 | // Copyright 2024 Jordan Danford 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | //! An utility for walking through a directory asynchronously and recursively. 17 | //! 18 | //! Based on [async-fs](https://docs.rs/async-fs) and [blocking](https://docs.rs/blocking), 19 | //! it uses a thread pool to handle blocking IOs. Please refere to those crates for the rationale. 20 | //! This crate is compatible with any async runtime based on [futures 0.3](https://docs.rs/futures-core), 21 | //! which includes [tokio](https://docs.rs/tokio), [async-std](https://docs.rs/async-std) and [smol](https://docs.rs/smol). 22 | //! 23 | //! Symbolic links are walked through but they are not followed. 24 | //! 25 | //! # Example 26 | //! 27 | //! Recursively traverse a directory: 28 | //! 29 | //! ``` 30 | //! use async_walkdir::WalkDir; 31 | //! use futures_lite::future::block_on; 32 | //! use futures_lite::stream::StreamExt; 33 | //! 34 | //! block_on(async { 35 | //! let mut entries = WalkDir::new("my_directory"); 36 | //! loop { 37 | //! match entries.next().await { 38 | //! Some(Ok(entry)) => println!("file: {}", entry.path().display()), 39 | //! Some(Err(e)) => { 40 | //! eprintln!("error: {}", e); 41 | //! break; 42 | //! } 43 | //! None => break, 44 | //! } 45 | //! } 46 | //! }); 47 | //! ``` 48 | //! 49 | //! Do not recurse through directories whose name starts with '.': 50 | //! 51 | //! ``` 52 | //! use async_walkdir::{Filtering, WalkDir}; 53 | //! use futures_lite::future::block_on; 54 | //! use futures_lite::stream::StreamExt; 55 | //! 56 | //! block_on(async { 57 | //! let mut entries = WalkDir::new("my_directory").filter(|entry| async move { 58 | //! if let Some(true) = entry 59 | //! .path() 60 | //! .file_name() 61 | //! .map(|f| f.to_string_lossy().starts_with('.')) 62 | //! { 63 | //! return Filtering::IgnoreDir; 64 | //! } 65 | //! Filtering::Continue 66 | //! }); 67 | //! 68 | //! loop { 69 | //! match entries.next().await { 70 | //! Some(Ok(entry)) => println!("file: {}", entry.path().display()), 71 | //! Some(Err(e)) => { 72 | //! eprintln!("error: {}", e); 73 | //! break; 74 | //! } 75 | //! None => break, 76 | //! } 77 | //! } 78 | //! }); 79 | //! ``` 80 | 81 | #![forbid(unsafe_code)] 82 | #![deny(missing_docs)] 83 | 84 | mod error; 85 | 86 | use std::future::Future; 87 | use std::path::{Path, PathBuf}; 88 | use std::pin::Pin; 89 | use std::task::{Context, Poll}; 90 | 91 | use async_fs::{read_dir, ReadDir}; 92 | use futures_lite::future::Boxed as BoxedFut; 93 | use futures_lite::future::FutureExt; 94 | use futures_lite::stream::{self, Stream, StreamExt}; 95 | 96 | #[doc(no_inline)] 97 | pub use async_fs::DirEntry; 98 | 99 | pub use error::Error; 100 | use error::InnerError; 101 | 102 | /// A specialized [`Result`][`std::result::Result`] type. 103 | pub type Result = std::result::Result; 104 | 105 | type BoxStream = futures_lite::stream::Boxed>; 106 | 107 | /// A `Stream` of `DirEntry` generated from recursively traversing 108 | /// a directory. 109 | /// 110 | /// Entries are returned without a specific ordering. The top most root directory 111 | /// is not returned but child directories are. 112 | /// 113 | /// # Panics 114 | /// 115 | /// Panics if the directories depth overflows `usize`. 116 | pub struct WalkDir { 117 | root: PathBuf, 118 | entries: BoxStream, 119 | } 120 | 121 | /// Sets the filtering behavior. 122 | #[derive(Debug, PartialEq, Eq)] 123 | pub enum Filtering { 124 | /// Ignore the current entry. 125 | Ignore, 126 | /// Ignore the current entry and, if a directory, 127 | /// do not traverse its childs. 128 | IgnoreDir, 129 | /// Continue the normal processing. 130 | Continue, 131 | } 132 | 133 | impl WalkDir { 134 | /// Returns a new `Walkdir` starting at `root`. 135 | pub fn new(root: impl AsRef) -> Self { 136 | Self { 137 | root: root.as_ref().to_owned(), 138 | entries: walk_dir( 139 | root, 140 | None:: BoxedFut + Send>>, 141 | ), 142 | } 143 | } 144 | 145 | /// Filter entries. 146 | pub fn filter(self, f: F) -> Self 147 | where 148 | F: FnMut(DirEntry) -> Fut + Send + 'static, 149 | Fut: Future + Send, 150 | { 151 | let root = self.root.clone(); 152 | Self { 153 | root: self.root, 154 | entries: walk_dir(root, Some(f)), 155 | } 156 | } 157 | } 158 | 159 | impl Stream for WalkDir { 160 | type Item = Result; 161 | 162 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 163 | let entries = Pin::new(&mut self.entries); 164 | entries.poll_next(cx) 165 | } 166 | } 167 | 168 | fn walk_dir(root: impl AsRef, filter: Option) -> BoxStream 169 | where 170 | F: FnMut(DirEntry) -> Fut + Send + 'static, 171 | Fut: Future + Send, 172 | { 173 | stream::unfold( 174 | State::Start((root.as_ref().to_owned(), filter)), 175 | move |state| async move { 176 | match state { 177 | State::Start((root, filter)) => match read_dir(&root).await { 178 | Err(source) => Some(( 179 | Err(InnerError::Io { path: root, source }.into()), 180 | State::Done, 181 | )), 182 | Ok(rd) => walk(vec![(root, rd)], filter).await, 183 | }, 184 | State::Walk((dirs, filter)) => walk(dirs, filter).await, 185 | State::Done => None, 186 | } 187 | }, 188 | ) 189 | .boxed() 190 | } 191 | 192 | enum State { 193 | Start((PathBuf, Option)), 194 | Walk((Vec<(PathBuf, ReadDir)>, Option)), 195 | Done, 196 | } 197 | 198 | type UnfoldState = (Result, State); 199 | 200 | fn walk( 201 | mut dirs: Vec<(PathBuf, ReadDir)>, 202 | filter: Option, 203 | ) -> BoxedFut>> 204 | where 205 | F: FnMut(DirEntry) -> Fut + Send + 'static, 206 | Fut: Future + Send, 207 | { 208 | async move { 209 | if let Some((path, dir)) = dirs.last_mut() { 210 | match dir.next().await { 211 | Some(Ok(entry)) => walk_entry(entry, dirs, filter).await, 212 | Some(Err(source)) => Some(( 213 | Err(InnerError::Io { 214 | path: path.to_path_buf(), 215 | source, 216 | } 217 | .into()), 218 | State::Walk((dirs, filter)), 219 | )), 220 | None => { 221 | dirs.pop(); 222 | walk(dirs, filter).await 223 | } 224 | } 225 | } else { 226 | None 227 | } 228 | } 229 | .boxed() 230 | } 231 | 232 | fn walk_entry( 233 | entry: DirEntry, 234 | mut dirs: Vec<(PathBuf, ReadDir)>, 235 | mut filter: Option, 236 | ) -> BoxedFut>> 237 | where 238 | F: FnMut(DirEntry) -> Fut + Send + 'static, 239 | Fut: Future + Send, 240 | { 241 | async move { 242 | match entry.file_type().await { 243 | Err(source) => Some(( 244 | Err(InnerError::Io { 245 | path: entry.path(), 246 | source, 247 | } 248 | .into()), 249 | State::Walk((dirs, filter)), 250 | )), 251 | Ok(ft) => { 252 | let filtering = match filter.as_mut() { 253 | Some(filter) => filter(entry.clone()).await, 254 | None => Filtering::Continue, 255 | }; 256 | if ft.is_dir() { 257 | let path = entry.path(); 258 | let rd = match read_dir(&path).await { 259 | Err(source) => { 260 | return Some(( 261 | Err(InnerError::Io { path, source }.into()), 262 | State::Walk((dirs, filter)), 263 | )) 264 | } 265 | Ok(rd) => rd, 266 | }; 267 | if filtering != Filtering::IgnoreDir { 268 | dirs.push((path, rd)); 269 | } 270 | } 271 | match filtering { 272 | Filtering::Continue => Some((Ok(entry), State::Walk((dirs, filter)))), 273 | Filtering::IgnoreDir | Filtering::Ignore => walk(dirs, filter).await, 274 | } 275 | } 276 | } 277 | } 278 | .boxed() 279 | } 280 | 281 | #[cfg(test)] 282 | mod tests { 283 | use std::io::{ErrorKind, Result}; 284 | 285 | use futures_lite::future::block_on; 286 | use futures_lite::stream::StreamExt; 287 | 288 | use super::{Filtering, WalkDir}; 289 | 290 | #[test] 291 | fn walk_dir_empty() -> Result<()> { 292 | block_on(async { 293 | let root = tempfile::tempdir()?; 294 | let mut wd = WalkDir::new(root.path()); 295 | assert!(wd.next().await.is_none()); 296 | Ok(()) 297 | }) 298 | } 299 | 300 | #[test] 301 | fn walk_dir_not_exist() { 302 | block_on(async { 303 | let mut wd = WalkDir::new("foobar"); 304 | match wd.next().await.unwrap() { 305 | Err(e) => { 306 | assert_eq!(wd.root, e.path().unwrap()); 307 | assert_eq!(e.io().unwrap().kind(), ErrorKind::NotFound); 308 | assert_eq!(e.into_io().unwrap().kind(), ErrorKind::NotFound); 309 | } 310 | _ => panic!("want IO error"), 311 | } 312 | }) 313 | } 314 | 315 | #[test] 316 | fn into_io_error() { 317 | block_on(async { 318 | let mut wd = WalkDir::new("foobar"); 319 | match wd.next().await.unwrap() { 320 | Err(e) => { 321 | let e: std::io::Error = e.into(); 322 | assert_eq!(e.kind(), ErrorKind::NotFound); 323 | } 324 | _ => panic!("want IO error"), 325 | } 326 | }) 327 | } 328 | 329 | #[test] 330 | fn walk_dir_files() -> Result<()> { 331 | block_on(async { 332 | let root = tempfile::tempdir()?; 333 | let f1 = root.path().join("f1.txt"); 334 | let d1 = root.path().join("d1"); 335 | let f2 = d1.join("f2.txt"); 336 | let d2 = d1.join("d2"); 337 | let f3 = d2.join("f3.txt"); 338 | 339 | async_fs::create_dir_all(&d2).await?; 340 | async_fs::write(&f1, []).await?; 341 | async_fs::write(&f2, []).await?; 342 | async_fs::write(&f3, []).await?; 343 | 344 | let want = vec![ 345 | d1.to_owned(), 346 | d2.to_owned(), 347 | f3.to_owned(), 348 | f2.to_owned(), 349 | f1.to_owned(), 350 | ]; 351 | let mut wd = WalkDir::new(root.path()); 352 | 353 | let mut got = Vec::new(); 354 | while let Some(entry) = wd.next().await { 355 | let entry = entry.unwrap(); 356 | got.push(entry.path()); 357 | } 358 | got.sort(); 359 | assert_eq!(got, want); 360 | 361 | Ok(()) 362 | }) 363 | } 364 | 365 | #[test] 366 | fn filter_dirs() -> Result<()> { 367 | block_on(async { 368 | let root = tempfile::tempdir()?; 369 | let f1 = root.path().join("f1.txt"); 370 | let d1 = root.path().join("d1"); 371 | let f2 = d1.join("f2.txt"); 372 | let d2 = d1.join("d2"); 373 | let f3 = d2.join("f3.txt"); 374 | 375 | async_fs::create_dir_all(&d2).await?; 376 | async_fs::write(&f1, []).await?; 377 | async_fs::write(&f2, []).await?; 378 | async_fs::write(&f3, []).await?; 379 | 380 | let want = vec![f3.to_owned(), f2.to_owned(), f1.to_owned()]; 381 | 382 | let mut wd = WalkDir::new(root.path()).filter(|entry| async move { 383 | match entry.file_type().await { 384 | Ok(ft) if ft.is_dir() => Filtering::Ignore, 385 | _ => Filtering::Continue, 386 | } 387 | }); 388 | 389 | let mut got = Vec::new(); 390 | while let Some(entry) = wd.next().await { 391 | let entry = entry.unwrap(); 392 | got.push(entry.path()); 393 | } 394 | got.sort(); 395 | assert_eq!(got, want); 396 | 397 | Ok(()) 398 | }) 399 | } 400 | 401 | #[test] 402 | fn filter_dirs_no_traverse() -> Result<()> { 403 | block_on(async { 404 | let root = tempfile::tempdir()?; 405 | let f1 = root.path().join("f1.txt"); 406 | let d1 = root.path().join("d1"); 407 | let f2 = d1.join("f2.txt"); 408 | let d2 = d1.join("d2"); 409 | let f3 = d2.join("f3.txt"); 410 | 411 | async_fs::create_dir_all(&d2).await?; 412 | async_fs::write(&f1, []).await?; 413 | async_fs::write(&f2, []).await?; 414 | async_fs::write(&f3, []).await?; 415 | 416 | let want = vec![d1, f2.to_owned(), f1.to_owned()]; 417 | 418 | let mut wd = WalkDir::new(root.path()).filter(move |entry| { 419 | let d2 = d2.clone(); 420 | async move { 421 | if entry.path() == d2 { 422 | Filtering::IgnoreDir 423 | } else { 424 | Filtering::Continue 425 | } 426 | } 427 | }); 428 | 429 | let mut got = Vec::new(); 430 | while let Some(entry) = wd.next().await { 431 | let entry = entry.unwrap(); 432 | got.push(entry.path()); 433 | } 434 | got.sort(); 435 | assert_eq!(got, want); 436 | 437 | Ok(()) 438 | }) 439 | } 440 | } 441 | 442 | #[cfg(all(unix, test))] 443 | mod test_unix { 444 | use async_fs::unix::PermissionsExt; 445 | use std::io::Result; 446 | 447 | use futures_lite::future::block_on; 448 | use futures_lite::stream::StreamExt; 449 | 450 | use super::WalkDir; 451 | #[test] 452 | fn walk_dir_error_path() -> Result<()> { 453 | block_on(async { 454 | let root = tempfile::tempdir()?; 455 | let d1 = root.path().join("d1"); 456 | async_fs::create_dir_all(&d1).await?; 457 | let mut perms = async_fs::metadata(&d1).await?.permissions(); 458 | perms.set_mode(0o222); 459 | async_fs::set_permissions(&d1, perms).await?; 460 | let mut wd = WalkDir::new(&root); 461 | match wd.next().await.unwrap() { 462 | Err(e) => assert_eq!(e.path().unwrap(), d1.as_path()), 463 | _ => panic!("want IO error"), 464 | } 465 | Ok(()) 466 | }) 467 | } 468 | } 469 | --------------------------------------------------------------------------------