如何在 Rust 中递归复制文件夹



从我在文档中看到的内容来看,没有开箱即用的解决方案。

你可以使用我写的fs_extra箱。该板条箱扩展了标准库std::fsstd::io模块。

它可以(除其他外):

  • 复制文件(可选地包含有关进度的信息)。
  • 递归复制目录(可选地包含有关进度的信息)。

我发现的最简单的代码有效:

use std::{io, fs};
fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
    fs::create_dir_all(&dst)?;
    for entry in fs::read_dir(src)? {
        let entry = entry?;
        let ty = entry.file_type()?;
        if ty.is_dir() {
            copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
        } else {
            fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
        }
    }
    Ok(())
}
use std::fs;
use std::path::{Path, PathBuf};
pub fn copy<U: AsRef<Path>, V: AsRef<Path>>(from: U, to: V) -> Result<(), std::io::Error> {
    let mut stack = Vec::new();
    stack.push(PathBuf::from(from.as_ref()));
    let output_root = PathBuf::from(to.as_ref());
    let input_root = PathBuf::from(from.as_ref()).components().count();
    while let Some(working_path) = stack.pop() {
        println!("process: {:?}", &working_path);
        // Generate a relative path
        let src: PathBuf = working_path.components().skip(input_root).collect();
        // Create a destination if missing
        let dest = if src.components().count() == 0 {
            output_root.clone()
        } else {
            output_root.join(&src)
        };
        if fs::metadata(&dest).is_err() {
            println!(" mkdir: {:?}", dest);
            fs::create_dir_all(&dest)?;
        }
        for entry in fs::read_dir(working_path)? {
            let entry = entry?;
            let path = entry.path();
            if path.is_dir() {
                stack.push(path);
            } else {
                match path.file_name() {
                    Some(filename) => {
                        let dest_path = dest.join(filename);
                        println!("  copy: {:?} -> {:?}", &path, &dest_path);
                        fs::copy(&path, &dest_path)?;
                    }
                    None => {
                        println!("failed: {:?}", path);
                    }
                }
            }
        }
    }
    Ok(())
}

其他答案实际上并没有显示如何做到这一点,他们只是说你可以怎么做;这是一个具体的例子。

如其他答案所述,相关的 API 是 fs::create_dir_allfs::copyfs::metadata

没有"多合一"的标准库 API。

你可以

使用 std::fs::walk_dir 递归遍历目录结构,这会在Result中生成一个迭代器,对于每个Path,使用 PathExtensions 扩展特性提供的 is_file() 方法检查它是否是一个文件。如果是,则使用 std::io::fs::copy 实际复制文件。

fs_extra根本不适合我。此外,它有令人困惑的选项,整体质量令人怀疑(例如,他们认为 64000 字节是 64 kB)。

无论如何,有效的替代方案是copy_dir它包含一个函数来执行此操作,没有选项。它不会覆盖现有目录,但您可能可以很容易地修改代码以允许这样做。

一个程序复制目录内容的完整示例,使用 crate walkdir:

让我们从列出当前文件开始。

(注意:在Windows上tree /F列出所有文件和目录)

$ tree /F
│   Cargo.lock
│   Cargo.toml
│
├───in
│   │   b.txt
│   │
│   └───dir_a
│           a.txt
│
└───src
        main.rs

锈源

# File Cargo.toml
[package]
name = "plop"
version = "0.1.0"
edition = "2021"
[dependencies]
walkdir = "2" # latest version when writing this answer is 2.3
// File src/main.rs
use std::error::Error;
use std::fs;
use std::io;
use std::path::PathBuf;
use walkdir::WalkDir;
fn main() -> Result<(), Box<dyn Error>> {
    let in_dir = PathBuf::from("in");
    let out_dir = PathBuf::from("out");
    for entry in WalkDir::new(&in_dir) {
        let entry = entry?;
 
        let from = entry.path();
        let to = out_dir.join(from.strip_prefix(&in_dir)?);
        println!("tcopy {} => {}", from.display(), to.display());
        // create directories
        if entry.file_type().is_dir() {
            if let Err(e) = fs::create_dir(to) {
                match e.kind() {
                    io::ErrorKind::AlreadyExists => {}
                    _ => return Err(e.into()),
                }
            }
        }
        // copy files
        else if entry.file_type().is_file() {
            fs::copy(from, to)?;
        }
        // ignore the rest
        else {
            eprintln!("copy: ignored symlink {}", from.display());
        }
    }
    Ok(())
}

让我们构建并运行它

$ cargo run
   Compiling winapi v0.3.9
   Compiling winapi-util v0.1.5
   Compiling same-file v1.0.6
   Compiling walkdir v2.3.2
   Compiling plop v0.1.0 (C:UsersSamDevelopmentplop)
    Finished dev [unoptimized + debuginfo] target(s) in 6.33s
     Running `targetdebugplop.exe`
        copy in => out
        copy inb.txt => outb.txt
        copy indir_a => outdir_a
        copy indir_aa.txt => outdir_aa.txt

一切顺利,让我们再次检查文件和目录。

$ tree /F
│   Cargo.lock
│   Cargo.toml
│
├───in
│   │   b.txt
│   │
│   └───dir_a
│           a.txt
│
├───out
│   │   b.txt
│   │
│   └───dir_a
│           a.txt
│
├───src
│       main.rs
| 
... # more content we can ignore, like the 'target' build directory

我借用了道格的解决方案,但我认为人造丝是最好的床单,所以我用人造丝做了。这是快速而肮脏的,因为它不处理任何错误情况(线程只是崩溃而不是完成),但是如果您按原样使用它,您将知道何时发生这种情况,因为它不会隐藏它。如果需要,应该很容易添加自己的错误处理。我也是一个非常业余的程序员,我的代码不应该被任何有理智的人使用。我这样做的方式是,您可以使用 indicatif 板条箱非常轻松地添加一个漂亮的进度条。

当对大量小文件进行大文件夹的网络复制时,这在我的测试中可以很快完成工作。

没有使用特定于平台的位,因此它应该在 rust std 编译的任何地方运行。

use clap::Parser;
use rayon::Scope;
use std::fs::{self, DirEntry};
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use crossbeam::queue::SegQueue;
use rayon::iter::{ParallelIterator, IntoParallelRefIterator};
#[derive(Debug, Parser)]
#[clap(name = "mtcopy", about = "Multi-threaded copy...in rust!")]
struct Opt {
    /// Source directory
    source: String,
    /// Destination directory
    dest: String,
}
fn main() {
    let args = Opt::parse();
    copy(&args.source, &args.dest);
}
fn copy<U: AsRef<Path>, V: AsRef<Path>>(from: &U, to: &V) {
    let start = std::time::Instant::now();
    let source = PathBuf::from(from.as_ref());
    let dest = PathBuf::from(to.as_ref());
    let total_size = AtomicU64::new(0);
    // Could possibly use dashmap for the bag instead, slower but it can be used to avoid duplicate files from e.g. symlinks
    let bag = SegQueue::new();
    rayon::scope(|s| scan(&source, &bag, s, &total_size));
    // Convert to a vec so that we can rayon it and throw the bag away
    let files = bag.into_iter().collect::<Vec<_>>();
    let total_size = total_size.into_inner();
    files.par_iter().for_each(|entry| {
        let entry = entry.as_ref().unwrap();
        let spath = entry.path();
        let stem = spath.strip_prefix(&source).unwrap();
        let dpath = dest.join(&stem);
        let size = entry.metadata().unwrap().len();
        fs::create_dir_all(&dpath.parent().unwrap()).unwrap();
        // println!("  copy: {:?} -> {:?}", &path, &dpath);
        fs::copy(&spath, &dpath).unwrap();
    });
    println!("Copied {} files, {} bytes in {}",  files.len(), total_size, start.elapsed().as_secs());
}
fn scan<'a, U: AsRef<Path>>(src: &U, bag: &'a SegQueue<Result<DirEntry, std::io::Error>>, s: &Scope<'a>, total_size: &'a AtomicU64) {
    let dir = fs::read_dir(src).unwrap();
    dir.into_iter().for_each(|entry| {
        let info = entry.as_ref().unwrap();
        let path = info.path();
        if path.is_dir() {
            s.spawn(move |s| scan(&path, bag, s, total_size))
        } else {
            // println!("{}", path.as_os_str().to_string_lossy());
            let filelength = info.metadata().unwrap().len();
            total_size.fetch_add(filelength, Ordering::SeqCst);
            bag.push(entry)
        }
    })
}

最新更新