从我在文档中看到的内容来看,没有开箱即用的解决方案。
你可以使用我写的fs_extra箱。该板条箱扩展了标准库std::fs
和std::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_all
、 fs::copy
和 fs::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)
}
})
}