Select like rsync · Compress like 7-Zip · Access like a seekable stream
rsync-archive create -o game.7z --method zstd --exclude '*.tmp' --level 5 /data/game/
rsync-archive create -o logs.tar.zst --max-total-size 500M --newer-than 7d /var/log/
rsync-archive embed -o master.7z nest1.7z nest2.7zInstall · Quick start · Features · CLI reference · Formats · Selection · Docs
Packing large trees for backup, distribution, or incident response usually forces a trade-off:
| Traditional approach | Pain |
|---|---|
| Solid 7z / tar|zstd | Great ratio, no per-file random access |
Full 7-Zip GUI / 7zz a |
Strong archives, weak rsync-style selection |
rsync + archive later |
Great filters, two tools, two passes, temp disk |
| Solid streams on live trees | Mid-run vanish → whole job aborts |
rsync-archive unifies that workflow:
- Rsync-style include/exclude (and budgets) decide what goes in
- Streaming compress writes without copying the tree to temp
- Non-solid / seekable layouts keep file-level random access
- Embed nests finished archives into a master store 7z (Copy, no recompress)
Built in Rust, tested across Ubuntu and Rocky Linux, released as distro-matched x86_64 binaries.
| Version | v0.6.1 |
| Commands | create · embed |
| Default format | Non-solid 7z (per-file packs) |
| Also | Seekable-zstd · tar.zst · tar.lz4 |
| Codecs (7z) | LZMA2 · Zstd · LZ4 |
| Selection | rsync filters + dir/global size & count budgets |
| Safety | Unique OUT.partial.<pid> → fsync → atomic rename · soft-skip live-tree races · verify-before-commit |
| License | MIT |
| Capability | Details |
|---|---|
| Rsync-grade filters | --include / --exclude / --filter / --filter-from / *-from files; first-match-wins; basename rules (*.tmp); * / ** / ?; anchored /pat; dir-only pat/ |
| Master lists | SRC... walks, --files-from explicit lists, optional --include-cwd |
| Directory budgets | --dir-max-size PATH=SIZE, --dir-max-files PATH=N — recursive, newest-mtime-first, longest prefix wins |
| Global caps | --max-total-size, --max-files, --max-size, --min-size, --newer-than |
| Restriction lists | --file-size-from, --dir-max-size-from, --dir-max-files-from (rsync-like line syntax) |
| Dry-run | -n lists selected archive paths without writing |
| Parallel encode (7z) | Worker pool, ordered write, --threads, --encode-concurrency, --encode-size-budget (default 500M) |
| Live-tree resilience | Soft-skip vanished/unreadable members; 7z pack rollback; tar/seekable zero-pad + warn; --strict exit 1 on any degraded member |
| Verify | --verify checks the finished archive before commit — a failed verify keeps the previous -o |
| Safe output | Refuse overwrite without --force; unique .partial.<pid>.<n> temp + fsync + atomic rename; concurrent runs at the same -o never interleave (last commit wins) |
| Self-exclusion | The -o output and its partial temps are never archived — even when they sit inside a SRC tree or CWD |
| Capability | Details |
|---|---|
| Store / Copy 7z | Method 0x00 — wrap finished files as members |
| Naming | Basename by default; --keep-path + optional --prefix |
| Magic policy | Warn on non-7z · --require-7z hard-fail · --allow-any silent |
| Dry-run / verify / force | Same safety model as create |
| Format | Random access | Metadata | Symlinks / hard links | Infer from -o |
|---|---|---|---|---|
| 7z (default) | Per-file packs | mtime, empty flags | File bodies only | .7z / default |
| seekable-zstd | Byte-range + name index | Member names | File bodies only | .zst |
| tar-zstd | Seekable Zstd + RATAIDX1 |
mode/uid/gid, uname/gname, dirs | Full | .tar.zst / .tzst |
| tar-lz4 | Multi-frame LZ4 + RATLFRM1 + RATAIDX1 |
same as tar-zstd | Full | .tar.lz4 / .tlz4 |
Create compresses. Embed only stores.
Embed does not convert solid → non-solid (use archiveconverter for that).
- Non-solid only for create — every member independently accessible
- Disk-friendly — stream read / stream compress / append packs; no full-tree temp copy
- Selection first —
--dry-runbefore you spend CPU on compression - Fail soft on live trees — one rotated log should not kill a 100k-file pack
- Docs + tests required on every behavior change (
AGENTS.md)
Release assets for Ubuntu 22.04 / 24.04 and Rocky Linux 8 / 9 / 10 (x86_64, glibc-linked):
# Example: Ubuntu 24.04
curl -fsSL -o rsync-archive.tar.gz \
https://github.com/hilather/rsync-archive/releases/download/v0.6.1/rsync-archive-ubuntu24.04-x86_64.tar.gz
tar -xzf rsync-archive.tar.gz
./rsync-archive --version
# optional: install system-wide
sudo install -m755 rsync-archive /usr/local/bin/Each tarball contains a binary named rsync-archive (no OS suffix inside the archive).
SHA-256 sidecars are published next to every asset.
Release tags v* attach Rocky Linux 8 / 9 / 10 (x86_64) .rpm packages built on that distro (that glibc). Pick the major that matches the host — the filename carries rocky8 / rocky9 / rocky10; the NVR inside is rsync-archive-<version>-1.x86_64 on all three. The package installs /usr/bin/rsync-archive.
# Example: Rocky Linux 9
curl -fsSL -O https://github.com/hilather/rsync-archive/releases/download/v0.6.1/rsync-archive-0.6.1-1.rocky9.x86_64.rpm
sudo dnf install ./rsync-archive-*.rocky9.x86_64.rpm
# or: sudo rpm -i ./rsync-archive-0.6.1-1.rocky9.x86_64.rpm
rsync-archive --versionOn a Rocky/RHEL host with gcc + rustc:
PACKAGE_FAMILY=rpm DISTRO_LABEL=rocky9 SKIP_TARBALL=1 ./packaging/build-native-packages.sh
sudo dnf install ./dist/rsync-archive-*.rpmRequirements: Rust 1.85+ (locked deps include edition-2024 crates)
git clone https://github.com/hilather/rsync-archive.git
cd rsync-archive
cargo build --release
# binary: target/release/rsync-archive# Denser LZMA2 (liblzma) + LZ4HC for --method lz4 at levels ≥3
cargo build --release --features native-codecs| Feature | Effect |
|---|---|
liblzma |
C-backed LZMA2 via system or bundled xz |
liblzma-static |
Force static/bundled liblzma |
lz4-hc |
True LZ4HC for levels ≥3 (lz4 crate / vendored sources) |
native-codecs |
Convenience: liblzma + lz4-hc |
Default build uses pure-Rust lzma-rust2 + lz4_flex + libzstd bindings — no system liblzma required.
# Help
rsync-archive --help
rsync-archive create --help
rsync-archive embed --help
# Non-solid 7z (LZMA2 default)
rsync-archive create -o out.7z --exclude '*.tmp' --level 5 --verify ./src/
# Fast path — Zstd packs inside 7z
rsync-archive create -o out.7z --method zstd --level 5 ./data/
# Dry-run selection
rsync-archive create -o out.7z -n --exclude '*.tmp' ./src/
# Explicit file list
rsync-archive create -o out.7z --files-from list.txt
# Seekable Zstd stream (byte-range access)
rsync-archive create -o out.zst --format seekable-zstd --level 5 ./src/
rsync-archive create -o pack.zst --level 3 ./data/ # .zst → seekable-zstd
# RA-friendly tar containers
rsync-archive create -o out.tar.zst --format tar-zstd ./src/
rsync-archive create -o out.tar.lz4 --format tar-lz4 ./src/
# Log collection with budgets
rsync-archive create -o logs.7z \
--max-total-size 500M \
--max-files 1000 \
--max-size 50M \
--newer-than 7d \
/var/log/
# Embed finished archives under a master store 7z
rsync-archive embed -o master.7z nest1.7z nest2.7z
rsync-archive embed -o master.7z --force --verify nest1.7z nest2.7z
rsync-archive embed -o master.7z --allow-any a.bin b.binrsync-archive create -o game.7z \
--exclude '*.tmp' \
--exclude-from excludes.txt \
--filter '- cache/**' \
--method zstd --level 5 --verify \
/data/game/rsync-archive create -o o.7z --dir-max-size logs/=100M --dir-max-size cache=50M tree/
rsync-archive create -o o.7z --dir-max-files logs/=10 --dir-max-files-from limits.txt tree/
rsync-archive create -o o.7z \
--files-from master.txt \
--file-size-from sizes.txt \
--dir-max-size-from dirs.txtPer-file compressed packs → file-level random access.
--method |
Codec ID | Notes |
|---|---|---|
lzma2 (default) |
LZMA2 0x21 |
Best ratio among built-in options; slower |
zstd |
Zstd 04 F7 11 01 |
Best speed × ratio for many small files |
lz4 |
LZ4 04 F7 11 04 |
Fastest; levels 1–2 flex, ≥3 HC with lz4-hc |
- Empty files use empty flags; mtimes from source
- Parallel encode: fixed worker pool + 500M in-flight uncompressed budget
- Symlinks / hard-link members are skipped (first regular-file body for a hard-linked inode is kept)
Single seekable Zstd stream (zeekstd) with length-prefixed members and a trailer index (name → uncompressed offset).
- Infer from bare
-o *.zstwhen--formatomitted --methodis 7z-only (omit for this format)- Layout:
docs/FORMAT_SEEKABLE_ZSTD.md
Valid ustar/pax tar inside seekable Zstd + RATAIDX1 member index (path, size, mtime, mode, uid, gid; uname/gname in tar headers).
Includes:
- Parent directory members (
typeflag='5', trailing/) - Symbolic links (
typeflag='2') - Hard links (
typeflag='1', linkname = first archive path for the inode)
Infer from -o *.tar.zst or *.tzst.
Layout: docs/FORMAT_TAR_ZSTD.md
Same tar + index idea with independent LZ4 frames + cleartext RATLFRM1 frame table (not a standard “seekable LZ4” format).
Infer from -o *.tar.lz4 or *.tlz4.
Layout: docs/FORMAT_TAR_LZ4.md
| Output path ends with | Resolved format |
|---|---|
.tar.zst / .tzst |
tar-zstd |
.tar.lz4 / .tlz4 |
tar-lz4 |
.zst |
seekable-zstd |
.7z / other |
7z |
Full semantics: docs/SELECTION.md · rsync parity: docs/RSYNC_PARITY.md
| Rule | Behavior |
|---|---|
| First match wins | Ordered rule list |
| No match | Include (default) |
No / and no ** in pattern |
Basename match (*.tmp matches dir/a.tmp) |
Leading / |
Anchored from archive root |
Trailing / |
Directory-only (exclude prunes the tree) |
| Unanchored multi-segment | End-anchored suffix (foo/bar matches a/foo/bar) |
* / ? |
One path segment |
** |
Across segments; forces full-path mode |
[...] classes |
Not in the dialect — brackets match literally; a parse-time warning is printed |
\ in patterns |
Treated as / (Windows-style), not as a wildcard escape — parse-time warning |
| Filter file caps | 10 MiB or 1,000,000 lines per file |
Clap stores each flag type in its own list — heterogeneous flags are not interleaved by CLI position:
1. --include-from FILE…
2. --exclude-from FILE…
3. --filter-from FILE… ← preferred for full ordered lists
4. --filter RULE…
5. --include PATTERN… (all includes as a batch)
6. --exclude PATTERN… (all excludes as a batch)
Prefer --filter-from or repeated --filter for rsync-like ordered mixes.
Prune note (differs from rsync): rsync never scans an excluded directory, so an include for a path under it (
+ skipme/keep.txtbefore- skipme/) finds nothing. rsync-archive conservatively keeps walking when an include could match below, so that include does take effect. The selected set diverges from rsync only when an include rule precedes the excluding rule and targets a path under the excluded tree; when the exclude comes first, first-match still drops those paths (identical to rsync). Seedocs/RSYNC_PARITY.md(P2-5).
# Include-only idiom (batch order works)
rsync-archive create -o o.7z --include '*.c' --exclude '*' ./src/
# True ordered mix
rsync-archive create -o o.7z --filter '- *' --filter '+ *.c' ./src/
rsync-archive create -n -o o.7z --filter-from rules.txt ./src/Filter line syntax:
+ pattern
- pattern
include pattern
exclude pattern
# comments and blank lines ignored
| Source | Behavior |
|---|---|
SRC without trailing / |
Archive paths include the directory name (photos → photos/a.jpg) |
SRC/ with trailing / |
Strips the directory name (photos/ → a.jpg) |
SRC as a symlink |
Followed (rsync arg behavior); members keep the link's name |
--files-from |
Exclusive of SRC...; relative lines keep path; absolute lines use basename |
--files-from-skip-missing |
Soft-skip missing list lines (default: hard-fail) |
--include-cwd |
Pack CWD at archive root; ignores rsync filters |
| Output self-exclusion | The -o file and its .partial temps are excluded from every source (SRC walks, --files-from, --include-cwd) |
| Multi-SRC missing roots | ≥2 roots (or SRC + --include-cwd): soft-skip missing root; single missing SRC hard-fails |
1. Master collect SRC… / --files-from / --include-cwd
2. Rsync filters include · exclude · filter (-from)
3. Per-file global --max-size · --min-size · --newer-than
4. --file-size-from pattern max=SIZE (first match wins)
5. Dir budgets --dir-max-size[-from] then --dir-max-files[-from]
6. Global fill --max-total-size then --max-files (newest mtime first)
Restriction lists only affect matching paths; unlisted paths ignore that list.
--file-size-from
**/*.log max=100M
var/log/app.log max=10M
--dir-max-size-from
logs/ max=500M
cache/ max=1G files=50
logs/=100M # legacy PATH=SIZE
--dir-max-files-from
logs/=10
cache/ files=50
| Kind | tar-zstd / tar-lz4 | 7z / seekable-zstd |
|---|---|---|
| Regular file | Full body | Full body |
| Symlink | Stored as link | Skipped |
| Hard link (Unix) | Stored as hard link (size 0) | First body only |
| Special (fifo/device/…) | Skipped | Skipped |
Encode soft-skip: members that vanish between selection and open are omitted (counted as vanished); a source-side EIO mid-read drops just that member instead of aborting the run. 7z rolls back partial packs; tar/seekable re-stat after open and zero-pad short mid-reads with a warning (counted as padded). A file that grows past 0 after being selected as empty is archived with its real current content; a file that grows during a tar/seekable read is truncated at the committed size and noted as padded. If all members soft-skip, create errors unless --allow-empty.
--strict: the archive is still written, but create exits 1 when any selected member vanished or was padded at encode (rsync exit-23 style) — automation can distinguish a complete archive from a degraded one.
| Flag | Meaning |
|---|---|
-v / -vv |
Debug / trace logging on stderr (info → debug → trace) |
RUST_LOG |
Overrides verbosity filter |
Exit codes: 0 success · 1 operational error · 2 usage error
rsync-archive create -o OUT [OPTIONS] [SRC...]
| Flag | Default | Meaning |
|---|---|---|
-o, --output |
required | Output path |
--format / --output-format |
infer / 7z |
7z · seekable-zstd · tar-zstd · tar-lz4 |
-n, --dry-run |
off | List selection only; no write |
--force |
off | Overwrite existing -o |
--verify |
off | Verify finished archive before commit (failed verify keeps previous -o) |
--allow-empty |
off | Empty / all-vanished → exit 0, no -o |
--strict |
off | Archive still written, but exit 1 if any member vanished or was padded at encode |
--level |
5 |
Compression level 0–9 |
--method |
lzma2 |
7z only: lzma2 · zstd · lz4 |
--threads |
auto | 7z only: encode workers (many tiny files → 1, else CPUs) |
--encode-concurrency |
0 |
7z only: max concurrent encodes (0 = auto) |
--encode-size-budget |
500M |
7z only: max in-flight uncompressed size (0 = unlimited) |
| Flag | Default | Meaning |
|---|---|---|
SRC... |
— | Source paths (required unless --files-from or --include-cwd) |
--files-from |
— | Master collect list (exclusive of SRC...) |
--files-from-skip-missing |
off | Soft-skip missing/unreadable list lines |
--include-cwd |
off | Pack CWD at archive root; ignores rsync filters |
| Flag | Meaning |
|---|---|
--exclude / --include |
Rsync-style patterns (repeatable; batched) |
--exclude-from / --include-from |
Pattern files (repeatable) |
--filter-from |
Ordered +/- filter file (repeatable; preferred) |
--filter |
+ pattern / - pattern (repeatable; CLI order among filters) |
| Flag | Meaning |
|---|---|
--dir-max-size PATH=SIZE |
Cap selected bytes under dir tree (repeatable) |
--dir-max-size-from FILE |
Dir size/count list file |
--dir-max-files PATH=N |
Cap file count under dir tree (repeatable) |
--dir-max-files-from FILE |
Dir file-count list file |
--file-size-from FILE |
Per-path max size (PATTERN max=SIZE) |
--max-total-size SIZE |
Global selected-byte cap (newest-first) |
--max-files N |
Global max selected file count (newest-first) |
--max-size SIZE |
Skip any single file larger than SIZE |
--min-size SIZE |
Skip files smaller than SIZE (0 = off) |
--newer-than DURATION |
Keep mtime within window (7d, 24h, 30m, 90s) |
SIZE syntax: raw bytes or K / M / G suffixes (e.g. 100M, 1G).
DURATION syntax: integer + optional d / h / m / s.
rsync-archive embed -o OUT [OPTIONS] FILE...
| Flag | Default | Meaning |
|---|---|---|
-o, --output |
required | Master .7z path |
FILE... |
required | Inputs to embed (typically finished .7z) |
-n, --dry-run |
off | List planned member names only |
--force |
off | Overwrite existing -o |
--prefix PREFIX |
— | Prefix all member names |
--keep-path |
off | Keep path as name (default: basename) |
--require-7z |
off | Fail if missing 7z magic |
--allow-any |
off | Allow non-7z store blobs (silent) |
--verify |
off | Post-write test of master archive |
rsync-archive embed -o master.7z nest1.7z nest2.7z
rsync-archive embed -o master.7z --keep-path --prefix packs/ ./build/a.7z
rsync-archive embed -o master.7z --require-7z --verify a.7z b.7z
rsync-archive embed -o master.7z --allow-any --dry-run blob.binFair non-solid benchmarks vs stock 7zz / 7-Zip-zstd (host: 12-core, 2026-07-28). Full tables: docs/bench/RESULTS.md.
| Goal | Winner |
|---|---|
| Wall time (zstd / lz4, many small files) | rsync-archive — often 2–10× faster than 7zz-zstd |
| Wall time (lzma2 single-thread) | stock 7zz still ~1.2–1.4× ahead |
| Wall time (lzma2 multi-thread, small L5) | rsync-archive can beat 7zz |
| Archive size / ratio | Native tools denser (~1.3–1.9× smaller) |
Product picks:
| Priority | Recommendation |
|---|---|
| Speed | --method zstd |
| Best ratio we offer | --method lzma2 (+ optional --features native-codecs) |
| Max throughput | --method lz4 |
| Metadata + links | --format tar-zstd or tar-lz4 |
| Simple byte-range stream | --format seekable-zstd |
cargo build --release --bin rsync-archive --bin bench_compress
./target/release/bench_compress run --scale small --threads 1,4 --level 1,5 --methods allsrc/
main.rs, cli.rs, lib.rs, error.rs
archive/
sevenz/ # non-solid header, store writer, LZMA2/Zstd/LZ4 packs
seekable_zstd/ # seekable-zstd create + list/extract helpers
tar_zstd/ # RA tar.zst (seekable Zstd + RATAIDX1)
tar_lz4/ # RA tar.lz4 (multi-frame LZ4 + RATLFRM1 + RATAIDX1)
tar_common.rs # shared ustar/pax + index helpers
select/ # pathnorm, rules, matcher, walk, dir budgets, global restrict
pipeline/
create.rs # selection + multi-format write
embed.rs # store outer embed
output.rs # unique partial temps, --force, fsync + atomic rename, verify-before-commit
util/ # size parse, soft log, auto threads
docs/ # design, selection, format layouts, benches
tests/ # CLI smoke, e2e create/embed/formats, filter parity, soft-fail
packaging/ # nfpm Rocky 8/9/10 RPMs (packages.yml + release.yml)
| Stage | Status |
|---|---|
| Bootstrap, foundations, store writer | Done |
| Embed pipeline | Done |
| Rsync filter engine + walk + dry-run | Done |
| Non-solid LZMA2 create + verify | Done |
| Large-file streaming e2e | Done |
| Parallel encode (threads / concurrency / budget) | Done |
| Zstd + LZ4 in 7z | Done |
| Seekable-zstd / tar-zstd / tar-lz4 | Done |
| Dir size & file-count budgets | Done |
| Global log-collection limits | Done |
| Encode performance (OPT-01..14) | Done |
See docs/DESIGN.md and docs/BACKLOG.md for roadmap items (e.g. --older-than, head/tail sampling, free-disk guards).
| Workflow | Trigger | What |
|---|---|---|
| CI | push / PR to main |
Build on Ubuntu 22.04 / 24.04 and Rocky 8 / 9 / 10; full cargo test on Ubuntu 24.04 |
| Packages | PR touching packaging/ / release+packages workflows / Cargo.* |
Build + dnf install Rocky 8 / 9 / 10 .rpm artifacts (no GitHub Release) |
| Release | tag v* |
Per-distro .tar.gz + Rocky 8 / 9 / 10 .rpm + .sha256 assets |
# Maintainers: cut a release (Cargo.toml version must match the tag)
git tag -a v0.6.1 -m "v0.6.1"
git push origin v0.6.1| Document | Contents |
|---|---|
docs/DESIGN.md |
Full design, stages, decisions |
docs/SELECTION.md |
Filter & restriction source of truth (v1) |
docs/RSYNC_PARITY.md |
Parity matrix vs rsync |
docs/FORMAT_SEEKABLE_ZSTD.md |
Seekable-zstd on-disk layout |
docs/FORMAT_TAR_ZSTD.md |
tar.zst layout |
docs/FORMAT_TAR_LZ4.md |
tar.lz4 layout |
docs/BENCH.md |
Benchmark harness & fairness rules |
docs/bench/RESULTS.md |
Published numbers |
docs/BACKLOG.md |
Future work |
AGENTS.md |
Contributor / agent policy |
Every behavior-changing change must update docs and include regression tests in the same PR.
cargo test
cargo run -- --help
./scripts/v1_acceptance.sh # when present / for release checksLive rsync parity tests soft-skip if rsync is not installed (CI stays green).
| Project | Relationship |
|---|---|
| archiveconverter | Solid → non-solid conversion; outer store append sibling to embed |
| rsync | Selection semantics inspiration |
| 7-Zip / 7-Zip-zstd | Native non-solid peers for benchmarks |
MIT © hilather contributors
Select with confidence. Archive without thrashing the disk. Extract one file without unpacking the world.
