如何异步浏览目录及其子目录

时间:2019-06-22 16:21:24

标签: asynchronous rust filesystems rust-tokio

我需要研究目录及其所有子目录。我可以通过同步方式轻松地通过递归浏览目录:

use failure::Error;
use std::fs;
use std::path::Path;

fn main() -> Result<(), Error> {
    visit(Path::new("."))
}

fn visit(path: &Path) -> Result<(), Error> {
    for e in fs::read_dir(path)? {
        let e = e?;
        let path = e.path();
        if path.is_dir() {
            visit(&path)?;
        } else if path.is_file() {
            println!("File: {:?}", path);
        }
    }
    Ok(())
}

但是当我尝试使用tokio_fs执行相同的异步操作时:

use failure::Error;
use futures::Future;
use std::path::PathBuf;
use tokio::fs;
use tokio::prelude::*;

fn visit(path: PathBuf) -> impl Future<Item = (), Error = Error> {
    let task = fs::read_dir(path)
        .flatten_stream()
        .for_each(|entry| {
            println!("{:?}", entry.path());
            let path = entry.path();
            if path.is_dir() {
                let task = visit(entry.path());
                tokio::spawn(task.map_err(drop));
            }
            future::ok(())
        })
        .map_err(Error::from);

    task
}

我收到以下错误:

   Compiling playground v0.0.1 (/playground)
error[E0391]: cycle detected when processing `visit::{{opaque}}#0`
 --> src/main.rs:9:28
  |
9 | fn visit(path: PathBuf) -> impl Future<Item = (), Error = Error> {
  |                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
note: ...which requires processing `visit`...
 --> src/main.rs:9:1
  |
9 | fn visit(path: PathBuf) -> impl Future<Item = (), Error = Error> {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  = note: ...which requires evaluating trait selection obligation `futures::future::map_err::MapErr<impl futures::future::Future, fn(failure::error::Error) {std::mem::drop::<failure::error::Error>}>: std::marker::Send`...
  = note: ...which again requires processing `visit::{{opaque}}#0`, completing the cycle
note: cycle used when checking item types in top-level module
 --> src/main.rs:1:1
  |
1 | / use failure::Error;
2 | | use futures::Future;
3 | | use std::path::PathBuf;
4 | | use tokio::fs;
... |
23| |     task
24| | }
  | |_^

Link to Playground

异步浏览目录及其子目录(传播所有错误)的正确方法是什么?

2 个答案:

答案 0 :(得分:2)

您的代码有两个错误:

首先,返回impl Trait的函数当前无法递归,因为返回的实际类型将取决于自身。

因此,要使您的示例生效,您需要返回一个大小类型。最明显的候选者是特征对象,即Box<Future<...>>

fn visit(path: PathBuf) -> Box<Future<Item = (), Error = Error>> {
    ...
            let task = visit(entry.path());
            tokio::spawn(task.map_err(drop));
    ...

    Box::new(task)
}

还有第二个错误:

dyn futures::future::Future<...>` cannot be sent between threads safely
   |
17 |                 tokio::spawn(task.map_err(drop));
   |                 ^^^^^^^^^^^^ `dyn futures::future::Future<...>` cannot be sent between threads safely
   |
   = help: the trait `std::marker::Send` is not implemented for `dyn futures::future::Future<...>`
   = note: required because of the requirements on the impl of `std::marker::Send` for `std::ptr::Unique<dyn futures::future::Future<...>`
   = note: required because it appears within the type `std::boxed::Box<dyn futures::future::Future<...>`
   = note: required because it appears within the type `futures::future::map_err::MapErr<...>`
   = note: required by `tokio::executor::spawn`

这意味着您的特征对象不是Send,因此无法计划使用tokio::spawn()在另一个线程中执行它。幸运的是,这很容易解决:只需将+ Send添加到特征对象中即可:

fn visit(path: PathBuf) -> Box<Future<Item = (), Error = Error> + Send> {
    ...
}

Playground中查看完整代码。

答案 1 :(得分:2)

我将对rodrigo's existing answer进行一些修改:

  1. 从函数中返回Stream,使调用者可以通过给定的文件条目执行所需的操作。
  2. 返回impl Stream而不是Box<dyn Stream>。这为实现中的更大灵活性留出了空间。例如,可以创建一个使用内部堆栈而不是效率较低的递归类型的自定义类型。
  3. 从函数中返回io::Error,以允许用户处理任何错误。
  4. 接受impl Into<PathBuf>以使用更好的API。
  5. 创建一个内部隐藏的实现函数,该函数在其API中使用具体类型。

未来0.3 /东京0.2

在此版本中,我避免了深度递归调用,而是在本地保留要访问的路径栈(to_visit

use futures::{stream, Stream, StreamExt}; // 0.3.1
use std::{io, path::PathBuf};
use tokio::fs::{self, DirEntry}; // 0.2.4

fn visit(path: impl Into<PathBuf>) -> impl Stream<Item = io::Result<DirEntry>> + Send + 'static {
    async fn one_level(path: PathBuf, to_visit: &mut Vec<PathBuf>) -> io::Result<Vec<DirEntry>> {
        let mut dir = fs::read_dir(path).await?;
        let mut files = Vec::new();

        while let Some(child) = dir.next_entry().await? {
            if child.metadata().await?.is_dir() {
                to_visit.push(child.path());
            } else {
                files.push(child)
            }
        }

        Ok(files)
    }

    stream::unfold(vec![path.into()], |mut to_visit| {
        async {
            let path = to_visit.pop()?;
            let file_stream = match one_level(path, &mut to_visit).await {
                Ok(files) => stream::iter(files).map(Ok).left_stream(),
                Err(e) => stream::once(async { Err(e) }).right_stream(),
            };

            Some((file_stream, to_visit))
        }
    })
    .flatten()
}

#[tokio::main]
async fn main() {
    let root_path = std::env::args().nth(1).expect("One argument required");
    let paths = visit(root_path);

    paths
        .for_each(|entry| {
            async {
                match entry {
                    Ok(entry) => println!("visiting {:?}", entry),
                    Err(e) => eprintln!("encountered an error: {}", e),
                }
            }
        })
        .await;
}

期货0.1 /东京0.1

use std::path::PathBuf;
use tokio::{fs, prelude::*}; // 0.1.22
use tokio_fs::DirEntry; // 1.0.6

fn visit(
    path: impl Into<PathBuf>,
) -> impl Stream<Item = DirEntry, Error = std::io::Error> + Send + 'static {
    fn visit_inner(
        path: PathBuf,
    ) -> Box<dyn Stream<Item = DirEntry, Error = std::io::Error> + Send + 'static> {
        Box::new({
            fs::read_dir(path)
                .flatten_stream()
                .map(|entry| {
                    let path = entry.path();
                    if path.is_dir() {
                        // Optionally include `entry` if you want to
                        // include directories in the resulting
                        // stream.
                        visit_inner(path)
                    } else {
                        Box::new(stream::once(Ok(entry)))
                    }
                })
                .flatten()
        })
    }

    visit_inner(path.into())
}

fn main() {
    tokio::run({
        let root_path = std::env::args().nth(1).expect("One argument required");
        let paths = visit(root_path);

        paths
            .then(|entry| {
                match entry {
                    Ok(entry) => println!("visiting {:?}", entry),
                    Err(e) => eprintln!("encountered an error: {}", e),
                };

                Ok(())
            })
            .for_each(|_| Ok(()))
    });
}

另请参阅: