feat(p7-cli-namespace): rename colliding short flags to rsync parity (Wave A)
-c -> --checksum, -m -> --prune-empty-dirs, -M -> --remote-option, -f -> --filter, -s -> --secluded-args, -p -> --perms, -T -> --temp-dir; -a/--archive is now real rsync -rlptgoD (links+metadata+devices+specials). FastSync's own flags moved to long-form-only or new shorts: -j/--threads (multithreading), --preserve (metadata), --sendfile, --chunk-serialization, --timeout, --ssh-port. Client-side only; the wire config fields are unchanged (no PROTOCOL_VERSION bump). The server keeps -p as its port. Docs (README, RSYNC_COMPAT summary 129->132) and unit/integration tests updated. 37/37 unit, 400-pass integration.
This commit is contained in:
@@ -88,16 +88,18 @@ partial, alternate, and planned behavior.
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| Positional | `<source> <dest>` — automatic SSH detection if dest contains `:` |
|
||||
| `-c [level]` | Compression with optional level (1–22, default 5) |
|
||||
| `-z [level]` | Alias for `-c` |
|
||||
| `-a, --archive` | Archive mode: enables `-c -m -M` (no `-s`) |
|
||||
| `-m` | Multithreading mode |
|
||||
| `-s` | Chunk serialization (batch all files per chunk) |
|
||||
| `--secluded-args` | Accepted as an rsync compatibility option with no effect; `-s` remains chunk serialization. |
|
||||
| `-f, --sendfile` | Sendfile zero-copy. Incompatible with `-c` / `-s`. TCP only. |
|
||||
| `-M, --preserve` | Preserve supported file metadata (mode and mtime; ownership and atime are unsupported) |
|
||||
| `-c, --checksum` | Verify content by checksum instead of size+mtime |
|
||||
| `-z, --compress [level]` | Enable streaming zstd compression (level 1–22, default 5) |
|
||||
| `-a, --archive` | rsync archive mode (`-rlptgoD`): links, metadata, devices and specials (not compression/multithreading) |
|
||||
| `-j, --threads` | Multithreading mode |
|
||||
| `-m` | rsync `--prune-empty-dirs` (short form now rsync-parity) |
|
||||
| `--chunk-serialization` | Chunk serialization (batch all files per chunk; long form only) |
|
||||
| `-s` | rsync `--secluded-args` compatibility no-op (remote SSH argv is already injection-safe) |
|
||||
| `--sendfile` | Sendfile zero-copy. Incompatible with compression / chunk serialization. TCP only. Long form only. |
|
||||
| `--preserve` | Preserve supported file metadata (mode and mtime; ownership and atime are unsupported) |
|
||||
| `-n, --dry-run` | Scan and print what would be transferred |
|
||||
| `-p <port>` | SSH port (default: 22) |
|
||||
| `-p, --perms` | Preserve permission bits (part of the metadata bundle) |
|
||||
| `--ssh-port <port>` | SSH port (default: 22) |
|
||||
| `-v, --verbose` | Enable debug logging |
|
||||
| `-q, --quiet` | Suppress non-error output |
|
||||
| `--progress` | Show real-time transfer speed |
|
||||
@@ -113,7 +115,7 @@ partial, alternate, and planned behavior.
|
||||
| `--max-size <n>` | Skip files larger than n bytes |
|
||||
| `--min-size <n>` | Skip files smaller than n bytes |
|
||||
| `--max-alloc <SIZE>` | Maximum single allocation (binary units: B, K, M, G, T, P, E; default 1G) |
|
||||
| `--incremental` | Skip files unchanged since last transfer (size + mtime). Auto-enables `--preserve`. Incompatible with `-s`. |
|
||||
| `--incremental` | Skip files unchanged since last transfer (size + mtime). Auto-enables `--preserve`. Incompatible with `--chunk-serialization`. |
|
||||
| `--existing` | Skip files not already present at the destination; update existing files normally. |
|
||||
| `--bwlimit <KB/s>` | Bandwidth limit in kilobytes per second |
|
||||
| `--chunk-size <n>` | Chunk size in bytes (default: 10485760) |
|
||||
@@ -350,16 +352,16 @@ features without changing the meaning of ordinary compatibility options.
|
||||
|
||||
| Option | Purpose |
|
||||
|---|---|
|
||||
| `-m` | Enable the multithreaded scanner/loader/sender pipeline. |
|
||||
| `-c [level]`, `-z [level]` | Enable streaming zstd compression, levels 1-22. |
|
||||
| `-j`, `--threads` | Enable the multithreaded scanner/loader/sender pipeline. |
|
||||
| `-z [level]`, `--compress [level]` | Enable streaming zstd compression, levels 1-22. |
|
||||
| `--compress-level <n>` | Set the zstd compression level. |
|
||||
| `--zc <alg>` | Alias for `--compress-choice`. FastSync supports `zstd` and `none`. |
|
||||
| `--zl <n>` | Alias for `--compress-level`. |
|
||||
| `--skip-compress <list>` | Skip compression for comma-separated suffixes; incompatible with `-s`. |
|
||||
| `--skip-compress <list>` | Skip compression for comma-separated suffixes; incompatible with `--chunk-serialization`. |
|
||||
| `--compress-threads <n>` | Use `n` zstd compression workers. Requires compression and a zstd build with threaded support; the setting affects sender CPU work only. |
|
||||
| `--chunk-size <bytes>` | Set the transfer chunk size. |
|
||||
| `-s` | Enable FastSync chunk serialization. |
|
||||
| `-f`, `--sendfile` | Use TCP `sendfile()` zero-copy transfer. Incompatible with compression and chunk serialization. |
|
||||
| `--chunk-serialization` | Enable FastSync chunk serialization (long form only; `-s` is rsync's `--secluded-args`). |
|
||||
| `--sendfile` | Use TCP `sendfile()` zero-copy transfer. Incompatible with compression and chunk serialization. Long form only. |
|
||||
| `--delta` | Use FastSync-native block delta transfer. Requires `--incremental`. |
|
||||
| `--delta-block <bytes>` | Set the FastSync delta block size. |
|
||||
| `--delta-max <bytes>` | Limit files eligible for FastSync delta transfer. |
|
||||
@@ -372,15 +374,18 @@ features without changing the meaning of ordinary compatibility options.
|
||||
| `--timeout <seconds>` | Set I/O timeout. |
|
||||
| `--contimeout <seconds>` | Set connection timeout. |
|
||||
|
||||
Current short-option conflicts are tracked as compatibility work. In
|
||||
particular, FastSync currently uses `-p` for SSH port, `-s` for chunk
|
||||
serialization, and `-S` for sparse handling. These meanings must be reconciled
|
||||
before FastSync can claim full rsync CLI compatibility.
|
||||
Short-option conflicts with rsync have been resolved for the CLI namespace
|
||||
(Phase 7): `-c` is now rsync's `--checksum`, `-m` is `--prune-empty-dirs`, `-M`
|
||||
is `--remote-option`, `-f` is `--filter`, `-s` is `--secluded-args`, `-p` is
|
||||
`--perms`, and `-T` is `--temp-dir`. FastSync's own flags were renamed to
|
||||
long-form-only or new shorts: multithreading is `-j`/`--threads`, metadata
|
||||
is `--preserve`, sendfile is `--sendfile`, chunk serialization is
|
||||
`--chunk-serialization`, timeout is `--timeout`, and SSH port is `--ssh-port`.
|
||||
`-a`/`--archive` is now real rsync archive (`-rlptgoD`).
|
||||
|
||||
`--secluded-args` is accepted as a long-form compatibility no-op. It does not
|
||||
change FastSync's transport or protocol behavior. The rsync short form `-s` is
|
||||
intentionally not aliased because it remains FastSync's chunk-serialization
|
||||
option.
|
||||
`--secluded-args` (and its short form `-s`) is accepted as a compatibility
|
||||
no-op. It does not change FastSync's transport or protocol behavior, because
|
||||
remote SSH argv is already built injection-safe.
|
||||
|
||||
## Client Options
|
||||
|
||||
@@ -388,7 +393,7 @@ option.
|
||||
|
||||
| Option | Description |
|
||||
|---|---|
|
||||
| `-a`, `--archive` | Enable current archive preset. Full rsync archive semantics are planned. |
|
||||
| `-a`, `--archive` | rsync archive mode (`-rlptgoD`): links, metadata, devices and specials. |
|
||||
| `-n`, `--dry-run` | Scan and report without writing files. |
|
||||
| `--delete` | Request removal of destination entries absent from the source. The server must allow deletion. Default timing is delete-after: extras are removed only after the whole transfer succeeded. |
|
||||
| `--delete-before` | Delete extras before the transfer starts (implies `--delete`). |
|
||||
@@ -417,7 +422,7 @@ use with `--partial`. |
|
||||
|
||||
| Option | Description |
|
||||
|---|---|
|
||||
| `-M`, `--preserve` | Preserve supported file metadata, currently mode and modification time. |
|
||||
| `--preserve` | Preserve supported file metadata, currently mode and modification time (long form only). |
|
||||
| `-l`, `--links` | Request symlink preservation;
|
||||
link-target transfer remains incomplete. |
|
||||
| `--copy-links` | Copy symlink referents. |
|
||||
@@ -440,7 +445,7 @@ link-target transfer remains incomplete. |
|
||||
|
||||
| Option | Description |
|
||||
|---|---|
|
||||
| `-p <port>` | SSH port in the current CLI. This conflicts with rsync's `-p` permissions option and is planned for correction. |
|
||||
| `--ssh-port <port>` | SSH port for the SSH transport (default: 22). Note the short `-p` is now rsync's `--perms`. |
|
||||
| `--fastsync-server-path <path>` | Remote FastSync server path for SSH mode. |
|
||||
| `--source-dir <path>` | Set the source directory explicitly. |
|
||||
| `--dest-dir <path>` | Set the destination directory explicitly. |
|
||||
|
||||
+15
-15
@@ -6,8 +6,8 @@ This document maps rsync's full feature set to FastSync's current implementation
|
||||
|
||||
| Status | Count | Description |
|
||||
|--------|-------|-------------|
|
||||
| ✅ Implemented | 129 | Feature works end-to-end |
|
||||
| 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics |
|
||||
| ✅ Implemented | 132 | Feature works end-to-end |
|
||||
| 🔀 Alt Arg | 0 | Functionality exists but under different flag/semantics |
|
||||
| ⚠️ Partial | 10 | Flag parsed/stored but behavior incomplete |
|
||||
| 🔄 Compatibility No-op | 3 | Flag is accepted for CLI compatibility but has no effect |
|
||||
| ❌ Not Implemented | 2 | Flag not recognized or no behavior |
|
||||
@@ -19,7 +19,7 @@ This document maps rsync's full feature set to FastSync's current implementation
|
||||
|
||||
| Flag | Rsync Description | FastSync Status | Notes |
|
||||
|------|-------------------|-----------------|-------|
|
||||
| `-a`, `--archive` | Archive mode is -rlptgoD | 🔀 Alt Arg | Maps to -c -m -M (compression + multithread + metadata) |
|
||||
| `-a`, `--archive` | Archive mode is -rlptgoD | ✅ Implemented | Phase 7 Wave A: real rsync archive. `-a`/`--archive` now implies `--links` + metadata (perms/times/group/owner as FastSync's broad bundle) + `--devices` + `--specials`. FastSync is always recursive, so no `-r` is needed. It no longer implies compression or multithreading (those moved to `-z`/`-j`). The short-option namespace is now rsync-parity (see the Phase 7 note) |
|
||||
| `-v`, `--verbose` | Increase verbosity | ✅ Implemented | Sets `log_level=DEBUG` |
|
||||
| `-q`, `--quiet` | Suppress non-error messages | ✅ Implemented | Suppresses client output while preserving errors |
|
||||
| `--help` | Show help | ✅ Implemented | Prints usage and exits; `-h` is not accepted |
|
||||
@@ -97,7 +97,7 @@ This document maps rsync's full feature set to FastSync's current implementation
|
||||
| `--backup-dir=DIR` | Backup directory hierarchy | ✅ Implemented | `backup_dir` config field |
|
||||
| `--suffix=SUFFIX` | Backup suffix (default ~) | ✅ Implemented | `suffix` config field |
|
||||
| `--delay-updates` | Put updated files in place at end | ✅ Implemented | Successfully received files are staged under a private 0700 `.fastsync-stage` dir inside the receive root and atomically renamed into their final destinations only after the whole transfer (manifest/delete handling included) succeeds, just before the success/outcome frame is sent. The delete walker deliberately skips the staging dir at the receive root, so `--delete` removes genuine extras but never the staged files (deletion runs before publication; rsync's delete-after ordering is not implemented). `--existing`/`--ignore-existing`/`--update` decide against the final destination path at stage time; `--backup` moves the old file aside at publication. Incompatible with `--inplace` and with `--backup-dir=.fastsync-stage` (the internal staging name is reserved; both are rejected). The staging dir name is fixed, so two simultaneous delayed transfers to the same destination root are serialized with an exclusive advisory lock held for the whole transfer: the second session fails cleanly instead of corrupting the first. Aborting or failing before publication installs nothing and removes the staging tree; a crash between stage and publish leaves staged leftovers that the next delayed run wipes at start (process death releases the lock). A stage→publish failure aborts the transfer (best-effort cleanup of the not-yet-published staged files; already-published files are not rolled back). Works in single-threaded and `-m` modes |
|
||||
| `-T`, `--temp-dir=DIR` | Create temporary files in DIR | ✅ Implemented | `--temp-dir` only; `-T` stays FastSync's `--timeout` alias. Scratch dir is resolved under the receive root; temp copies use a unique name there and are atomically renamed into place. If the scratch dir and destination are on different filesystems the atomic rename fails with EXDEV and the file save fails, which aborts the whole transfer (FastSync has no per-file skip/resume on a save error; rsync's non-atomic copy fallback is deliberately not used). `--inplace` and `--partial-dir` writes bypass the scratch dir |
|
||||
| `-T`, `--temp-dir=DIR` | Create temporary files in DIR | ✅ Implemented | `--temp-dir` with the rsync short `-T` (Phase 7 Wave A; the timeout alias moved to long-only `--timeout`). Scratch dir is resolved under the receive root; temp copies use a unique name there and are atomically renamed into place. If the scratch dir and destination are on different filesystems the atomic rename fails with EXDEV and the file save fails, which aborts the whole transfer (FastSync has no per-file skip/resume on a save error; rsync's non-atomic copy fallback is deliberately not used). `--inplace` and `--partial-dir` writes bypass the scratch dir |
|
||||
|
||||
## 7. Deletion
|
||||
|
||||
@@ -112,7 +112,7 @@ This document maps rsync's full feature set to FastSync's current implementation
|
||||
| `--max-delete=NUM` | Max files to delete | ✅ Implemented | `max_delete` config field (default -1 = no client limit; 0 = delete nothing). NUM bounds a `--delete` run with rsync's all-or-nothing semantics: the receiver rehearses the deletion first and, if the destination holds more than NUM extras, deletes NOTHING and fails the transfer with a distinct `--max-delete` error. A run at or below NUM deletes exactly the extras. NUM only applies together with `--delete` (it is inert otherwise, matching rsync). The hard server bound `MAX_SERVER_DELETE_COUNT` (100000) still caps the walk; a NUM above it never raises that cap, and exceeding the server bound is its own all-or-nothing error. Directories count toward the limit (each removed empty directory is one deletion), like rsync |
|
||||
| `--ignore-errors` | Delete even with I/O errors | ✅ Implemented | Sender-side, client-only config field. rsync suppresses `--delete` when the transfer had I/O errors; FastSync's equivalent is a source-scan I/O error (an unreadable directory, e.g. EACCES): by default the scan aborts the run so no deletion happens. With `--ignore-errors` the scan continues past the unreadable directory, the readable tree is transferred and the deletion still runs (the mirror of the unreadable directory is treated as an extra). The run still exits non-zero (the error is reported, matching rsync's error status). Divergence: without the flag FastSync aborts the whole run on the scan error, whereas rsync transfers the rest of the tree and merely skips the deletion; both leave the deletion undone |
|
||||
| `--force` | Force deletion of non-empty dirs | ✅ Implemented | `force_delete` receiver config field (crosses the wire). rsync's `--force` lets an incoming non-directory replace a destination directory; FastSync implements exactly that: when a regular file is written to a path that is currently a (possibly non-empty) destination directory, `--force` removes that directory tree first — confined to the receive root and symlink-safe (O_NOFOLLOW fd walk, symlinks removed by name, never followed) — so the atomic install can place the file. Without `--force` such a write fails and the run aborts. Divergence: `--force` acts on the immediate-install path only; under `--delay-updates` a blocking directory is not cleared (publication renames over regular files) |
|
||||
| `--prune-empty-dirs` | Prune empty dir chains | ✅ Implemented | Long-only: FastSync's `-m` is already multithreading (recorded divergence — rsync's `-m` short form is not reassigned). FastSync's recursive transfer never emits directory entries, so empty directories are inherently never transferred (which is rsync's `-m` behavior) and truly-empty destination directory chains are removed by `--delete` regardless of this flag. The flag's additional real effect is on the `--dirs` explicit directory-entry generator: a plain `-d <empty-dir>` run omits the empty source directory's entry, so nothing is created at the destination (no `STATUS_MKDIR`, no `-i`/`--out-format` change line, and an existing empty mirror becomes an extra that `--delete` prunes). Explicitly `--files-from`-listed directories always pass through (documented `--files-from` behavior). A directory that still holds an excluded-but-protected file survives, matching the `--delete-excluded` default |
|
||||
| `-m`, `--prune-empty-dirs` | Prune empty dir chains | ✅ Implemented | `-m`/`--prune-empty-dirs` (Phase 7 Wave A freed the rsync short `-m`; FastSync multithreading is now `-j`/`--threads`). FastSync's recursive transfer never emits directory entries, so empty directories are inherently never transferred (which is rsync's `-m` behavior) and truly-empty destination directory chains are removed by `--delete` regardless of this flag. The flag's additional real effect is on the `--dirs` explicit directory-entry generator: a plain `-d <empty-dir>` run omits the empty source directory's entry, so nothing is created at the destination (no `STATUS_MKDIR`, no `-i`/`--out-format` change line, and an existing empty mirror becomes an extra that `--delete` prunes). Explicitly `--files-from`-listed directories always pass through (documented `--files-from` behavior). A directory that still holds an excluded-but-protected file survives, matching the `--delete-excluded` default |
|
||||
|
||||
**Deletion-timing implementation notes (Phase 3):** the delete flags above are
|
||||
real. Two new config booleans (`delete_during`, `delete_delay`) join the already
|
||||
@@ -238,7 +238,7 @@ why plain `--append` works on the normal atomic path, not only with `--inplace`.
|
||||
| Flag | Rsync Description | FastSync Status | Notes |
|
||||
|------|-------------------|-----------------|-------|
|
||||
| `-M`, `--preserve` | Preserve file metadata | ✅ Implemented | Mode, uid, gid, mtime |
|
||||
| `-p`, `--perms` | Preserve permissions | 🔀 Alt Arg | `-p` means SSH port; permissions preserved via `-M`/`--preserve` |
|
||||
| `-p`, `--perms` | Preserve permissions | ✅ Implemented | Phase 7 Wave A: `-p`/`--perms` now preserve permission bits, folded into FastSync's broad metadata bundle (`--preserve`); the SSH port moved to `--ssh-port`. rsync-parity short form |
|
||||
| `-o`, `--owner` | Preserve owner | ✅ Implemented | Part of -M |
|
||||
| `-g`, `--group` | Preserve group | ✅ Implemented | Part of -M |
|
||||
| `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M |
|
||||
@@ -597,7 +597,7 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
||||
|
||||
| Flag | Rsync Description | FastSync Status | Notes |
|
||||
|------|-------------------|-----------------|-------|
|
||||
| `-z`, `--compress` | Compress file data | 🔀 Alt Arg | Always uses zstd (rsync supports multiple algorithms) |
|
||||
| `-z`, `--compress` | Compress file data | ✅ Implemented | Always uses zstd (rsync supports multiple algorithms — a documented divergence, selectable via `--compress-choice`). Phase 7 Wave A: `-z` is now the compression short form; `-c` is rsync's `--checksum` |
|
||||
| `--compress-choice=STR`, `--zc=STR` | Choose compression algorithm | ✅ Implemented | FastSync supports `zstd` and `none` |
|
||||
| `--compress-level=NUM`, `--zl=NUM` | Set compression level | ✅ Implemented | 1-22, default 5 |
|
||||
| `--compress-threads=NUM` | Set compression threads | ✅ Implemented | `compression_threads` config field (client-only; does not cross the wire). Sets the number of worker threads used by the zstd compression pool to NUM (1..64; 0/garbage/oversized rejected up front). Accepted in both `--compress-threads=NUM` and two-argument `--compress-threads NUM` forms. Composes with `-c`/compression; under the `-m` multithreaded pipeline it parallelizes compressed chunk encoding. See test_tcp.py `-c --compress-threads=2` and test_client_cli.c |
|
||||
@@ -616,7 +616,7 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
||||
| `--address=ADDRESS` | Bind address for outgoing socket | ✅ Implemented | Binds the outgoing client socket to a local source address before `connect()` (resolved with the same `-4`/`-6` family hints as the destination). Local socket concern: never crosses the wire |
|
||||
| `-4`, `--ipv4` | Prefer IPv4 | ✅ Implemented | Forces `AF_INET` in the `getaddrinfo` hints for client destination/source resolution and the server bind (see the Phase 5, Wave B note). Mutually exclusive with `-6` |
|
||||
| `-6`, `--ipv6` | Prefer IPv6 | ✅ Implemented | Forces `AF_INET6` in the `getaddrinfo` hints for client destination/source resolution and the server bind. Mutually exclusive with `-4` |
|
||||
| `--remote-option=OPT`, `-M` | Send an option only to the remote side | ✅ Implemented | Long form only; each value is appended to the remote server invocation over SSH as an individually single-quote-escaped shell word in `ssh_build_remote_command()`. Values are validated (non-empty, no control characters) and shell metacharacters cannot break out of the quoting (`;`, `&`, `|`, <code>`</code>, `$`, `(`, `)`, quotes are neutralized), so a value cannot inject an arbitrary remote command and a subsequent `--` on the client line cannot be turned into one. The options never cross the binary config frame. Divergence: the short `-M` form is intentionally unavailable because `-M` is already FastSync's metadata-preservation flag/multiplier (see Phase 5 notes below) |
|
||||
| `--remote-option=OPT`, `-M` | Send an option only to the remote side | ✅ Implemented | Each value is appended to the remote server invocation over SSH as an individually single-quote-escaped shell word in `ssh_build_remote_command()`. Values are validated (non-empty, no control characters) and shell metacharacters cannot break out of the quoting (`;`, `&`, `|`, <code>`</code>, `$`, `(`, `)`, quotes are neutralized), so a value cannot inject an arbitrary remote command and a subsequent `--` on the client line cannot be turned into one. The options never cross the binary config frame. Phase 7 Wave A: the short `-M` form is now available (as `-M OPT` and `-M=OPT`), matching rsync; metadata mode moved to long-only `--preserve` |
|
||||
|
||||
## 14. Daemon Mode
|
||||
|
||||
@@ -675,7 +675,7 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
||||
| `--protocol=NUM` | Force older protocol version | ✅ Implemented | Forces the wire protocol version for this transfer. FastSync has exactly ONE wire format (`PROTOCOL_VERSION`, currently 2.16.0) with no downgrade/backward-compat code paths, so `--protocol=2.16.0` is accepted (it sets the version claim the client sends, which the server already requires to match exactly) and **every other value is rejected up front** with a clear error before any connection — it does not and cannot speak an older or virtual wire format. Divergence from rsync (which negotiates a range and downgrades to an integer 0..31): FastSync's honest contract is force-to-the-one-supported-value; a genuine downgrade would require a per-version compatibility layer that does not exist. Client-only; the server-side exact-match check is unchanged. `--protocol=2.16`/`2.15.0`/`2.17.0`/`216`/`31`/garbage are all rejected. See the Phase-6 protocol note below |
|
||||
| `--iconv=CONVERT_SPEC` | Charset conversion | ✅ Implemented | Charset conversion of FILE NAMES (not content) at the protocol boundary via iconv(3): `--iconv=LOCAL[,REMOTE]` — the sender converts each local filename LOCAL→REMOTE before transmitting, and the receiver converts each wire filename REMOTE→LOCAL before creating/writing. The full CONVERT_SPEC is serialized into the config frame as a new trailing string field so the peer knows the wire charset; **PROTOCOL_VERSION bumped 2.15.0 → 2.16.0**. `LOCAL[,REMOTE]` parse: single charset ⇒ LOCAL==REMOTE (identity both ways); garbage rejected up front. Validation probes BOTH directions (a spec that only opens one way is refused, as is a NUL-emitting target charset like utf-16/utf-32/ucs-2, since filenames cannot contain NUL). An unrepresentable name (EILSEQ/EINVAL) fails that path cleanly with a logged `--iconv: cannot convert file name ...` and is never written mangled/truncated. Conversion is applied at EVERY wire-path site (regular/MKDIR/hardlink path+target/symlink path+target/SPECIAL, the delete manifest, the incremental-check path, and the `-s`/`chunk_serialize` embedded blob path), on both client and server (`--iconv` is also a server/daemon option). Zero overhead when unset. See the Phase-6 iconv notes below |
|
||||
| `--checksum-seed=NUM` | Set checksum seed | ✅ Implemented | Sets the seed for FastSync's whole-file xxHash64 digest (full 64-bit seed) and for the delta path's per-block xxHash32 strong checksum (low 32 bits of the seed). An explicit seed deterministically changes every computed digest on BOTH endpoints (sender and receiver share the seed via the config frame, protocol 2.10.0), so identical runs with the same seed skip the same files and a changed seed changes the digests — the explicit-seed path that makes xxHash comparisons deterministic. `--checksum-choice=md5` has no seed and ignores it (documented). The value is a strict decimal 0..2⁶⁴-1 (blank, signed, or non-numeric values are rejected). Like rsync, a seed only matters where a digest is actually computed (`--checksum` or a basis-dir run, or a delta transfer); it does not by itself enable `--checksum`/`--delta`. Divergence from rsync: the default is seed 0, and FastSync never randomizes the seed (rsync uses a random per-transfer seed when `--checksum-seed` is unset); FastSync's unset default therefore reproduces its historical byte-for-byte behavior |
|
||||
| `--secluded-args` | Use protocol to send args | 🔄 Compatibility No-op | Accepted for CLI compatibility; it does not change FastSync transport or protocol behavior. `-s` remains chunk serialization. |
|
||||
| `--secluded-args`, `-s` | Use protocol to send args | 🔄 Compatibility No-op | Accepted for CLI compatibility, including the rsync short `-s` (Phase 7 Wave A); it does not change FastSync transport or protocol behavior, because remote SSH argv is already built injection-safe (single-quote-escaped). Chunk serialization is the long-only `--chunk-serialization`. |
|
||||
| `--no-OPTION` | Turn off implied option | ✅ Supported | Supported boolean FastSync options and archive-implied options; unsafe or value-taking options are rejected. |
|
||||
|
||||
---
|
||||
@@ -683,7 +683,7 @@ now transmits targets (the prior behavior was broken/partial); its status moved
|
||||
## Implementation Difficulty Plan
|
||||
|
||||
**Phase 5 notes (remote-option wave):** `--remote-option=OPT` (long form only) and `--trust-sender` landed here.
|
||||
- `--remote-option` is CLIENT-only and never serialized into the binary config frame. On the SSH transport the client forwards each value to the remote server by appending it to the remote command line in `ssh_build_remote_command()`, after ` --stdio`, as an individually single-quoted shell word (`'...'` with `'\''` for embedded quotes). Values are validated at CLI parse time (non-empty; no ASCII control characters) and rejected otherwise, and a non-conforming value is refused again in the command builder, so shell metacharacters (`;`, `&`, `|`, backticks, `$()`, quotes) can never break out of the quoting to inject an unrelated remote command — including after a client-side `--` separator, whose arguments are never forwarded anyway. Because the remote options affect the *remote server invocation*, not the transmitted config, the wire frame layout is unchanged, but `PROTOCOL_VERSION` was bumped **2.13.0 → 2.14.0** as the Phase-5 lockstep release marker (a 2.14 client against a 2.13 server fails the version check cleanly rather than the old server rejecting an unfamiliar forwarded argv later). Divergence: rsync's short `-M` form of `--remote-option` is intentionally NOT implemented, because `-M` is already FastSync's metadata-preservation mode/multiplier.
|
||||
- `--remote-option` is CLIENT-only and never serialized into the binary config frame. On the SSH transport the client forwards each value to the remote server by appending it to the remote command line in `ssh_build_remote_command()`, after ` --stdio`, as an individually single-quoted shell word (`'...'` with `'\''` for embedded quotes). Values are validated at CLI parse time (non-empty; no ASCII control characters) and rejected otherwise, and a non-conforming value is refused again in the command builder, so shell metacharacters (`;`, `&`, `|`, backticks, `$()`, quotes) can never break out of the quoting to inject an unrelated remote command — including after a client-side `--` separator, whose arguments are never forwarded anyway. Because the remote options affect the *remote server invocation*, not the transmitted config, the wire frame layout is unchanged, but `PROTOCOL_VERSION` was bumped **2.13.0 → 2.14.0** as the Phase-5 lockstep release marker (a 2.14 client against a 2.13 server fails the version check cleanly rather than the old server rejecting an unfamiliar forwarded argv later). Divergence: rsync's short `-M` form of `--remote-option` was intentionally NOT implemented at that time because `-M` was FastSync metadata mode; **Phase 7 Wave A later freed `-M` for `--remote-option` and moved metadata to long-only `--preserve`** (see the Sending Options table).
|
||||
- `--trust-sender` is a receiver-local policy: it never crosses the wire (the sender's value is never serialized, so a wire peer can never enable it). On the receiving process it skips the up-front re-validation of the incoming file list (empty/`..` path rejection and the escaping-symlink-target containment), trusting the sender's list instead of double-checking — fewer checks, faster, and potentially unsafe, matching rsync. It is OFF by default (`config.trust_sender`). As a deliberate safety floor, the low-level fd-relative confinement primitives are NOT disabled: `file_open_secure_parent()` (O_NOFOLLOW walk, `..` rejection, root containment) and leaf/destination confinement still hold, so even under `--trust-sender` a hostile sender cannot write or create a symlink outside the authorized root — the relaxation only removes the redundant list-layer double-checks, never the root-confinement guarantees.
|
||||
|
||||
The estimates below cover the currently unimplemented features in this document. They assume one engineer familiar with the codebase, include implementation and focused tests, and exclude production rollout time. A feature should not be marked implemented until its behavior is tested in both local and SSH/TCP paths where applicable.
|
||||
@@ -798,7 +798,7 @@ These are the hardest compatibility items because they require durable formats o
|
||||
|
||||
These are the last compatibility items and the closing phase toward rsync flag parity. Per the project decision: every rsync flag (short **and** long) that is *possible* gets real rsync-parity behavior; anything physically impossible becomes an explicit **Impossible/Divergence** status (accepted for CLI compatibility, safely inert, with coverage tests proving that); and the two privilege flags are deferred to the final wave pending an explicit privilege-model decision. The remaining `⚠️ Partial`, `🔄 Compatibility No-op`, `🔀 Alt Arg`, and `❌ Not Implemented` rows in the Summary are this phase's scope. All Wave A renames are **client-side only** (the wire config fields `use_compression`/`use_metadata`/`use_sendfile`/`use_chunk_serialization` are unchanged), so they require **no `PROTOCOL_VERSION` bump**.
|
||||
|
||||
**Wave A — CLI namespace parity (rename colliding FastSync short flags).** This is the prerequisite for all short-flag parity: it frees the short letters rsync needs and makes the three `🔀 Alt Arg` rows real. Renames touch `src/client/client_cli.c` `OPTION_TABLE` + the legacy bool table, `src/client/usage.c`, the README, and integration fixtures that reference the old short flags; the server's independent little CLI stays as-is (`-p` there remains the server port). Includes an opportunistic mechanical CLI refactor (align the client tables; dedupe where the pattern is identical). A fresh rsync-collision audit fixes the final short letters for the freed FastSync flags during this wave. Flags already matching rsync (`-e`/`--rsh`, `-S`/`--sparse`, `-H`, `-K`, `-k`, `-A`, `-X`, `-x`, `-C`, `-R`, `-d`, `-u`, `-W`, `-I`, `-l`, `-o`) are untouched.
|
||||
**Wave A — CLI namespace parity (rename colliding FastSync short flags) — ✅ implemented.** This freed the short letters rsync needs and made the three `🔀 Alt Arg` rows real. `-c`→`--checksum`, `-m`→`--prune-empty-dirs`, `-M`→`--remote-option`, `-f`→`--filter`, `-s`→`--secluded-args`, `-p`→`--perms`, `-T`→`--temp-dir`, `-a`/`--archive`→real `-rlptgoD`. FastSync's own flags moved to long-form-only or new shorts: `-j`/`--threads` (multithreading), `--preserve` (metadata), `--sendfile`, `--chunk-serialization`, `--timeout`, `--ssh-port`. The server's independent little CLI keeps `-p` as its port. All client-side, no wire change, no `PROTOCOL_VERSION` bump. Unit tests 37/37, full integration 400 passed, cppcheck and clang-format clean.
|
||||
|
||||
| FastSync flag today | rsync wants that name | Proposed rename |
|
||||
|---------------------|----------------------|-----------------|
|
||||
@@ -857,10 +857,10 @@ Ranked by user demand, implementation complexity, and interoperability impact (_
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| `-m` | Multithreaded pipeline (scanner/loader/sender) |
|
||||
| `-s` | Chunk serialization mode |
|
||||
| `-f` / `--sendfile` | Zero-copy sendfile() syscall (TCP only) |
|
||||
| `-c [level]` | zstd compression level (1-22) |
|
||||
| `-j` / `--threads` | Multithreaded pipeline (scanner/loader/sender) (renamed from `-m` in Phase 7 Wave A; `-m` is now rsync `--prune-empty-dirs`) |
|
||||
| `--chunk-serialization` | Chunk serialization mode (long form only; `-s` is now rsync `--secluded-args`) |
|
||||
| `--sendfile` | Zero-copy sendfile() syscall (TCP only) (long form only; `-f` is now rsync `--filter`) |
|
||||
| `-z [level]` / `--compress` | zstd compression level (1-22) (`-c` is now rsync `--checksum`) |
|
||||
| `--chunk-size` | Configurable chunk size |
|
||||
| `--tls` | TLS encryption (mutual auth) |
|
||||
| `--fastsync-server-path` | Path to fastsync-server binary |
|
||||
|
||||
+51
-34
@@ -554,7 +554,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--stats", NULL, OPT_FLAG, offsetof(Config, stats)},
|
||||
{"--human-readable", "-h", OPT_FLAG, offsetof(Config, human_readable)},
|
||||
{"--partial", NULL, OPT_FLAG, offsetof(Config, partial)},
|
||||
{"--secluded-args", NULL, OPT_NOOP, 0},
|
||||
{"--secluded-args", "-s", OPT_NOOP, 0},
|
||||
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
|
||||
{"--old-args", NULL, OPT_FLAG, offsetof(Config, old_args)},
|
||||
{"--rsh", "-e", OPT_STRING, offsetof(Config, rsh_command)},
|
||||
@@ -573,7 +573,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--append", NULL, OPT_FLAG, offsetof(Config, append)},
|
||||
{"--append-verify", NULL, OPT_FLAG, offsetof(Config, append_verify)},
|
||||
{"--fsync", NULL, OPT_FLAG, offsetof(Config, use_fsync)},
|
||||
{"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)},
|
||||
{"--checksum", "-c", OPT_FLAG, offsetof(Config, checksum)},
|
||||
{"--8-bit-output", "-8", OPT_FLAG, offsetof(Config, eight_bit_output)},
|
||||
{"--itemize-changes", "-i", OPT_FLAG, offsetof(Config, itemize_changes)},
|
||||
{"--list-only", NULL, OPT_FLAG, offsetof(Config, list_only)},
|
||||
@@ -620,7 +620,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--max-delete", NULL, OPT_NONNEG_INT, offsetof(Config, max_delete)},
|
||||
{"--ignore-errors", NULL, OPT_FLAG, offsetof(Config, ignore_errors)},
|
||||
{"--force", NULL, OPT_FLAG, offsetof(Config, force_delete)},
|
||||
{"--prune-empty-dirs", NULL, OPT_FLAG, offsetof(Config, prune_empty_dirs)},
|
||||
{"--prune-empty-dirs", "-m", OPT_FLAG, offsetof(Config, prune_empty_dirs)},
|
||||
{"--ignore-missing-args", NULL, OPT_FLAG, offsetof(Config, ignore_missing_args)},
|
||||
{"--delete-missing-args", NULL, OPT_FLAG, offsetof(Config, delete_missing_args)},
|
||||
|
||||
@@ -635,7 +635,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
/* --rsync-path is rsync's spelling for the same "server program path"; it
|
||||
* is a pure alias for fastsync_server_path (never a distinct field). */
|
||||
{"--rsync-path", NULL, OPT_STRING, offsetof(Config, fastsync_server_path)},
|
||||
{"--temp-dir", NULL, OPT_STRING, offsetof(Config, temp_dir)},
|
||||
{"--temp-dir", "-T", OPT_STRING, offsetof(Config, temp_dir)},
|
||||
{"--partial-dir", NULL, OPT_STRING, offsetof(Config, partial_dir)},
|
||||
{"--suffix", NULL, OPT_STRING, offsetof(Config, suffix)},
|
||||
{"--compress-choice", "--zc", OPT_STRING, offsetof(Config, compress_choice)},
|
||||
@@ -668,10 +668,10 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--xattrs", "-X", OPT_FLAG, offsetof(Config, preserve_xattrs)},
|
||||
{"--acls", "-A", OPT_FLAG, offsetof(Config, preserve_acls)},
|
||||
{"--fake-super", NULL, OPT_FLAG, offsetof(Config, fake_super)},
|
||||
/* Long-form-only: rsync's -M short form of --remote-option is INTENTIONALLY
|
||||
* unavailable because -M already means metadata mode in FastSync (a
|
||||
* documented divergence; see RSYNC_COMPAT.md). --trust-sender is a local
|
||||
* receiver policy and never travels to the remote peer. */
|
||||
/* rsync's -M/--remote-option: -M is now the short alias for --remote-option
|
||||
* (metadata mode is long-only --preserve), handled in the parse loop where
|
||||
* --remote-option is parsed. --trust-sender is a local receiver policy and
|
||||
* never travels to the remote peer. */
|
||||
{"--trust-sender", NULL, OPT_FLAG, offsetof(Config, trust_sender)},
|
||||
};
|
||||
|
||||
@@ -696,17 +696,17 @@ static const NegatableOption NEGATABLE_OPTIONS[] = {
|
||||
{"sparse", "S", offsetof(Config, preserve_sparse)},
|
||||
{"inplace", NULL, offsetof(Config, inplace)},
|
||||
{"preallocate", NULL, offsetof(Config, preallocate)},
|
||||
{"checksum", NULL, offsetof(Config, checksum)},
|
||||
{"checksum", "c", offsetof(Config, checksum)},
|
||||
{"from0", NULL, offsetof(Config, from0)},
|
||||
{"cvs-exclude", NULL, offsetof(Config, cvs_exclude)},
|
||||
|
||||
/* These options are also implied by --archive or handled outside the table. */
|
||||
{"compress", "c", offsetof(Config, use_compression)},
|
||||
{"compress", NULL, offsetof(Config, use_compression)},
|
||||
{"compress", "z", offsetof(Config, use_compression)},
|
||||
{"multithreading", "m", offsetof(Config, use_multithreading)},
|
||||
{"preserve", "M", offsetof(Config, use_metadata)},
|
||||
{"sendfile", "f", offsetof(Config, use_sendfile)},
|
||||
{"chunk-serialization", "s", offsetof(Config, use_chunk_serialization)},
|
||||
{"multithreading", "j", offsetof(Config, use_multithreading)},
|
||||
{"preserve", NULL, offsetof(Config, use_metadata)},
|
||||
{"sendfile", NULL, offsetof(Config, use_sendfile)},
|
||||
{"chunk-serialization", NULL, offsetof(Config, use_chunk_serialization)},
|
||||
{"xattrs", "X", offsetof(Config, preserve_xattrs)},
|
||||
{"acls", "A", offsetof(Config, preserve_acls)},
|
||||
{"fake-super", NULL, offsetof(Config, fake_super)},
|
||||
@@ -1029,17 +1029,35 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
config->preserve_specials = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled preservation of device and special files (-D)");
|
||||
} else if (opt_is(argv[i], "-a", "--archive")) {
|
||||
config->use_compression =
|
||||
!config->compress_choice || strcmp(config->compress_choice, "zstd") == 0;
|
||||
config->use_multithreading = true;
|
||||
/* Real rsync archive (-rlptgoD). FastSync is always recursive and always
|
||||
* preserves hard-link/other transfer semantics per its own flags, so -a
|
||||
* implies links, full metadata (perms/times/group/owner as FastSync's
|
||||
* broad bundle), devices and specials. Compression and multithreading
|
||||
* are NOT implied (they are no longer part of archive mode). */
|
||||
config->follow_symlinks = true;
|
||||
config->use_metadata = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled archive mode (-c -m -M)");
|
||||
} else if (opt_is(argv[i], "-p", NULL)) {
|
||||
config->preserve_devices = true;
|
||||
config->preserve_specials = true;
|
||||
log_info_message(LOG_INFO_MISC,
|
||||
"Enabled archive mode (-rlptgoD: links, metadata, devices, specials)");
|
||||
} else if (opt_is(argv[i], "-p", "--perms")) {
|
||||
/* rsync -p/--perms: preserve permission bits. Folded into FastSync's
|
||||
* broad metadata bundle (mode/mtime travel together). */
|
||||
config->use_metadata = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled permission preservation");
|
||||
} else if (opt_is(argv[i], "--ssh-port", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_positive_int_option(&config->ssh_port, argv[++i], "-p") != 0)
|
||||
if (set_positive_int_option(&config->ssh_port, argv[++i], "--ssh-port") != 0)
|
||||
return -1;
|
||||
if (config->ssh_port > 65535) {
|
||||
log_message(LOG_LEVEL_ERROR, "SSH port must be 1-65535");
|
||||
return -1;
|
||||
}
|
||||
} else if (strncmp(argv[i], "--ssh-port=", 11) == 0) {
|
||||
if (set_positive_int_option(&config->ssh_port, argv[i] + 11, "--ssh-port") != 0)
|
||||
return -1;
|
||||
if (config->ssh_port > 65535) {
|
||||
log_message(LOG_LEVEL_ERROR, "SSH port must be 1-65535");
|
||||
@@ -1085,7 +1103,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
config->delta_max_file_size = val;
|
||||
else
|
||||
log_message(LOG_LEVEL_WARNING, "--delta-max value %llu too small, using default", val);
|
||||
} else if (opt_is(argv[i], "-c", "-z")) {
|
||||
} else if (opt_is(argv[i], "-z", "--compress")) {
|
||||
config->use_compression =
|
||||
!config->compress_choice || strcmp(config->compress_choice, "zstd") == 0;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled Compression");
|
||||
@@ -1102,20 +1120,20 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
i++;
|
||||
}
|
||||
}
|
||||
} else if (opt_is(argv[i], "-M", "--preserve")) {
|
||||
} else if (opt_is(argv[i], "--preserve", NULL)) {
|
||||
config->use_metadata = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled metadata preservation");
|
||||
} else if (opt_is(argv[i], "-E", "--executability")) {
|
||||
config->use_metadata = true;
|
||||
config->use_executability = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled executable permission preservation");
|
||||
} else if (opt_is(argv[i], "-f", "--sendfile")) {
|
||||
} else if (opt_is(argv[i], "--sendfile", NULL)) {
|
||||
config->use_sendfile = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled sendfile");
|
||||
} else if (opt_is(argv[i], "-m", NULL)) {
|
||||
} else if (opt_is(argv[i], "-j", "--threads")) {
|
||||
config->use_multithreading = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled Multithreading");
|
||||
} else if (opt_is(argv[i], "-s", NULL)) {
|
||||
} else if (opt_is(argv[i], "--chunk-serialization", NULL)) {
|
||||
config->use_chunk_serialization = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled Chunk Serialization");
|
||||
} else if (opt_is(argv[i], "--server-port", NULL)) {
|
||||
@@ -1210,7 +1228,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
} else if (strncmp(argv[i], "--filter=", 9) == 0) {
|
||||
if (config_add_filter(config, argv[i] + 9) != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--filter", NULL)) {
|
||||
} else if (strncmp(argv[i], "-f=", 3) == 0) {
|
||||
if (config_add_filter(config, argv[i] + 3) != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--filter", "-f")) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
@@ -1248,13 +1269,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
} else if (opt_is(argv[i], "--info", NULL)) {
|
||||
if (i + 1 >= argc || parse_info_flags(argv[++i], config) != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "-T", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0)
|
||||
return -1;
|
||||
} else if (strncmp(argv[i], "--skip-compress=", 16) == 0) {
|
||||
if (parse_skip_compress(config, argv[i] + 16) != 0)
|
||||
return -1;
|
||||
@@ -1308,7 +1322,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
} else if (strncmp(argv[i], "--remote-option=", 16) == 0) {
|
||||
if (config_add_remote_option(config, argv[i] + 16, "--remote-option") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--remote-option", NULL)) {
|
||||
} else if (strncmp(argv[i], "-M=", 3) == 0) {
|
||||
if (config_add_remote_option(config, argv[i] + 3, "-M") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--remote-option", "-M")) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for --remote-option");
|
||||
return -1;
|
||||
|
||||
+22
-23
@@ -17,12 +17,14 @@ void print_usage(void) {
|
||||
printf(" /local/path TCP transport (requires server on localhost:8080)\n");
|
||||
printf("\n");
|
||||
printf("Options:\n");
|
||||
printf(" -c [level] Enable compression (level 1-22, default 5)\n");
|
||||
printf(" -z [level] Alias for -c\n");
|
||||
printf(" -a, --archive Archive mode (-c -m -M)\n");
|
||||
printf(" -c, --checksum Verify content by checksum instead of size+mtime\n");
|
||||
printf(" -z, --compress [level] Enable compression (level 1-22, default 5)\n");
|
||||
printf(" -a, --archive rsync archive mode (-rlptgoD): links, metadata,\n");
|
||||
printf(" devices and specials (not compression/multithreading)\n");
|
||||
printf(" -n, --dry-run Show what would be transferred\n");
|
||||
printf(" --remove-source-files Remove regular source files after successful transfer\n");
|
||||
printf(" -p <port> SSH port (default: 22)\n");
|
||||
printf(" -p, --perms Preserve permission bits (part of the metadata bundle)\n");
|
||||
printf(" --ssh-port <port> SSH port (default: 22)\n");
|
||||
printf(" -e, --rsh <command> Remote shell to launch on the client for the SSH\n");
|
||||
printf(" transport (default: ssh). The command may include\n");
|
||||
printf(" arguments, e.g. -e \"ssh -p 2222\"\n");
|
||||
@@ -80,9 +82,8 @@ void print_usage(void) {
|
||||
printf(" entry's destination mirror receiver-side. Independent of\n");
|
||||
printf(" --delete (it does not imply --delete; a non-empty directory\n");
|
||||
printf(" mirror is removed only with --force or --delete)\n");
|
||||
printf(" --prune-empty-dirs Do not transfer empty directory entries (--dirs mode);\n");
|
||||
printf(" recursive transfers never send empty dirs. rsync's -m\n");
|
||||
printf(" short form stays FastSync multithreading\n");
|
||||
printf(" -m, --prune-empty-dirs Do not transfer empty directory entries (--dirs mode);\n");
|
||||
printf(" recursive transfers never send empty dirs\n");
|
||||
printf(" Note: each timing flag implies --delete. Combining a timing flag with\n");
|
||||
printf(" --no-delete (in either order) is rejected as a config error.\n");
|
||||
printf(" --ignore-existing Skip files that already exist on receiver\n");
|
||||
@@ -105,8 +106,8 @@ void print_usage(void) {
|
||||
printf(" --files-from <file> Read the source file list from FILE (paths relative to the "
|
||||
"source root)\n");
|
||||
printf(" -0, --from0 Entries in --files-from are NUL-delimited\n");
|
||||
printf(" --filter=RULE rsync-style filter rule (+/- include/exclude; repeatable; the\n");
|
||||
printf(" rsync -f short form conflicts with FastSync sendfile -f)\n");
|
||||
printf(" -f, --filter=RULE rsync-style filter rule (+/- include/exclude; repeatable;\n");
|
||||
printf(" both --filter=RULE and the -f RULE / -f=RULE short forms work)\n");
|
||||
printf(" -C, --cvs-exclude Auto-ignore common CVS/SCM files (.git/, .svn/, *.o, *~, ...)\n");
|
||||
printf(" -F Apply per-directory .rsync-filter files during the scan\n");
|
||||
printf(" --max-size <n> Skip files larger than n bytes\n");
|
||||
@@ -143,10 +144,11 @@ void print_usage(void) {
|
||||
DELTA_BLOCK_SIZE_DEFAULT);
|
||||
printf(" --delta-max <n> Max file size for delta transfer (default: %llu)\n",
|
||||
DELTA_MAX_FILE_SIZE);
|
||||
printf(" -m Enable multithreading\n");
|
||||
printf(" -s Enable chunk serialization\n");
|
||||
printf(" --secluded-args Accept rsync compatibility option (no effect)\n");
|
||||
printf(" -f Enable sendfile (TCP only, not with -c or -s)\n");
|
||||
printf(" -j, --threads Enable multithreading\n");
|
||||
printf(" --chunk-serialization Enable chunk serialization (long form only)\n");
|
||||
printf(" -s, --secluded-args Protect-args compatibility option (no effect; remote\n");
|
||||
printf(" SSH argv is already built injection-safe)\n");
|
||||
printf(" --sendfile Enable sendfile zero-copy (TCP only; long form only)\n");
|
||||
printf(" --compress-choice <alg> Compression algorithm (default: zstd)\n");
|
||||
printf(" --zc <alg> Alias for --compress-choice\n");
|
||||
printf(" -v, --verbose Enable debug logging\n");
|
||||
@@ -154,7 +156,7 @@ void print_usage(void) {
|
||||
printf(" --debug=FLAGS Fine-grained debug logging (use --debug=help for flags)\n");
|
||||
printf(" --info=FLAGS Fine-grained info: copy,misc,skip,stats,all,none\n");
|
||||
printf(" none suppresses info even with --verbose\n");
|
||||
printf(" -M, --preserve Preserve file metadata\n");
|
||||
printf(" --preserve Preserve file metadata (long form only)\n");
|
||||
printf(" -E, --executability Preserve executable permission bits\n");
|
||||
printf(" -X, --xattrs Preserve user extended attributes (user.* only;\n");
|
||||
printf(" privileged security.*/trusted.* namespaces are\n");
|
||||
@@ -178,8 +180,8 @@ void print_usage(void) {
|
||||
printf(" USER:GROUP, USER (owner only), :GROUP (group only); a\n");
|
||||
printf(" value of * means the current/root user as appropriate.\n");
|
||||
printf(" Names resolve on the source machine; @N for numerics.\n");
|
||||
printf(" Note: -M is already FastSync's preserve flag; these use\n");
|
||||
printf(" long forms only.\n");
|
||||
printf(" (Metadata is enabled with --preserve; -M now means\n");
|
||||
printf(" rsync's --remote-option.)\n");
|
||||
printf(" --chunk-size <n> Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE);
|
||||
printf(" --source-dir <path> Source directory\n");
|
||||
printf(" --dest-dir <path> Destination directory\n");
|
||||
@@ -197,8 +199,7 @@ void print_usage(void) {
|
||||
printf(" --cert <path> TLS certificate file (PEM)\n");
|
||||
printf(" --key <path> TLS private key file (PEM)\n");
|
||||
printf(" --ca <path> TLS CA certificate file (PEM)\n");
|
||||
printf(" --timeout <sec> I/O timeout in seconds (default: 30)\n");
|
||||
printf(" -T <sec> Alias for --timeout\n");
|
||||
printf(" --timeout <sec> I/O timeout in seconds (default: 30; long form only)\n");
|
||||
printf(" --contimeout <sec> Connection timeout in seconds (default: 10)\n");
|
||||
printf(" --stop-after=MINS Stop the transfer after MINS minutes (a positive\n");
|
||||
printf(" integer); whatever was already transferred is kept\n");
|
||||
@@ -227,17 +228,15 @@ void print_usage(void) {
|
||||
printf(" --stderr=MODE Route logging to stderr: errors or all\n");
|
||||
printf(" --partial Keep partial files on interrupted transfer\n");
|
||||
printf(" --partial-dir <dir> Directory for partial files\n");
|
||||
printf(" --temp-dir <dir> Scratch dir for temp files before atomic install\n");
|
||||
printf(" -T, --temp-dir <dir> Scratch dir for temp files before atomic install\n");
|
||||
printf(" --fastsync-server-path <path>\n");
|
||||
printf(" Path to fastsync-server on remote (default: fastsync-server)\n");
|
||||
printf(
|
||||
" --old-args Disable safe SSH command argument quoting (legacy compatibility)\n");
|
||||
printf(" --remote-option=OPT Append OPT to the REMOTE server invocation over SSH\n");
|
||||
printf(" -M, --remote-option=OPT Append OPT to the REMOTE server invocation over SSH\n");
|
||||
printf(" (repeatable; each value is single-quote-escaped on the remote\n");
|
||||
printf(" command line; empty values and values with control characters\n");
|
||||
printf(" are rejected). Long form only: rsync's -M short form is NOT\n");
|
||||
printf(" available because -M already means metadata preservation in\n");
|
||||
printf(" FastSync (documented divergence)\n");
|
||||
printf(" are rejected; -M OPT, -M=OPT and --remote-option=OPT work)\n");
|
||||
printf(" --trust-sender Trust the remote sender's file list: the receiver skips its\n");
|
||||
printf(" own up-front path-traversal/containment re-validation of the\n");
|
||||
printf(" incoming file list (fewer checks, faster, potentially unsafe).\n");
|
||||
|
||||
@@ -138,7 +138,7 @@ class TestAppend:
|
||||
self._place(source, REL, prefix + added)
|
||||
self._place(self._dest_file(source, dest, ""), REL, prefix)
|
||||
|
||||
result, _ = run_client(source, dest, flags=["--append", "-m"], port=shared_server.port)
|
||||
result, _ = run_client(source, dest, flags=["--append", "--threads"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--append -m failed: {(result.stderr or result.stdout)[:400]}"
|
||||
assert self._read(self._dest_file(source, dest, ""), REL) == prefix + added
|
||||
+117
-117
@@ -108,7 +108,7 @@ class TestDeviceSpecial:
|
||||
self._setup()
|
||||
os.mkfifo(os.path.join(DEVICE_SOURCE, "pipe.fifo"))
|
||||
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||
flags=["-m", "--remove-source-files", "--specials"],
|
||||
flags=["--threads", "--remove-source-files", "--specials"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, (
|
||||
f"Exit {result.returncode}: {result.stderr[:300]}"
|
||||
@@ -129,7 +129,7 @@ class TestDeviceSpecial:
|
||||
src_dev = os.path.join(DEVICE_SOURCE, "realdev")
|
||||
os.mknod(src_dev, stat.S_IFCHR | 0o666, os.makedev(1, 3))
|
||||
result, _ = run_client(DEVICE_SOURCE, DEVICE_DEST,
|
||||
flags=["-m", "--remove-source-files", "--devices"],
|
||||
flags=["--threads", "--remove-source-files", "--devices"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, (
|
||||
f"Exit {result.returncode}: {result.stderr[:300]}"
|
||||
@@ -263,7 +263,7 @@ class TestRemoveSourceFiles:
|
||||
os.makedirs(os.path.join(source, "directory"))
|
||||
os.symlink("one.txt", os.path.join(source, "link.txt"))
|
||||
|
||||
result, _ = run_client(source, dest, flags=["--remove-source-files", "-m"],
|
||||
result, _ = run_client(source, dest, flags=["--remove-source-files", "--threads"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"Remove-source sync failed: {result.stderr[:200]}"
|
||||
assert not os.path.exists(os.path.join(source, "one.txt"))
|
||||
@@ -323,7 +323,7 @@ class TestRemoveSourceFiles:
|
||||
# The seed run preserves timestamps (-M) so the destination copy has the
|
||||
# source's exact mtime; otherwise the incremental skip would depend on
|
||||
# both writes landing in the same whole second (a race).
|
||||
result, _ = run_client(source, dest, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(source, dest, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["--remove-source-files", "--incremental"],
|
||||
@@ -352,7 +352,7 @@ class TestArchiveMode:
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["--archive", "--no-compress", "--no-m", "--no-preserve"],
|
||||
flags=["--archive", "--no-links", "--no-preserve"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
@@ -477,7 +477,7 @@ class TestSkipCompress:
|
||||
f.write((b"skip compression case test\n" * 100))
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-c", "--skip-compress=.txt"],
|
||||
flags=["-z", "--skip-compress=.txt"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Skip-compress sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
@@ -489,7 +489,7 @@ class TestSkipCompress:
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-c", "--skip-compress="],
|
||||
flags=["-z", "--skip-compress="],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Empty skip-compress sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
@@ -503,7 +503,7 @@ class TestSkipCompress:
|
||||
path = os.path.join(SOURCE_DIR, "incremental-skip.TXT")
|
||||
with open(path, "wb") as f:
|
||||
f.write(b"original skipped content\n")
|
||||
flags = ["-c", "-M", "--skip-compress=.txt"]
|
||||
flags = ["-z", "--preserve", "--skip-compress=.txt"]
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"Initial sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
with open(path, "wb") as f:
|
||||
@@ -521,7 +521,7 @@ class TestSkipCompress:
|
||||
def test_skip_compress_rejects_chunk_serialization(self, shared_server):
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-c", "-s", "--skip-compress=.txt"],
|
||||
flags=["-z", "--chunk-serialization", "--skip-compress=.txt"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode != 0
|
||||
@@ -620,7 +620,7 @@ class TestIncremental:
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M"],
|
||||
flags=["--preserve"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"First sync failed: {result.stderr[:100]}"
|
||||
@@ -628,7 +628,7 @@ class TestIncremental:
|
||||
start = time.monotonic()
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental"],
|
||||
flags=["--preserve", "--incremental"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
incremental_time = time.monotonic() - start
|
||||
@@ -645,7 +645,7 @@ class TestIncremental:
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M"],
|
||||
flags=["--preserve"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
@@ -656,7 +656,7 @@ class TestIncremental:
|
||||
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental"],
|
||||
flags=["--preserve", "--incremental"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
@@ -673,7 +673,7 @@ class TestIncremental:
|
||||
|
||||
def test_checksum_detects_same_size_and_mtime_change(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
@@ -685,7 +685,7 @@ class TestIncremental:
|
||||
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime))
|
||||
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental", "--checksum"],
|
||||
flags=["--preserve", "--incremental", "--checksum"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"Checksum sync failed: {result.stderr[:200]}"
|
||||
with open(received_file, "rb") as f:
|
||||
@@ -693,7 +693,7 @@ class TestIncremental:
|
||||
|
||||
def test_size_only_skips_same_size_with_different_mtime(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
@@ -704,7 +704,7 @@ class TestIncremental:
|
||||
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental", "--size-only"],
|
||||
flags=["--preserve", "--incremental", "--size-only"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Size-only sync failed: {result.stderr[:200]}"
|
||||
@@ -713,7 +713,7 @@ class TestIncremental:
|
||||
|
||||
def test_ignore_times_transfers_same_size_and_mtime(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
@@ -725,7 +725,7 @@ class TestIncremental:
|
||||
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime))
|
||||
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental", "--ignore-times"],
|
||||
flags=["--preserve", "--incremental", "--ignore-times"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"Ignore-times sync failed: {result.stderr[:200]}"
|
||||
with open(received_file, "rb") as f:
|
||||
@@ -733,7 +733,7 @@ class TestIncremental:
|
||||
|
||||
def test_modify_window_allows_subsecond_mtime_difference(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
@@ -746,7 +746,7 @@ class TestIncremental:
|
||||
source_stat.st_mtime_ns - 1500000000))
|
||||
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--incremental", "--modify-window=2"],
|
||||
flags=["--preserve", "--incremental", "--modify-window=2"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"Modify-window sync failed: {result.stderr[:200]}"
|
||||
with open(received_file, "rb") as f:
|
||||
@@ -754,7 +754,7 @@ class TestIncremental:
|
||||
|
||||
def test_whole_file_disables_delta_and_keeps_compression(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
source_file = os.path.join(SOURCE_DIR, "medium.txt")
|
||||
@@ -764,7 +764,7 @@ class TestIncremental:
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR,
|
||||
DEST_DIR,
|
||||
flags=["-M", "--incremental", "--delta", "-W", "-c"],
|
||||
flags=["--preserve", "--incremental", "--delta", "-W", "-z"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Whole-file sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
@@ -795,11 +795,11 @@ class TestChecksumChoice:
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_unchanged_skipped_and_bytes_preserved(self, shared_server, algo, mt):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||
|
||||
flags = (["-M", "--incremental", "--checksum", f"--checksum-choice={algo}"] +
|
||||
(["-m"] if mt else []))
|
||||
flags = (["--preserve", "--incremental", "--checksum", f"--checksum-choice={algo}"] +
|
||||
(["--threads"] if mt else []))
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"checksum {algo} run failed: {result.stderr[:200]}"
|
||||
|
||||
@@ -816,7 +816,7 @@ class TestChecksumChoice:
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_changed_same_size_mtime_redetected(self, shared_server, algo, mt):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
@@ -827,8 +827,8 @@ class TestChecksumChoice:
|
||||
f.write(b"DDDDDDDDDDDD") # same size, different content
|
||||
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime))
|
||||
|
||||
flags = (["-M", "--incremental", "--checksum", f"--checksum-choice={algo}"] +
|
||||
(["-m"] if mt else []))
|
||||
flags = (["--preserve", "--incremental", "--checksum", f"--checksum-choice={algo}"] +
|
||||
(["--threads"] if mt else []))
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"checksum {algo} redetect failed: {result.stderr[:200]}"
|
||||
with open(received_file, "rb") as f:
|
||||
@@ -840,10 +840,10 @@ class TestChecksumChoice:
|
||||
# small handshake travels, not the payloads. Proxy byte counts are not
|
||||
# available for -m (multithreaded connections), so single-thread only.
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0
|
||||
|
||||
flags = ["-M", "--incremental", "--checksum", f"--checksum-choice={algo}"]
|
||||
flags = ["--preserve", "--incremental", "--checksum", f"--checksum-choice={algo}"]
|
||||
proxy = CountingProxy(shared_server.port)
|
||||
cmd = (CLIENT_CMD + ["--source-dir", SOURCE_DIR, "--dest-dir", DEST_DIR,
|
||||
"--save-to-disk", "--server-port", str(proxy.port)] + flags)
|
||||
@@ -855,8 +855,8 @@ class TestChecksumChoice:
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_seed_is_deterministic_and_preserves_content(self, shared_server, mt):
|
||||
clean_dir(DEST_DIR)
|
||||
flags = ["-M", "--incremental", "--checksum",
|
||||
"--checksum-choice=xxh64", "--checksum-seed=987654"] + (["-m"] if mt else [])
|
||||
flags = ["--preserve", "--incremental", "--checksum",
|
||||
"--checksum-choice=xxh64", "--checksum-seed=987654"] + (["--threads"] if mt else [])
|
||||
first, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port)
|
||||
assert first.returncode == 0, f"seeded run failed: {first.stderr[:200]}"
|
||||
|
||||
@@ -894,7 +894,7 @@ class TestChecksumChoice:
|
||||
big = os.path.join(source, "big.bin")
|
||||
with open(big, "wb") as f:
|
||||
f.write(bytes(range(256)) * 200) # 51200 bytes > delta 16K floor
|
||||
result, _ = run_client(source, dest, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(source, dest, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed delta seed failed: {result.stderr[:200]}"
|
||||
|
||||
# Edit a region so the receiver must match a changed block with the seed.
|
||||
@@ -906,8 +906,8 @@ class TestChecksumChoice:
|
||||
# edit and the prior sync share a second. Setting an old dest mtime
|
||||
# guarantees the delta path is exercised deterministically.
|
||||
os.utime(os.path.join(get_dest_received_dir(dest, source), "big.bin"), (0, 0))
|
||||
flags = (["-M", "--incremental", "--delta", "--checksum-seed=314159"] +
|
||||
(["-m"] if mt else []))
|
||||
flags = (["--preserve", "--incremental", "--delta", "--checksum-seed=314159"] +
|
||||
(["--threads"] if mt else []))
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed delta run failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -975,7 +975,7 @@ class TestExisting:
|
||||
@pytest.mark.ci
|
||||
def test_existing_updates_existing_and_skips_new(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"Initial sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
|
||||
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||
@@ -988,7 +988,7 @@ class TestExisting:
|
||||
|
||||
try:
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--existing"], port=shared_server.port)
|
||||
flags=["--preserve", "--existing"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"--existing sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
|
||||
with open(os.path.join(received, "small.txt"), "rb") as f:
|
||||
@@ -1060,7 +1060,7 @@ class TestDelete:
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M"],
|
||||
flags=["--preserve"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
@@ -1076,7 +1076,7 @@ class TestDelete:
|
||||
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "--delete"],
|
||||
flags=["--preserve", "--delete"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
|
||||
@@ -1119,7 +1119,7 @@ class TestProgress:
|
||||
clean_dir(DEST_DIR)
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-m", "-h", "--progress"],
|
||||
flags=["--threads", "-h", "--progress"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}"
|
||||
@@ -1145,7 +1145,7 @@ class TestInfo:
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-m", "--info=stats"],
|
||||
flags=["--threads", "--info=stats"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"Info stats sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||
@@ -1277,7 +1277,7 @@ class TestRemoveSourceFilesSkips:
|
||||
assert result.returncode == 0
|
||||
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["--remove-source-files", "--ignore-existing", "-m"],
|
||||
flags=["--remove-source-files", "--ignore-existing", "--threads"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"Sync failed: {result.stderr[:200]}"
|
||||
# Destination already has the file, so the receiver (writer thread)
|
||||
@@ -1425,7 +1425,7 @@ class TestOneFileSystem:
|
||||
source = os.path.join(TEST_DATA_DIR, "ofs_m_src")
|
||||
self._make_tree(source)
|
||||
self._assert_full_tree_transferred(source, os.path.join(TEST_DATA_DIR, "ofs_m_dst"),
|
||||
shared_server.port, ["-m", "--one-file-system"])
|
||||
shared_server.port, ["--threads", "--one-file-system"])
|
||||
|
||||
def test_x_skips_other_device_mountpoint(self, shared_server):
|
||||
if os.geteuid() != 0 or shutil.which("mount") is None or shutil.which("umount") is None:
|
||||
@@ -1518,7 +1518,7 @@ class TestTempDir:
|
||||
source = self._make_source("tempdir_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "tempdir_dst")
|
||||
clean_dir(dest)
|
||||
flags = ["--temp-dir=scratch"] + (["-m"] if mt else [])
|
||||
flags = ["--temp-dir=scratch"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"temp-dir sync failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -1629,7 +1629,7 @@ class TestListOnly:
|
||||
|
||||
def test_list_only_multithreaded(self):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--list-only", "-m"])
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--list-only", "--threads"])
|
||||
assert result.returncode == 0, f"list-only -m failed: {result.stderr[:200]}"
|
||||
for full_path in _source_files():
|
||||
assert full_path in result.stdout, f"list-only -m omitted {full_path}"
|
||||
@@ -1643,7 +1643,7 @@ class TestItemizeChanges:
|
||||
def test_first_run_prints_sent_lines(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "-i"], port=shared_server.port)
|
||||
flags=["--preserve", "-i"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"itemize sync failed: {result.stderr[:200]}"
|
||||
sent_lines = {">f+++++++++ " + p for p in _source_files()}
|
||||
assert sent_lines <= set(result.stdout.splitlines()), (
|
||||
@@ -1652,10 +1652,10 @@ class TestItemizeChanges:
|
||||
|
||||
def test_incremental_second_run_prints_no_line_for_unchanged(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "-i", "--incremental"],
|
||||
flags=["--preserve", "-i", "--incremental"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"incremental itemize failed: {result.stderr[:200]}"
|
||||
itemized = [line for line in result.stdout.splitlines() if line and line[0] in ">.<c"]
|
||||
@@ -1664,7 +1664,7 @@ class TestItemizeChanges:
|
||||
def test_multithreaded_emits_same_itemize_lines(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["-M", "-i", "-m"], port=shared_server.port)
|
||||
flags=["--preserve", "-i", "--threads"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"itemize -m sync failed: {result.stderr[:200]}"
|
||||
sent_lines = {">f+++++++++ " + p for p in _source_files()}
|
||||
assert sent_lines <= set(result.stdout.splitlines()), (
|
||||
@@ -1690,14 +1690,14 @@ class TestItemizeChanges:
|
||||
with open(untouched, "wb") as fh:
|
||||
fh.write(b"stable\n")
|
||||
|
||||
result, _ = run_client(source, dest, flags=["-M"], port=shared_server.port)
|
||||
result, _ = run_client(source, dest, flags=["--preserve"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||
|
||||
with open(changed, "wb") as fh:
|
||||
fh.write(b"edited payload\n")
|
||||
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["-M", "-i", "--incremental"],
|
||||
flags=["--preserve", "-i", "--incremental"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"incremental itemize failed: {result.stderr[:200]}"
|
||||
itemized = [line for line in result.stdout.splitlines() if line.startswith(">f")]
|
||||
@@ -1724,7 +1724,7 @@ class TestOutFormat:
|
||||
def test_out_format_multithreaded_matches_single(self, shared_server):
|
||||
clean_dir(DEST_DIR)
|
||||
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=["--out-format=%f %l", "-m"], port=shared_server.port)
|
||||
flags=["--out-format=%f %l", "--threads"], port=shared_server.port)
|
||||
assert result.returncode == 0, f"out-format -m sync failed: {result.stderr[:200]}"
|
||||
expected = {f"{p} {os.path.getsize(p)}" for p in _source_files()}
|
||||
got = set(result.stdout.splitlines())
|
||||
@@ -1767,7 +1767,7 @@ class TestLogFileFormat:
|
||||
result, _ = run_client(
|
||||
source,
|
||||
dest,
|
||||
flags=["--log-file", log_path, "--log-file-format=%f %l", "-m"],
|
||||
flags=["--log-file", log_path, "--log-file-format=%f %l", "--threads"],
|
||||
port=shared_server.port,
|
||||
)
|
||||
assert result.returncode == 0, f"log-file -m sync failed: {result.stderr[:200]}"
|
||||
@@ -1812,7 +1812,7 @@ class TestDelayUpdates:
|
||||
|
||||
result, _ = run_client(source, plain_dest, port=shared_server.port)
|
||||
assert result.returncode == 0, f"plain sync failed: {result.stderr[:200]}"
|
||||
flags = ["--delay-updates"] + (["-m"] if mt else [])
|
||||
flags = ["--delay-updates"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, delay_dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"delay-updates sync failed: {result.stderr[:200]}"
|
||||
|
||||
@@ -1834,7 +1834,7 @@ class TestDelayUpdates:
|
||||
source = self._make_source("delay_rerun_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "delay_rerun_dst")
|
||||
clean_dir(dest)
|
||||
flags = ["--delay-updates", "-M", "--incremental"] + (["-m"] if mt else [])
|
||||
flags = ["--delay-updates", "--preserve", "--incremental"] + (["--threads"] if mt else [])
|
||||
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"first delayed sync failed: {result.stderr[:200]}"
|
||||
@@ -1853,7 +1853,7 @@ class TestDelayUpdates:
|
||||
source = self._make_source("delay_rsf_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "delay_rsf_dst")
|
||||
clean_dir(dest)
|
||||
flags = ["--remove-source-files", "--delay-updates"] + (["-m"] if mt else [])
|
||||
flags = ["--remove-source-files", "--delay-updates"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"delayed remove-source sync failed: {result.stderr[:200]}"
|
||||
|
||||
@@ -1891,7 +1891,7 @@ class TestDelayUpdates:
|
||||
fh.write(b"BBBB")
|
||||
os.remove(os.path.join(source, "extra.txt"))
|
||||
|
||||
flags = ["--delete", "--delay-updates"] + (["-m"] if mt else [])
|
||||
flags = ["--delete", "--delay-updates"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"delete+delay-updates sync failed: {result.stderr[:200]}"
|
||||
@@ -1945,7 +1945,7 @@ class TestDelayUpdates:
|
||||
with open(os.path.join(received, "sub"), "wb") as fh:
|
||||
fh.write(b"blocks the nested destination directory")
|
||||
|
||||
flags = ["--delay-updates"] + (["-m"] if mt else [])
|
||||
flags = ["--delay-updates"] + (["--threads"] if mt else [])
|
||||
if remove_source_files:
|
||||
flags += ["--remove-source-files"]
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
@@ -1982,7 +1982,7 @@ class TestDelayUpdates:
|
||||
fh.write(b"changed on source")
|
||||
with open(os.path.join(source, "deliver.txt"), "wb") as fh:
|
||||
fh.write(b"new file")
|
||||
flags = ["--remove-source-files", "--ignore-existing", "--delay-updates"] + (["-m"] if mt else [])
|
||||
flags = ["--remove-source-files", "--ignore-existing", "--delay-updates"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"delayed skip sync failed: {result.stderr[:200]}"
|
||||
# keep.txt already existed at the destination: receiver skip -> source stays.
|
||||
@@ -2045,7 +2045,7 @@ class TestRelativeFilesFrom:
|
||||
dest = os.path.join(TEST_DATA_DIR, "rel_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"top.txt\nsub/x.txt\n")
|
||||
flags = ["--files-from", lst, "-R"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "-R"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"-R files-from sync failed: {result.stderr[:200]}"
|
||||
assert _read_file(os.path.join(dest, "sub", "x.txt")) == b"x\n", \
|
||||
@@ -2064,7 +2064,7 @@ class TestRelativeFilesFrom:
|
||||
source = _make_relative_source("rel_only_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "rel_only_dst")
|
||||
clean_dir(dest)
|
||||
flags = ["-R"] + (["-m"] if mt else [])
|
||||
flags = ["-R"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"-R alone sync failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -2077,7 +2077,7 @@ class TestRelativeFilesFrom:
|
||||
dest = os.path.join(TEST_DATA_DIR, "rel_noR_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"sub/x.txt\n")
|
||||
flags = ["--files-from", lst] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"files-from sync failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -2140,7 +2140,7 @@ class TestMissingArgs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "mg_default_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"a.txt\ngone.txt\nsub/b.txt\n")
|
||||
flags = ["--files-from", lst] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode != 0, "a listed-but-missing entry did not fail the run"
|
||||
assert "gone.txt" in (result.stderr or result.stdout)
|
||||
@@ -2154,7 +2154,7 @@ class TestMissingArgs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "mg_ignore_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"a.txt\ngone.txt\nsub/b.txt\n")
|
||||
flags = ["--files-from", lst, "--ignore-missing-args"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "--ignore-missing-args"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"ignore-missing-args sync failed: {result.stderr[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -2171,7 +2171,7 @@ class TestMissingArgs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "mg_all_missing_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"gone1.txt\ngone2.txt\n")
|
||||
flags = ["--files-from", lst, "--ignore-missing-args"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "--ignore-missing-args"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"all-missing run should succeed (rsync parity): {result.stderr[:300]}"
|
||||
@@ -2184,7 +2184,7 @@ class TestMissingArgs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "mg_empty_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"")
|
||||
flags = ["--files-from", lst, "--ignore-missing-args"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "--ignore-missing-args"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode != 0, "an empty --files-from list must stay a hard error"
|
||||
assert "contains no entries" in (result.stderr or result.stdout)
|
||||
@@ -2201,7 +2201,7 @@ class TestMissingArgs:
|
||||
server.start(extra_args=["--allow-delete"])
|
||||
seed = _write_rel_list(b"a.txt\nsub/b.txt\n")
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["--files-from", seed, "-R"] + (["-m"] if mt else []),
|
||||
flags=["--files-from", seed, "-R"] + (["--threads"] if mt else []),
|
||||
port=server.port)
|
||||
assert result.returncode == 0, f"seed -R sync failed: {result.stderr[:200]}"
|
||||
assert os.path.isfile(os.path.join(dest, "a.txt"))
|
||||
@@ -2214,7 +2214,7 @@ class TestMissingArgs:
|
||||
fh.write("unrelated")
|
||||
|
||||
lst = _write_rel_list(b"a.txt\ngone.txt\nsub/b.txt\n")
|
||||
flags = ["--files-from", lst, "-R", "--delete-missing-args"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "-R", "--delete-missing-args"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, f"delete-missing sync failed: {result.stderr[:300]}"
|
||||
assert not os.path.exists(os.path.join(dest, "gone.txt")), \
|
||||
@@ -2227,7 +2227,7 @@ class TestMissingArgs:
|
||||
# Now with --delete the unrelated extra is an ordinary extra and must go.
|
||||
lst2 = _write_rel_list(b"a.txt\ngone.txt\nsub/b.txt\n")
|
||||
flags2 = ["--files-from", lst2, "-R", "--delete-missing-args", "--delete"] + \
|
||||
(["-m"] if mt else [])
|
||||
(["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags2, port=server.port)
|
||||
assert result.returncode == 0, f"delete-missing + delete sync failed: {result.stderr[:300]}"
|
||||
assert not os.path.exists(os.path.join(dest, "unrelated.txt")), \
|
||||
@@ -2287,7 +2287,7 @@ class TestMissingArgs:
|
||||
|
||||
lst = _write_rel_list(b"a.txt\nprot/gone.txt\n")
|
||||
flags = ["--files-from", lst, "--filter=- prot/", "--delete-missing-args",
|
||||
"--delete"] + (["-m"] if mt else [])
|
||||
"--delete"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, f"delete-missing exclude sync failed: {result.stderr[:300]}"
|
||||
assert not os.path.exists(os.path.join(received, "prot", "gone.txt")), \
|
||||
@@ -2314,7 +2314,7 @@ class TestMissingArgs:
|
||||
server.start(extra_args=["--allow-delete"])
|
||||
lst = _write_rel_list(b"a.txt\ngone.txt\ngone2.txt\n")
|
||||
flags = ["--files-from", lst, "-R", "--delete-missing-args", "--delete-before"] + \
|
||||
(["-m"] if mt else [])
|
||||
(["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, f"early delete-missing sync failed: {result.stderr[:300]}"
|
||||
assert not os.path.exists(os.path.join(dest, "gone.txt")), \
|
||||
@@ -2353,7 +2353,7 @@ class TestMissingArgs:
|
||||
|
||||
lst = _write_rel_list(b"a.txt\nsub/gone.txt\n")
|
||||
flags = ["--files-from", lst, "--delete-missing-args", "--delete"] + rel_flags + \
|
||||
(["-m"] if mt else [])
|
||||
(["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"deep missing-entry sync failed: {result.stderr[:300]}"
|
||||
@@ -2373,7 +2373,7 @@ class TestMissingArgs:
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"a.txt\ngone.txt\n")
|
||||
flags = ["--files-from", lst, "--dirs", "-R", "--ignore-missing-args"] + \
|
||||
(["-m"] if mt else [])
|
||||
(["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"--dirs ignore-missing sync failed: {result.stderr[:300]}"
|
||||
assert _read_file(os.path.join(dest, "a.txt")) == b"a\n", \
|
||||
@@ -2395,7 +2395,7 @@ class TestNoImpliedDirs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "noimplied_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"a/b.txt\n") # "a" itself is not listed
|
||||
flags = ["--files-from", lst, "-R", "--no-implied-dirs"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "-R", "--no-implied-dirs"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode != 0, "implied parent directory was not rejected"
|
||||
assert "--no-implied-dirs" in (result.stderr or result.stdout)
|
||||
@@ -2407,7 +2407,7 @@ class TestNoImpliedDirs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "noimplied_ok_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"a\na/b.txt\n")
|
||||
flags = ["--files-from", lst, "-R", "--no-implied-dirs"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "-R", "--no-implied-dirs"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"listed dir + file sync failed: {result.stderr[:200]}"
|
||||
assert _read_file(os.path.join(dest, "a", "b.txt")) == b"nested\n"
|
||||
@@ -2418,7 +2418,7 @@ class TestNoImpliedDirs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "noimplied_noR_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"a/b.txt\n")
|
||||
flags = ["--files-from", lst, "--no-implied-dirs"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "--no-implied-dirs"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, "--no-implied-dirs without -R changed behavior"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -2446,7 +2446,7 @@ class TestDirs:
|
||||
source = self._make()
|
||||
dest = os.path.join(TEST_DATA_DIR, "dirs_dst")
|
||||
clean_dir(dest)
|
||||
flags = [flag] + (["-m"] if mt else [])
|
||||
flags = [flag] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"{flag} sync failed: {result.stderr[:200]}"
|
||||
self._assert_only_empty_mirror(dest, source)
|
||||
@@ -2458,7 +2458,7 @@ class TestDirs:
|
||||
clean_dir(dest)
|
||||
# A listed directory is created empty; a listed file is transferred.
|
||||
lst = _write_rel_list(b"dir1\nsub/x.txt\n")
|
||||
flags = ["--files-from", lst, "--dirs", "-R"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "--dirs", "-R"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"dirs files-from sync failed: {result.stderr[:200]}"
|
||||
assert os.path.isdir(os.path.join(dest, "dir1")), "listed dir was not created"
|
||||
@@ -2476,7 +2476,7 @@ class TestDirs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "dirs_ff_noR_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"dir1\n")
|
||||
flags = ["--files-from", lst, "--dirs"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "--dirs"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"dirs files-from no-R sync failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -2495,7 +2495,7 @@ class TestDirs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "dirs_s_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"dir1\nsub/x.txt\n")
|
||||
flags = ["--files-from", lst, "--dirs", "-R", "-s"] + (["-m"] if mt else [])
|
||||
flags = ["--files-from", lst, "--dirs", "-R", "--chunk-serialization"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"dirs -s sync failed: {result.stderr[:200]}"
|
||||
assert os.path.isdir(os.path.join(dest, "dir1")), "listed dir was not created"
|
||||
@@ -2554,7 +2554,7 @@ class TestMkpath:
|
||||
shutil.rmtree(dest, ignore_errors=True)
|
||||
with ServerManager() as server:
|
||||
server.start()
|
||||
flags = ["-m"] if mt else []
|
||||
flags = ["--threads"] if mt else []
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode != 0, "missing destination root did not fail without --mkpath"
|
||||
assert not os.path.exists(dest), "missing root was created without --mkpath"
|
||||
@@ -2566,7 +2566,7 @@ class TestMkpath:
|
||||
shutil.rmtree(os.path.join(TEST_DATA_DIR, "deep"), ignore_errors=True)
|
||||
with ServerManager() as server:
|
||||
server.start()
|
||||
flags = ["--mkpath"] + (["-m"] if mt else [])
|
||||
flags = ["--mkpath"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, f"--mkpath sync failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -2656,7 +2656,7 @@ class TestDeleteTiming:
|
||||
with open(extra, "wb") as fh:
|
||||
fh.write(b"should be deleted")
|
||||
|
||||
flags = [flag] + (["-m"] if mt else [])
|
||||
flags = [flag] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"{flag} sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -2687,7 +2687,7 @@ class TestDeleteTiming:
|
||||
with open(blocker, "wb") as fh:
|
||||
fh.write(b"blocks the nested destination directory")
|
||||
|
||||
flags = [flag] + (["-m"] if mt else [])
|
||||
flags = [flag] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"{flag} (early delete) did not remove the blocker in time: " \
|
||||
@@ -2721,7 +2721,7 @@ class TestDeleteTiming:
|
||||
with open(blocker, "wb") as fh:
|
||||
fh.write(b"blocks the nested destination directory")
|
||||
|
||||
flags = [flag] + (["-m"] if mt else [])
|
||||
flags = [flag] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode != 0, \
|
||||
f"{flag} (mt={mt}) unexpectedly succeeded (deletion must be deferred)"
|
||||
@@ -2804,7 +2804,7 @@ class TestDeletePolicy:
|
||||
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||
|
||||
# Default: the excluded mirrors survive --delete, genuine extras die.
|
||||
flags = ["--exclude", "*.log", timing] + (["-m"] if mt else [])
|
||||
flags = ["--exclude", "*.log", timing] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"default delete sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -2817,7 +2817,7 @@ class TestDeletePolicy:
|
||||
|
||||
# --delete-excluded: excluded mirrors are extras again and die.
|
||||
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||
flags = ["--exclude", "*.log", timing, "--delete-excluded"] + (["-m"] if mt else [])
|
||||
flags = ["--exclude", "*.log", timing, "--delete-excluded"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--delete-excluded sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -2846,7 +2846,7 @@ class TestDeletePolicy:
|
||||
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
|
||||
flags = ["--filter=- skipdir/", "--delete"] + (["-m"] if mt else [])
|
||||
flags = ["--filter=- skipdir/", "--delete"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"default delete sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -2855,7 +2855,7 @@ class TestDeletePolicy:
|
||||
assert os.path.exists(os.path.join(received, "skipdir", "deep", "b.log")), \
|
||||
"nested excluded dir content was deleted under plain --delete"
|
||||
|
||||
flags = ["--filter=- skipdir/", "--delete", "--delete-excluded"] + (["-m"] if mt else [])
|
||||
flags = ["--filter=- skipdir/", "--delete", "--delete-excluded"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--delete-excluded sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -2882,7 +2882,7 @@ class TestDeletePolicy:
|
||||
self._write(os.path.join(received, name), b"extra\n")
|
||||
extras.append(os.path.join(received, name))
|
||||
|
||||
flags = ["--max-delete=2", timing] + (["-m"] if mt else [])
|
||||
flags = ["--max-delete=2", timing] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode != 0, \
|
||||
f"--max-delete=2 with 4 extras unexpectedly succeeded: {result.stderr[:300]}"
|
||||
@@ -2906,7 +2906,7 @@ class TestDeletePolicy:
|
||||
received = get_dest_received_dir(dest, source)
|
||||
for i in range(3):
|
||||
self._write(os.path.join(received, f"e{i}.txt"), b"extra\n")
|
||||
flags = ["--max-delete=3", "--delete"] + (["-m"] if mt else [])
|
||||
flags = ["--max-delete=3", "--delete"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--max-delete=3 with 3 extras failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -2943,7 +2943,7 @@ class TestDeletePolicy:
|
||||
assert os.path.exists(os.path.join(received, "sub", "old.txt")), \
|
||||
"non-empty dir content was lost although the run failed without --force"
|
||||
|
||||
flags = ["--force"] + (["-m"] if mt else [])
|
||||
flags = ["--force"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--force run failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -3000,7 +3000,7 @@ class TestDeletePolicy:
|
||||
assert os.listdir(received) == []
|
||||
|
||||
# prune-empty-dirs: the empty mirror is pruned by --delete.
|
||||
flags = ["--dirs", "--prune-empty-dirs", "--delete"] + (["-m"] if mt else [])
|
||||
flags = ["--dirs", "--prune-empty-dirs", "--delete"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--dirs --prune-empty-dirs --delete failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -3012,7 +3012,7 @@ class TestDeletePolicy:
|
||||
clean_dir(dest2)
|
||||
with ServerManager() as server:
|
||||
server.start(extra_args=["--allow-delete"])
|
||||
flags = ["--dirs", "--prune-empty-dirs", "-i"] + (["-m"] if mt else [])
|
||||
flags = ["--dirs", "--prune-empty-dirs", "-i"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest2, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--dirs --prune-empty-dirs failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -3047,7 +3047,7 @@ class TestDeletePolicy:
|
||||
os.makedirs(os.path.join(received, "empty", "chain"))
|
||||
|
||||
for prune in ([], ["--prune-empty-dirs"]):
|
||||
flags = prune + ["--delete"] + (["-m"] if mt else [])
|
||||
flags = prune + ["--delete"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"prune recursive sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -3058,7 +3058,7 @@ class TestDeletePolicy:
|
||||
assert _read_file(os.path.join(received, "keep.txt")) == b"kept\n"
|
||||
|
||||
# An excluded file's mirror is protected: the dir that holds it stays.
|
||||
flags = ["--exclude", "*.log", "--delete", "--prune-empty-dirs"] + (["-m"] if mt else [])
|
||||
flags = ["--exclude", "*.log", "--delete", "--prune-empty-dirs"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"prune recursive sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -3100,7 +3100,7 @@ class TestDeletePolicy:
|
||||
|
||||
# Default: scan error aborts the run; nothing is deleted.
|
||||
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||
flags = ["--delete"] + (["-m"] if mt else [])
|
||||
flags = ["--delete"] + (["--threads"] if mt else [])
|
||||
result = self._run_client_as_nobody(source, dest, server.port, flags)
|
||||
assert result.returncode != 0, "unreadable source dir did not fail the run"
|
||||
assert os.path.exists(os.path.join(received, "extra.txt")), \
|
||||
@@ -3108,7 +3108,7 @@ class TestDeletePolicy:
|
||||
|
||||
# --ignore-errors: the readable tree transfers, deletion still runs.
|
||||
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||
flags = ["--delete", "--ignore-errors"] + (["-m"] if mt else [])
|
||||
flags = ["--delete", "--ignore-errors"] + (["--threads"] if mt else [])
|
||||
result = self._run_client_as_nobody(source, dest, server.port, flags)
|
||||
assert not os.path.exists(os.path.join(received, "extra.txt")), \
|
||||
f"--ignore-errors did not keep deletion active: {result.stderr[:300]}"
|
||||
@@ -3143,7 +3143,7 @@ class TestDeletePolicy:
|
||||
try:
|
||||
os.chmod(source, 0)
|
||||
self._write(os.path.join(received, "extra.txt"), b"extra\n")
|
||||
flags = [timing, "--ignore-errors"] + (["-m"] if mt else [])
|
||||
flags = [timing, "--ignore-errors"] + (["--threads"] if mt else [])
|
||||
result = self._run_client_as_nobody(source, dest, server.port, flags)
|
||||
assert result.returncode != 0, \
|
||||
f"unreadable source root with {timing} (mt={mt}) unexpectedly succeeded"
|
||||
@@ -3371,7 +3371,7 @@ class TestBasisDestDirs:
|
||||
dest = os.path.join(TEST_DATA_DIR, "basis_link_mt_dst")
|
||||
clean_dir(dest)
|
||||
basis = self._seed_basis(dest, source, "mtbasis", self._basis_tree("mt"))
|
||||
result, _ = run_client(source, dest, flags=["--link-dest=mtbasis", "-m"],
|
||||
result, _ = run_client(source, dest, flags=["--link-dest=mtbasis", "--threads"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, f"-m link-dest failed: {result.stderr[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -3665,7 +3665,7 @@ class TestFuzzy:
|
||||
with open(os.path.join(source, self.NEW_NAME), "wb") as fh:
|
||||
fh.write(new_bytes)
|
||||
|
||||
flags = ["--fuzzy"] + (["-m"] if mt else [])
|
||||
flags = ["--fuzzy"] + (["--threads"] if mt else [])
|
||||
result, proxy = self._run_measured(source, dest, flags, shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--fuzzy {'-m ' if mt else ''}rename failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -3926,7 +3926,7 @@ class TestIdentityMapping:
|
||||
with open(os.path.join(source, "f.txt"), "wb") as f:
|
||||
f.write(b"hello identity")
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["-M", "--numeric-ids"],
|
||||
flags=["--preserve", "--numeric-ids"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"exit {result.returncode}: {(result.stderr or '')[:200]}"
|
||||
@@ -3943,7 +3943,7 @@ class TestIdentityMapping:
|
||||
f.write(b"mapped")
|
||||
result, _ = run_client(
|
||||
source, dest,
|
||||
flags=["-M", "--usermap=@1000:@1001", "--groupmap=@100:@101", "--chown=@2000:@2001"],
|
||||
flags=["--preserve", "--usermap=@1000:@1001", "--groupmap=@100:@101", "--chown=@2000:@2001"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"exit {result.returncode}: {(result.stderr or '')[:200]}"
|
||||
@@ -3962,7 +3962,7 @@ class TestIdentityMapping:
|
||||
f.write(b"owner")
|
||||
os.chown(src_file, 12345, 12346)
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["-M", "--numeric-ids"],
|
||||
flags=["--preserve", "--numeric-ids"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"exit {result.returncode}: {(result.stderr or '')[:200]}"
|
||||
@@ -3984,7 +3984,7 @@ class TestIdentityMapping:
|
||||
f.write(b"root chown")
|
||||
os.chown(src_file, 1, 1)
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["-M", "--chown=@12345:@54321"],
|
||||
flags=["--preserve", "--chown=@12345:@54321"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"exit {result.returncode}: {(result.stderr or '')[:200]}"
|
||||
@@ -4013,7 +4013,7 @@ class TestHardLinks:
|
||||
fh.write(b"independent content\n" * 2000)
|
||||
return src
|
||||
|
||||
@pytest.mark.parametrize("flags", [[], ["-m"], ["--delay-updates"]])
|
||||
@pytest.mark.parametrize("flags", [[], ["--threads"], ["--delay-updates"]])
|
||||
def test_hard_links_preserved(self, shared_server, flags):
|
||||
src = self._make_source("hl_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "hl_dst")
|
||||
@@ -4041,7 +4041,7 @@ class TestHardLinks:
|
||||
src = self._make_source("hl_reject_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "hl_reject_dst")
|
||||
clean_dir(dest)
|
||||
result, _ = run_client(src, dest, flags=["-H", "-s"], port=shared_server.port)
|
||||
result, _ = run_client(src, dest, flags=["-H", "--chunk-serialization"], port=shared_server.port)
|
||||
assert result.returncode != 0, "-H with -s was accepted"
|
||||
|
||||
def test_hard_links_rejects_append(self, shared_server):
|
||||
@@ -4136,7 +4136,7 @@ class TestAtimes:
|
||||
source = os.path.join(TEST_DATA_DIR, f"atime_{'m' if mt else 's'}_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, f"atime_{'m' if mt else 's'}_dst")
|
||||
src_file, atime = self._make_source(source, dest)
|
||||
flags = ["-U"] + (["-m"] if mt else [])
|
||||
flags = ["-U"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-U failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -4158,7 +4158,7 @@ class TestAtimes:
|
||||
dest = os.path.join(TEST_DATA_DIR, f"atime_ctrl_{'m' if mt else 's'}_dst")
|
||||
src_file, atime = self._make_source(source, dest)
|
||||
now = time.time()
|
||||
flags = (["-m"] if mt else [])
|
||||
flags = (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"control run failed: {(result.stderr or '')[:200]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -4191,7 +4191,7 @@ class TestOpenNoatime:
|
||||
atime = 730486800 # 1993-02-11, distinct and far from now
|
||||
os.utime(path, ns=(atime * 10**9, atime * 10**9))
|
||||
|
||||
flags = ["--open-noatime"] + (["-m"] if mt else [])
|
||||
flags = ["--open-noatime"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--open-noatime failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -4220,7 +4220,7 @@ class TestCrtimes:
|
||||
with open(path, "wb") as f:
|
||||
f.write(payload)
|
||||
|
||||
flags = ["-N"] + (["-m"] if mt else [])
|
||||
flags = ["-N"] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-N failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -4267,7 +4267,7 @@ class TestOmitTimes:
|
||||
clean_dir(dest)
|
||||
with open(os.path.join(source, "a.txt"), "wb") as f:
|
||||
f.write(b"omit times content\n")
|
||||
flags = [flag] + (["-m"] if mt else [])
|
||||
flags = [flag] + (["--threads"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"{flag} failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -4289,7 +4289,7 @@ class TestOmitTimes:
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-d -O failed: {(result.stderr or result.stdout)[:300]}"
|
||||
result, _ = run_client(source, dest, flags=["-M", "-O", "-J"],
|
||||
result, _ = run_client(source, dest, flags=["--preserve", "-O", "-J"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-M -O -J failed: {(result.stderr or result.stdout)[:300]}"
|
||||
@@ -4536,7 +4536,7 @@ class TestExtendedAttributes:
|
||||
if not _xattr_supported(f):
|
||||
pytest.skip("filesystem does not support user xattrs")
|
||||
os.setxattr(f, "user.k", b"v")
|
||||
result, _ = run_client(source, dest, flags=["-X", "-m"], port=shared_server.port)
|
||||
result, _ = run_client(source, dest, flags=["-X", "--threads"], port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-X -m sync failed: {(result.stderr or result.stdout)[:300]}"
|
||||
received = get_dest_received_dir(dest, source)
|
||||
@@ -4552,7 +4552,7 @@ class TestExtendedAttributes:
|
||||
pytest.skip("filesystem does not support xattrs")
|
||||
acl_blob = None
|
||||
if shutil.which("setfacl") is not None:
|
||||
acl = subprocess.run(["setfacl", "-m", "o::r", f], capture_output=True, text=True)
|
||||
acl = subprocess.run(["setfacl", "--threads", "o::r", f], capture_output=True, text=True)
|
||||
if acl.returncode == 0:
|
||||
try:
|
||||
acl_blob = os.getxattr(f, "system.posix_acl_access")
|
||||
@@ -4662,7 +4662,7 @@ class TestConnectivityClientOptions:
|
||||
source, dest = self._source_and_dest("connopt_zlib")
|
||||
with open(os.path.join(source, "text.txt"), "wb") as f:
|
||||
f.write(b"compress me\n" * 4096)
|
||||
result, _ = run_client(source, dest, flags=["--blocking-io", "-c"],
|
||||
result, _ = run_client(source, dest, flags=["--blocking-io", "-z"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"--blocking-io -c failed: {(result.stderr or result.stdout)[:300]}"
|
||||
|
||||
@@ -243,7 +243,7 @@ def test_iconv_chunk_serialization_blob(shared_server):
|
||||
_place_bytes(source, name, data=b"blob\n")
|
||||
|
||||
result, _ = run_client(
|
||||
source, dest, flags=["--iconv=iso-8859-1,utf-8", "-s"], port=shared_server.port
|
||||
source, dest, flags=["--iconv=iso-8859-1,utf-8", "--chunk-serialization"], port=shared_server.port
|
||||
)
|
||||
assert result.returncode == 0, (result.stderr or result.stdout)[:400]
|
||||
|
||||
|
||||
@@ -97,31 +97,31 @@ class TestSSHStandard:
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithreading(self):
|
||||
r = _run_ssh_test("SSH Multithreading (-m)", ["-m"])
|
||||
r = _run_ssh_test("SSH Multithreading (-m)", ["--threads"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression(self):
|
||||
r = _run_ssh_test("SSH Compression (-c)", ["-c"])
|
||||
r = _run_ssh_test("SSH Compression (-c)", ["-z"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_chunk_serialization(self):
|
||||
r = _run_ssh_test("SSH Chunk Serialization (-s)", ["-s"])
|
||||
r = _run_ssh_test("SSH Chunk Serialization (-s)", ["--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression_chunk(self):
|
||||
r = _run_ssh_test("SSH Compression + Chunk (-c -s)", ["-c", "-s"])
|
||||
r = _run_ssh_test("SSH Compression + Chunk (-c -s)", ["-z", "--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_compression(self):
|
||||
r = _run_ssh_test("SSH Multithread + Compression (-m -c)", ["-m", "-c"])
|
||||
r = _run_ssh_test("SSH Multithread + Compression (-m -c)", ["--threads", "-z"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_chunk(self):
|
||||
r = _run_ssh_test("SSH Multithread + Chunk (-m -s)", ["-m", "-s"])
|
||||
r = _run_ssh_test("SSH Multithread + Chunk (-m -s)", ["--threads", "--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_all_flags(self):
|
||||
r = _run_ssh_test("SSH All Flags (-m -c -s)", ["-m", "-c", "-s"])
|
||||
r = _run_ssh_test("SSH All Flags (-m -c -s)", ["--threads", "-z", "--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
@@ -178,7 +178,7 @@ class TestSSHConnectivity:
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_blocking_io_with_compression(self):
|
||||
r = _run_ssh_test("SSH --blocking-io -c", ["--blocking-io", "-c"])
|
||||
r = _run_ssh_test("SSH --blocking-io -c", ["--blocking-io", "-z"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_trust_sender(self):
|
||||
|
||||
@@ -231,7 +231,7 @@ class TestStopDelete:
|
||||
"""-m immediate stop + --delete: the completion tail must not read the
|
||||
still-appendable manifest (no race) and must not delete the mirrors."""
|
||||
source, dest = self._seed("del_mt", shared_server.port, many=True)
|
||||
result, _ = run_client(source, dest, flags=["-m", "--delete", "--stop-at=now+0s"],
|
||||
result, _ = run_client(source, dest, flags=["--threads", "--delete", "--stop-at=now+0s"],
|
||||
port=shared_server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"-m --delete immediate stop failed: {(result.stderr or result.stdout)[:400]}"
|
||||
|
||||
@@ -30,11 +30,11 @@ def _run_tcp_test(name, port, flags, use_metadata=True, posix=False):
|
||||
clean_dir(DEST_DIR)
|
||||
if posix:
|
||||
result, dur = run_client_posix(SOURCE_DIR, DEST_DIR,
|
||||
flags=(["-M"] if use_metadata else []) + flags,
|
||||
flags=(["--preserve"] if use_metadata else []) + flags,
|
||||
port=port)
|
||||
else:
|
||||
result, dur = run_client(SOURCE_DIR, DEST_DIR,
|
||||
flags=(["-M"] if use_metadata else []) + flags,
|
||||
flags=(["--preserve"] if use_metadata else []) + flags,
|
||||
port=port)
|
||||
|
||||
if result.returncode != 0:
|
||||
@@ -68,45 +68,45 @@ class TestTCPStandard:
|
||||
class TestTCPFlags:
|
||||
@pytest.mark.ci
|
||||
def test_multithreading(self, shared_server):
|
||||
r = _run_tcp_test("Multithreading (-m)", shared_server.port, ["-m"])
|
||||
r = _run_tcp_test("Multithreading (-m)", shared_server.port, ["--threads"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
@pytest.mark.ci
|
||||
def test_compression(self, shared_server):
|
||||
r = _run_tcp_test("Compression (-c)", shared_server.port, ["-c"])
|
||||
r = _run_tcp_test("Compression (-c)", shared_server.port, ["-z"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression_threads(self, shared_server):
|
||||
r = _run_tcp_test("Compression threads (-c --compress-threads=2)", shared_server.port,
|
||||
["-c", "--compress-threads=2"])
|
||||
["-z", "--compress-threads=2"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_chunk_serialization(self, shared_server):
|
||||
r = _run_tcp_test("Chunk Serialization (-s)", shared_server.port, ["-s"])
|
||||
r = _run_tcp_test("Chunk Serialization (-s)", shared_server.port, ["--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_compression_chunk(self, shared_server):
|
||||
r = _run_tcp_test("Compression + Chunk (-c -s)", shared_server.port, ["-c", "-s"])
|
||||
r = _run_tcp_test("Compression + Chunk (-c -s)", shared_server.port, ["-z", "--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_compression(self, shared_server):
|
||||
r = _run_tcp_test("Multithreading + Compression (-m -c)", shared_server.port, ["-m", "-c"])
|
||||
r = _run_tcp_test("Multithreading + Compression (-m -c)", shared_server.port, ["--threads", "-z"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_multithread_chunk(self, shared_server):
|
||||
r = _run_tcp_test("Multithreading + Chunk (-m -s)", shared_server.port, ["-m", "-s"])
|
||||
r = _run_tcp_test("Multithreading + Chunk (-m -s)", shared_server.port, ["--threads", "--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_all_flags(self, shared_server):
|
||||
r = _run_tcp_test("Multithread + Compression + Chunk (-m -c -s)", shared_server.port, ["-m", "-c", "-s"])
|
||||
r = _run_tcp_test("Multithread + Compression + Chunk (-m -c -s)", shared_server.port, ["--threads", "-z", "--chunk-serialization"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_sendfile(self, shared_server):
|
||||
r = _run_tcp_test("Sendfile (-f)", shared_server.port, ["-f"])
|
||||
r = _run_tcp_test("Sendfile (-f)", shared_server.port, ["--sendfile"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
def test_sendfile_multithread(self, shared_server):
|
||||
r = _run_tcp_test("Sendfile + Multithreading (-f -m)", shared_server.port, ["-f", "-m"])
|
||||
r = _run_tcp_test("Sendfile + Multithreading (-f -m)", shared_server.port, ["--sendfile", "--threads"])
|
||||
assert r["status"] == "Success", r["error"]
|
||||
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ class TestTLSBasic:
|
||||
])
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-c", "--tls",
|
||||
flags=["-z", "--tls",
|
||||
"--cert", certs["client_cert"], "--key", certs["client_key"],
|
||||
"--ca", certs["ca"]],
|
||||
port=server.port,
|
||||
@@ -157,7 +157,7 @@ class TestTLSBasic:
|
||||
])
|
||||
result, dur = run_client(
|
||||
SOURCE_DIR, DEST_DIR,
|
||||
flags=["-m", "--tls",
|
||||
flags=["--threads", "--tls",
|
||||
"--cert", certs["client_cert"], "--key", certs["client_key"],
|
||||
"--ca", certs["ca"]],
|
||||
port=server.port,
|
||||
|
||||
+68
-40
@@ -103,19 +103,24 @@ static void test_cli_help() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test that --archive sets compression, multithreading, and metadata */
|
||||
/* Test that --archive's config bundle matches rsync -rlptgoD semantics:
|
||||
* links + metadata + devices + specials, and NOT compression/multithreading. */
|
||||
static void test_cli_archive_flags() {
|
||||
Config* cfg = config_create();
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
|
||||
/* Simulate --archive flag */
|
||||
cfg->use_compression = true;
|
||||
cfg->use_multithreading = true;
|
||||
/* Simulate the --archive flag's implied bundle. */
|
||||
cfg->follow_symlinks = true;
|
||||
cfg->use_metadata = true;
|
||||
cfg->preserve_devices = true;
|
||||
cfg->preserve_specials = true;
|
||||
|
||||
EXPECT_TRUE(cfg->use_compression);
|
||||
EXPECT_TRUE(cfg->use_multithreading);
|
||||
EXPECT_TRUE(cfg->follow_symlinks);
|
||||
EXPECT_TRUE(cfg->use_metadata);
|
||||
EXPECT_TRUE(cfg->preserve_devices);
|
||||
EXPECT_TRUE(cfg->preserve_specials);
|
||||
EXPECT_FALSE(cfg->use_compression);
|
||||
EXPECT_FALSE(cfg->use_multithreading);
|
||||
|
||||
config_delete(cfg);
|
||||
}
|
||||
@@ -325,10 +330,10 @@ static void test_parse_args_fake_super() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test parse_args with valid port */
|
||||
/* Test parse_args with valid SSH port (long form; -p is now rsync --perms) */
|
||||
static void test_parse_args_valid_port() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-p", "2222", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "--ssh-port", "2222", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
@@ -417,7 +422,7 @@ static void test_parse_args_rejects_invalid_chmod() {
|
||||
/* Test parse_args rejects port > 65535 */
|
||||
static void test_parse_args_invalid_port() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-p", "99999", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "--ssh-port", "99999", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
@@ -430,7 +435,7 @@ static void test_parse_args_invalid_port() {
|
||||
/* Test parse_args rejects non-numeric port */
|
||||
static void test_parse_args_non_numeric_port() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-p", "abc", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "--ssh-port", "abc", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
@@ -453,10 +458,10 @@ static void test_parse_args_invalid_server_port() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test parse_args rejects invalid compression level */
|
||||
/* Test parse_args rejects invalid compression level (-z/--compress) */
|
||||
static void test_parse_args_invalid_compression_level() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-c", "25", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "-z", "25", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
@@ -466,10 +471,10 @@ static void test_parse_args_invalid_compression_level() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Test parse_args accepts valid compression level */
|
||||
/* Test parse_args accepts valid compression level (-z/--compress) */
|
||||
static void test_parse_args_valid_compression_level() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-c", "10", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "-z", "10", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
@@ -977,11 +982,12 @@ static void test_parse_args_hard_links() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* -H/--hard-links violates the per-file streaming requirement of -s and the
|
||||
* payload-bearing tail-resume of --append: both combos are rejected up front. */
|
||||
/* -H/--hard-links violates the per-file streaming requirement of
|
||||
* --chunk-serialization and the payload-bearing tail-resume of --append: both
|
||||
* combos are rejected up front. */
|
||||
static void test_validate_config_hard_links_incompatible_modes() {
|
||||
Config* cfg = config_create();
|
||||
char* argv_s[] = {"fastsync", "-H", "-s", "/src", "/dst"};
|
||||
char* argv_s[] = {"fastsync", "-H", "--chunk-serialization", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv_s, positional_args, &positional_count), 0);
|
||||
@@ -1049,33 +1055,52 @@ static void test_parse_args_archive() {
|
||||
|
||||
int ret = parse_args(cfg, 4, argv, positional_args, &positional_count);
|
||||
EXPECT_EQ_INT(ret, 0);
|
||||
EXPECT_TRUE(cfg->use_compression);
|
||||
EXPECT_TRUE(cfg->use_multithreading);
|
||||
EXPECT_TRUE(cfg->follow_symlinks);
|
||||
EXPECT_TRUE(cfg->use_metadata);
|
||||
EXPECT_TRUE(cfg->preserve_devices);
|
||||
EXPECT_TRUE(cfg->preserve_specials);
|
||||
EXPECT_FALSE(cfg->use_compression);
|
||||
EXPECT_FALSE(cfg->use_multithreading);
|
||||
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* Negations must override archive's implied options in argument order. */
|
||||
/* Negations must override archive's implied options in argument order. Note:
|
||||
* archive implies devices+specials, and device/special preservation itself
|
||||
* forces metadata transmission (re-creating a node needs the metadata mode), so
|
||||
* --no-preserve cannot turn metadata back off while archive keeps devices/specials
|
||||
* on -- that is the correct interaction, not a bug. A link negation does work. */
|
||||
static void test_parse_args_negations() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--archive", "--no-compress", "--no-m",
|
||||
"--no-preserve", "--no-dry-run", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "--archive", "--no-links", "--no-preserve",
|
||||
"--no-dry-run", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
EXPECT_EQ_INT(parse_args(cfg, 8, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_FALSE(cfg->use_compression);
|
||||
EXPECT_FALSE(cfg->use_multithreading);
|
||||
EXPECT_FALSE(cfg->use_metadata);
|
||||
EXPECT_EQ_INT(parse_args(cfg, 7, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_FALSE(cfg->follow_symlinks);
|
||||
EXPECT_TRUE(cfg->use_metadata);
|
||||
EXPECT_FALSE(cfg->dry_run);
|
||||
EXPECT_EQ_INT(positional_count, 2);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* --no-preserve negates an explicit --preserve when nothing forces metadata back
|
||||
* on (no devices/specials). */
|
||||
static void test_parse_args_negate_preserve_without_devices() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--preserve", "--no-preserve", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_FALSE(cfg->use_metadata);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_parse_args_negation_order() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--no-z", "-c", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "--no-z", "-z", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
@@ -1428,9 +1453,11 @@ static void test_parse_args_secluded_args() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
static void test_parse_args_short_s_remains_chunk_serialization() {
|
||||
static void test_parse_args_chunk_serialization_long_form() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "-s", "/src", "/dst"};
|
||||
/* Chunk serialization is now long-form-only (the short -s is rsync's
|
||||
* --secluded-args no-op). */
|
||||
char* argv[] = {"fastsync", "--chunk-serialization", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
|
||||
@@ -1651,7 +1678,7 @@ static void test_parse_args_fuzzy_respects_no_incremental() {
|
||||
* sendfile (-f) modes reject -- mirroring the --delta constraint checks. */
|
||||
static void test_validate_config_fuzzy_incompatible_modes() {
|
||||
Config* cfg = config_create();
|
||||
char* argv[] = {"fastsync", "--fuzzy", "-s", "/src", "/dst"};
|
||||
char* argv[] = {"fastsync", "--fuzzy", "--chunk-serialization", "/src", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||
@@ -1662,7 +1689,7 @@ static void test_validate_config_fuzzy_incompatible_modes() {
|
||||
config_delete(cfg);
|
||||
|
||||
cfg = config_create();
|
||||
char* sendfile_argv[] = {"fastsync", "--fuzzy", "-f", "/src", "/dst"};
|
||||
char* sendfile_argv[] = {"fastsync", "--fuzzy", "--sendfile", "/src", "/dst"};
|
||||
positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 5, sendfile_argv, positional_args, &positional_count), 0);
|
||||
cfg->send_directory = str_dup("/src");
|
||||
@@ -2819,18 +2846,18 @@ static void test_parse_args_remote_option_rejects_bad_values() {
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
/* A short -M form must NOT be accepted as --remote-option: -M stays FastSync
|
||||
* metadata mode (documented divergence). */
|
||||
static void test_parse_args_remote_option_no_short_M() {
|
||||
/* Since the Phase-7 CLI-namespace pass, -M is rsync's --remote-option short
|
||||
* form (FastSync metadata mode is long-only --preserve): it consumes the next
|
||||
* argv as a remote-option value and must NOT set FastSync metadata mode. */
|
||||
static void test_parse_args_remote_option_short_M() {
|
||||
Config* cfg = valid_client_config();
|
||||
EXPECT_NOT_NULL(cfg);
|
||||
/* -M followed by a remote-option-looking word still means metadata mode. */
|
||||
char* argv[] = {"fastsync", "-M", "-v", "--source-dir", "/src", "--dest-dir", "/dst"};
|
||||
char* argv[] = {"fastsync", "-M", "--trust-sender", "--source-dir", "/src", "--dest-dir", "/dst"};
|
||||
int positional_args[2];
|
||||
int positional_count = 0;
|
||||
EXPECT_EQ_INT(parse_args(cfg, 7, argv, positional_args, &positional_count), 0);
|
||||
EXPECT_TRUE(cfg->use_metadata);
|
||||
EXPECT_EQ_INT(cfg->remote_option_count, 0);
|
||||
EXPECT_FALSE(cfg->use_metadata);
|
||||
EXPECT_EQ_INT(cfg->remote_option_count, 1);
|
||||
config_delete(cfg);
|
||||
}
|
||||
|
||||
@@ -2955,6 +2982,7 @@ void test_client_cli() {
|
||||
test_parse_args_rejects_invalid_info_flag();
|
||||
test_parse_args_archive();
|
||||
test_parse_args_negations();
|
||||
test_parse_args_negate_preserve_without_devices();
|
||||
test_parse_args_negation_order();
|
||||
test_parse_args_no_preserve_blocks_implicit_metadata();
|
||||
test_parse_args_rejects_unsafe_negation();
|
||||
@@ -2970,7 +2998,7 @@ void test_client_cli() {
|
||||
test_parse_args_stderr_modes();
|
||||
test_parse_args_rejects_unsupported_stderr_modes();
|
||||
test_parse_args_secluded_args();
|
||||
test_parse_args_short_s_remains_chunk_serialization();
|
||||
test_parse_args_chunk_serialization_long_form();
|
||||
test_parse_args_symlink_trust();
|
||||
test_parse_args_whole_file();
|
||||
test_parse_args_fuzzy_implies_delta();
|
||||
@@ -3021,7 +3049,7 @@ void test_client_cli() {
|
||||
test_parse_args_remote_option_space_form();
|
||||
test_parse_args_remote_option_missing_value();
|
||||
test_parse_args_remote_option_rejects_bad_values();
|
||||
test_parse_args_remote_option_no_short_M();
|
||||
test_parse_args_remote_option_short_M();
|
||||
test_parse_args_no_motd();
|
||||
test_parse_args_password_file();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user