file-engine
Async, cross-platform file operations engine for desktop apps and developer tools: copy, move, sync, watch, and compress files, with progress reporting and cooperative cancellation built in from the start. Small-file operations are automatically batched to avoid overloading the OS with per-file syscalls, without any configuration required.
Not tied to any specific application — a standalone crate consumable by any
Rust project (desktop apps, CLIs, Tauri backends, etc.), built on tokio.
Quickstart
use FileEngine;
async
Every operation follows the same builder pattern: a chainable builder
configures the operation, .start() spawns it as a background task and
returns a handle immediately, and the handle exposes a Progress stream
plus cooperative cancellation via .cancel().
Operations at a glance
| Method | Feature | Purpose | Key options | Returns |
|---|---|---|---|---|
.copy(source, dest) |
operations |
Copy a file or directory tree | .overwrite(), .skip_if_identical()¹, .preserve_permissions()², .allow_filesystem_integrity_risk(), .small_file_threshold(), .max_bytes_per_batch(), .max_files_per_batch(), .batch_sort_order(), .on_error(), .batch_concurrency() |
Handle<OperationOutcome> |
.move_path(source, dest) |
operations |
Move a file or directory tree — atomic rename, falls back to copy-then-delete across filesystems | same as .copy() |
Handle<OperationOutcome> |
.move_many(sources, dest_dir) |
operations |
Move several independent sources into one destination directory as a single batched operation | same as .copy() |
Handle<OperationOutcome> (adds sources_failed) |
.sync(source, dest) |
sync |
One-way mirror: copy new/changed entries, delete destination-only orphans | .overwrite() (defaults true), .diff_strategy()¹, plus the shared batching options above |
Handle<SyncOutcome> |
.analyze(path) |
analyze |
Read-only tree inspection — counts, sizes, largest files, extension/age breakdowns | .extensions(), .exclude_globs(), .min_size() / .max_size(), .modified_after() / .modified_before(), .max_depth(), .follow_symlinks(), .detect_mime_types(), .detect_duplicates()¹, .top_n_largest(), .on_error() |
AnalysisHandle → AnalysisReport |
.compress(source, dest) |
compress |
Zip or gzip a file or directory | .format(), .small_file_threshold(), .on_error(), .batch_concurrency() |
Handle<OperationOutcome> |
.remove(path) |
remove |
Criteria-based delete (extension, size, modified-time, glob) — trashes and dry-runs by default | .extensions(), .exclude(), .min_size() / .max_size(), .modified_after() / .modified_before(), .max_depth(), .follow_symlinks(), .dry_run(), .hard_delete(), .allow_unfiltered_delete(), .on_error(), .batch_concurrency() |
Handle<RemoveOutcome> |
.watch(path) |
watch |
Stream filesystem change events | .recursive() |
WatchHandle |
¹ requires the checksum feature. ² Unix only, requires the permissions
feature. Full option reference, including which defaults differ and why:
docs/guide/operations.md.
Feed that stream to an EtaEstimator for a predicted time remaining. It
models batched small files (cost per file), streamed large files (cost
per byte), and the directory pre-pass (cost per directory) separately,
because a single bytes-per-second figure describes none of them well.
See docs/guide/progress-and-cancellation.md.
FileEngine::analyze() inspects a path without touching it — file/
directory counts, total size, largest files, and extension/age
breakdowns, narrowed with filters (extension, glob excludes, size range,
modified-time range, depth). With the checksum feature, it can also
group files by content hash to surface duplicates:
let report = engine.analyze.detect_duplicates.start?.await?;
println!;
Copying across filesystems (e.g. onto a FAT32/exFAT drive) is checked for
several failure modes up front — case-insensitive-destination collisions,
Windows-reserved filenames, destination file-size limits, and a known
exFAT-on-macOS write-integrity risk — rather than failing unpredictably
partway through or silently losing data. See
docs/guide/filesystem-safety.md.
FileEngine::move_many(sources, dest_dir) moves several independent
sources into one destination directory as a single batched operation —
one shared ErrorStrategy, concurrency pool, and progress stream across
all of them, rather than one .move_path() call per source. Each source
keeps its own basename under dest_dir.
With the checksum feature, .skip_if_identical(true) on .copy(),
.move_path(), and .move_many() compares content instead of failing
outright when the destination already exists: an identical destination
is left in place (for a move, the now-redundant source is still
removed), while a genuinely different one still fails with
Error::DestExists for the caller to decide what to do.
engine.move_path.skip_if_identical.start?.await?;
FileEngine::remove() deletes files matching criteria (extension, size,
modified-time, exclude globs) instead of a whole path outright. It
defaults to a dry run (.dry_run(false) to actually remove) and to
trashing matches rather than unlinking them (.hard_delete(true) to
unlink permanently), and refuses to run with no criteria set at all
unless .allow_unfiltered_delete(true):
let outcome = engine.remove.extensions.max_size.dry_run.start?.await?;
println!;
Features
Only pay for what you use — the public surface grows and shrinks via Cargo feature flags.
| Feature | Enables | Notes |
|---|---|---|
operations (default) |
copy, move_path, move_many |
Also pulls in filesystem-capability detection (used by copy/move/sync). |
sync |
FileEngine::sync() |
Implies operations. |
checksum |
DiffStrategy::Checksum for sync; .detect_duplicates() for analyze; .skip_if_identical() for copy/move_path/move_many |
Content-hash (blake3) comparison/grouping instead of size+mtime. |
watch |
FileEngine::watch() |
Does not require operations — watching doesn't use the batching pipeline. |
compress |
FileEngine::compress() |
Zip or gzip, inferred from the destination extension or set explicitly via CompressFormat. No decompress support yet. |
permissions |
.preserve_permissions() on copy/move/sync |
Unix only. Mode bits, not ownership. |
analyze (default) |
FileEngine::analyze() |
Read-only tree inspection — counts, total size, largest files, extension/age breakdowns, with filters. checksum additionally enables duplicate detection. |
remove |
FileEngine::remove() |
Criteria-based delete (extension, size, modified-time, exclude globs). Implies operations. Trashes by default; dry-runs by default. |
diagnostics |
— | Reserved for error-engine message-catalog integration; not yet implemented. |
Documentation
docs/guide/— using the crate: quickstart per operation, the full builder option reference, progress/cancellation, error handling, and the filesystem-safety behavior above in detail.docs/contributing/— working on the crate: architecture, conventions for adding a feature, and this project's testing discipline.
License
Licensed under the MIT license.