| Crates.io | parcopy |
| lib.rs | parcopy |
| version | 0.1.0 |
| created_at | 2026-01-24 15:39:40.494025+00 |
| updated_at | 2026-01-24 15:39:40.494025+00 |
| description | Parallel, atomic, and safe file/directory copying for Rust |
| homepage | |
| repository | https://github.com/lucifer1004/parcopy |
| max_upload_size | |
| id | 2066885 |
| size | 150,942 |
Parallel, atomic, and safe file/directory copying for Rust.
A production-grade library for copying files and directories with safety guarantees that go beyond the standard library.
persist_noclobber to prevent race conditionsUpdateNewer)| Feature | std::fs |
fs_extra |
parcopy |
|---|---|---|---|
| Parallel | ❌ | ❌ | ✅ |
| Atomic writes | ❌ | ❌ | ✅ |
| TOCTOU safe | ❌ | ❌ | ✅ |
| Incremental copy | ❌ | ❌ | ✅ |
| Reflink/CoW | ❌ | ❌ | ✅ |
| Timestamp preservation | ❌ | ❌ | ✅ |
| Progress callbacks | ❌ | ✅ | ✅ |
[dependencies]
parcopy = "0.1"
[dependencies]
parcopy = { version = "0.1", features = ["progress", "reflink"] }
| Feature | Description |
|---|---|
progress |
Progress bar support with indicatif |
reflink |
Copy-on-write support for btrfs/XFS/APFS |
tracing |
Structured logging with tracing crate |
serde |
Serialize/Deserialize for CopyOptions |
full |
Enable all optional features |
The easiest way to use parcopy is with the CopyBuilder:
use parcopy::CopyBuilder;
// Simple copy with smart defaults
let stats = CopyBuilder::new("src", "dst").run()?;
println!("Copied {} files ({} bytes)", stats.files_copied, stats.bytes_copied);
Only copy files that have changed:
use parcopy::CopyBuilder;
let stats = CopyBuilder::new("project", "backup")
.update_newer()
.run()?;
println!("Updated {} files, {} already up-to-date",
stats.files_copied, stats.files_skipped);
Optimize for NFS or network filesystems:
use parcopy::CopyBuilder;
let stats = CopyBuilder::new("data", "backup")
.parallel(32) // More threads for NFS
.overwrite() // Replace existing files
.no_fsync() // Skip fsync for speed
.run()?;
Copy untrusted directories safely:
use parcopy::CopyBuilder;
let stats = CopyBuilder::new("untrusted_upload", "safe_location")
.block_escaping_symlinks() // Block symlinks with "../"
.max_depth(10) // Limit directory depth
.run()?;
For more control, use the function API with CopyOptions:
use parcopy::{copy_dir, CopyOptions, OnConflict};
use std::path::Path;
let options = CopyOptions::default()
.with_parallel(8)
.with_on_conflict(OnConflict::Overwrite)
.with_max_depth(100)
.without_fsync();
let stats = copy_dir(Path::new("src"), Path::new("dst"), &options)?;
| Option | Default | Description |
|---|---|---|
parallel |
16 | Number of concurrent copy operations |
on_conflict |
Skip |
How to handle existing files |
fsync |
true |
Sync data to disk after each file |
preserve_permissions |
true |
Copy file permissions |
preserve_timestamps |
true |
Copy file timestamps |
max_depth |
None |
Maximum directory depth |
block_escaping_symlinks |
false |
Block symlinks with .. |
| Strategy | Description |
|---|---|
OnConflict::Skip |
Skip files that already exist (default) |
OnConflict::Overwrite |
Replace existing files |
OnConflict::UpdateNewer |
Only copy if source is newer |
OnConflict::Error |
Return error if file exists |
All copy operations return CopyStats:
use parcopy::CopyBuilder;
let stats = CopyBuilder::new("src", "dst").run()?;
println!("Files copied: {}", stats.files_copied);
println!("Files skipped: {}", stats.files_skipped);
println!("Symlinks: {}", stats.symlinks_copied);
println!("Directories: {}", stats.dirs_created);
println!("Bytes copied: {}", stats.bytes_copied);
println!("Duration: {:?}", stats.duration);
Files are written to a temporary file in the destination directory, then renamed atomically:
fsync: true, data survives crashesUses renameat2(RENAME_NOREPLACE) on Linux to atomically fail if the destination was created between our existence check and the rename.
../) are warned or blockedThis crate is optimized for NFS and network filesystems where many small files cause metadata storms. By parallelizing operations, multiple NFS RPCs can be in-flight simultaneously.
// For slow NFS, reduce parallelism to avoid overwhelming the server
let stats = CopyBuilder::new("src", "dst")
.parallel(4)
.run()?;
For local SSDs, parallelism helps less but doesn't hurt:
// Default parallelism (16) works well for local storage too
let stats = CopyBuilder::new("src", "dst").run()?;
For large files, the reflink feature provides instant copy-on-write on supported filesystems (btrfs, XFS, APFS):
[dependencies]
parcopy = { version = "0.1", features = ["reflink"] }
A CLI tool pcp is available in the cli directory:
# Install
cargo install --path cli
# Usage
pcp -r src/ dst/ # Recursive copy
pcp --update-newer src/ dst/ # Incremental copy
pcp -j 8 src/ dst/ # 8 parallel threads
Licensed under either of:
at your option.
Contributions are welcome! Please feel free to submit a Pull Request.