Implements real rsync deletion-timing so the flags actually select when extra-file deletion happens.
Design
Deletion stays derived only from the transmitted keep-set manifest, uses the symlink-safe bounded walker, and skips the .fastsync-stage dir. Timing is picked purely from the config on both ends; the STATUS_MANIFEST frame is now self-delimiting/position-independent.
Early modes (--delete-before, --delete-during/--del): sender does a full path-only pre-scan and transmits the keep-set BEFORE any file data; receiver deletes extras and acks STATUS_OK; the sender only streams once deletion committed (or aborts). Delete-before/during therefore delete even if a later transfer phase fails (destructive by definition, like rsync).
Commit modes (plain --delete, --delete-after, --delete-delay): manifest closes the data stream; deletion commits only after STATUS_FINISHED proves the whole transfer succeeded. This is also the default under plain --delete (not rsync's delete-during default) to preserve FastSync's commit-style safety.
-m: late deletion is committed by server.c only after the disk-writer thread has drained (fixes a delete-vs-in-flight-temp-file race); early deletion runs in the receive thread before any data is enqueued.
Every timing flag implies --delete; at most one timing flag allowed; conflicts rejected on both ends.
Wire / protocol
Two new config booleans (delete_during, delete_delay) are serialized alongside the existing delete_before/delete_after; PROTOCOL_VERSION bumped 2.7.0 -> 2.8.0 (peers must match exactly, enforced). No new STATUS codes.
FastSync streams in one directory scan with no per-directory generator pass, so --delete-during cannot interleave deletions per-directory; it selects the same early engine mode as --delete-before (identical observable success/failure behaviour).
--delete-delay converges with --delete-after (FastSync never snapshots the destination during data flow); same end-of-transfer commit & safety.
rsync's --delete default is delete-during; FastSync's default stays delete-after.
Early keep-set is a pre-scan snapshot; a source file that appears between pre-scan and the data pass is still sent but was not protected from deletion.
Tests
Unit: CLI acceptance of each flag (+ --del alias) with use_delete implied; rejection of conflicting timings / timing with --no-delete; config wire round-trips for the new fields; receiver validation rejects two timings; config_delete_timing_early() mapping.
Integration (TCP, single + -m): every flag removes extras on success; early modes delete a destination file blocking a nested write so the transfer then succeeds, whereas plain --delete/--delete-after/--delete-delay keep everything intact and fail (commit-style); early timing completes (no delete) when the server refuses deletion.
27/27 unit groups pass; 151 feature + 26 other integration tests pass; strict-warnings build clean; clang-format 18 + cppcheck clean (CI image).
Implements real rsync deletion-timing so the flags actually select when extra-file deletion happens.
## Design
Deletion stays derived only from the transmitted keep-set manifest, uses the symlink-safe bounded walker, and skips the .fastsync-stage dir. Timing is picked purely from the config on both ends; the STATUS_MANIFEST frame is now self-delimiting/position-independent.
- Early modes (--delete-before, --delete-during/--del): sender does a full path-only pre-scan and transmits the keep-set BEFORE any file data; receiver deletes extras and acks STATUS_OK; the sender only streams once deletion committed (or aborts). Delete-before/during therefore delete even if a later transfer phase fails (destructive by definition, like rsync).
- Commit modes (plain --delete, --delete-after, --delete-delay): manifest closes the data stream; deletion commits only after STATUS_FINISHED proves the whole transfer succeeded. This is also the default under plain --delete (not rsync's delete-during default) to preserve FastSync's commit-style safety.
- -m: late deletion is committed by server.c only after the disk-writer thread has drained (fixes a delete-vs-in-flight-temp-file race); early deletion runs in the receive thread before any data is enqueued.
- Every timing flag implies --delete; at most one timing flag allowed; conflicts rejected on both ends.
## Wire / protocol
Two new config booleans (delete_during, delete_delay) are serialized alongside the existing delete_before/delete_after; PROTOCOL_VERSION bumped 2.7.0 -> 2.8.0 (peers must match exactly, enforced). No new STATUS codes.
## Defaults
Plain --delete keeps today's behavior = delete-after (commit) semantics.
## Documented divergences from rsync
- FastSync streams in one directory scan with no per-directory generator pass, so --delete-during cannot interleave deletions per-directory; it selects the same early engine mode as --delete-before (identical observable success/failure behaviour).
- --delete-delay converges with --delete-after (FastSync never snapshots the destination during data flow); same end-of-transfer commit & safety.
- rsync's --delete default is delete-during; FastSync's default stays delete-after.
- Early keep-set is a pre-scan snapshot; a source file that appears between pre-scan and the data pass is still sent but was not protected from deletion.
## Tests
- Unit: CLI acceptance of each flag (+ --del alias) with use_delete implied; rejection of conflicting timings / timing with --no-delete; config wire round-trips for the new fields; receiver validation rejects two timings; config_delete_timing_early() mapping.
- Integration (TCP, single + -m): every flag removes extras on success; early modes delete a destination file blocking a nested write so the transfer then succeeds, whereas plain --delete/--delete-after/--delete-delay keep everything intact and fail (commit-style); early timing completes (no delete) when the server refuses deletion.
- 27/27 unit groups pass; 151 feature + 26 other integration tests pass; strict-warnings build clean; clang-format 18 + cppcheck clean (CI image).
Deletion timing is now real and selected by the four rsync flags plus the
plain --delete default. Wire protocol bumps to 2.8.0: two new config
booleans (delete_during, delete_delay) are serialized and validated, joining
the existing delete_before/delete_after.
- Early modes (--delete-before, --delete-during/--del): the sender pre-scans
the whole tree (paths only), transmits the keep-set manifest BEFORE any
file data, and the receiver removes extras and acks STATUS_OK; the sender
only streams data after the deletion committed. Deletion is thus performed
even if a later transfer phase fails (rsync delete-before/during are
destructive by definition). FastSync streams in a single scan so it cannot
interleave per-directory like rsync delete-during; --delete-during selects
the same engine mode as --delete-before (documented divergence).
- Late/commit modes (plain --delete, --delete-after, --delete-delay): the
manifest closes the data stream and deletion is committed only after
STATUS_FINISHED proves the whole transfer succeeded, preserving FastSync's
commit-style safety. --delete-delay converges with --delete-after because
FastSync never snapshots the destination during data flow (documented).
- The STATUS_MANIFEST frame is now self-delimiting and position-independent.
Single-threaded receivers delete before the success frame; the -m receiver
hands the keep-set to server.c, which commits the deletion only after the
disk writer thread has drained (fixes a delete-vs-in-flight-temp race).
- Every timing flag implies --delete; at most one timing flag is allowed.
- Each timing flag implies --delete, matching rsync; conflicts are rejected.
- CLI: each timing flag (+ --del alias) accepted and implies --delete;
conflicting timings and a timing with --no-delete are rejected.
- Config: delete_during/delete_delay survive config_send/config_receive;
two simultaneous timings are rejected by the receiver-side validation;
config_delete_timing_early() mapping is unit-tested.
- Integration (TCP, single- and multithreaded): every flag removes extras on
a successful transfer; early modes (--delete-before/--delete-during/--del)
delete before data is applied so a destination file blocking a nested
write is removed and the transfer succeeds, while plain --delete /
--delete-after / --delete-delay keep it and fail with every extra intact
(commit-style). Early timing also completes (without deleting) when the
server refuses deletion.
RSYNC_COMPAT.md: flip --delete-before, --del/--delete-during, --delete-delay
and --delete-after to Implemented with precise notes (default-under--delete,
safety model, 2.7.0 -> 2.8.0 protocol bump, exact divergences from rsync).
README option tables list the new flags and the delete-after default.
The late/commit path keeps the received keep-set in a local list until
STATUS_FINISHED. Error exits after it was parked (STATUS_ABORT, a failing
receive_status / non-FINISHED status, a second manifest frame, or a later
file/chunk/store failure) previously dropped the only reference and leaked up
to ~16 MB of path strings + pointer array per connection. Both failure labels
now discard the parked list exactly once; the successful FINISHED path still
hands ownership to *pending_manifest (the -m caller) without freeing it.
The receiver performs the whole bounded deletion walk (up to
MAX_SERVER_DELETE_COUNT unlinks) before answering the delete-before/during
manifest, so its STATUS_OK reply can take far longer than the default 60 s
per-message receive window. Waiting with the default would make the sender
abort AFTER the deletion had already committed on the receiver. Add a timed
receive variant (receive_status_timed / protocol_receive_n_data_timed) and use
it for the early-manifest ACK with a 1 h explicit deadline; connection errors
and EOF still abort immediately.
- Unit (leak guards): drive receiver_process_pending() past a parked keep-set
into STATUS_ABORT, EOF, and a second manifest frame; each must return -1 with
no manifest handed out. Verified leak-free under ASan.
- Unit: receive_status_timed reads a status and fails cleanly on EOF.
- Integration: test_late_flags_commit_only_after_success now parametrizes the
-m path, proving a failed -m late-timing run preserves every extra and that
the deferred manifest is dropped (never applied) when the writer fails.
Usage help now gives --delete-during a complete description with --del on its
own line, and notes that timing flags imply --delete while timing+--no-delete
is rejected regardless of argument order. RSYNC_COMPAT.md documents: the
receiver's MAX_MANIFEST_ENTRIES/MAX_MANIFEST_BYTES caps now abort an early-mode
run before any data (previously only the deletion step failed), the extended
early-delete ACK deadline, and the order-independent flag-conflict policy.
Merged into dev via local merge (2FA blocks server-side merge). Commits: delete-timing 265b1e7 (rsync delete timing, protocol 2.8.0) + basis-dest 01a5a93. Independent c-review APPROVE WITH NITS / REQUEST CHANGES; all findings fixed and re-verified. dev CI run #479: all jobs success (lint, build-and-test, sanitizers address+undefined, fuzz-build, coverage, valgrind). Closing without server merge.
Merged into dev via local merge (2FA blocks server-side merge). Commits: delete-timing 265b1e7 (rsync delete timing, protocol 2.8.0) + basis-dest 01a5a93. Independent c-review APPROVE WITH NITS / REQUEST CHANGES; all findings fixed and re-verified. dev CI run #479: all jobs success (lint, build-and-test, sanitizers address+undefined, fuzz-build, coverage, valgrind). Closing without server merge.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements real rsync deletion-timing so the flags actually select when extra-file deletion happens.
Design
Deletion stays derived only from the transmitted keep-set manifest, uses the symlink-safe bounded walker, and skips the .fastsync-stage dir. Timing is picked purely from the config on both ends; the STATUS_MANIFEST frame is now self-delimiting/position-independent.
Wire / protocol
Two new config booleans (delete_during, delete_delay) are serialized alongside the existing delete_before/delete_after; PROTOCOL_VERSION bumped 2.7.0 -> 2.8.0 (peers must match exactly, enforced). No new STATUS codes.
Defaults
Plain --delete keeps today's behavior = delete-after (commit) semantics.
Documented divergences from rsync
Tests
Merged into dev via local merge (2FA blocks server-side merge). Commits: delete-timing
265b1e7(rsync delete timing, protocol 2.8.0) + basis-dest01a5a93. Independent c-review APPROVE WITH NITS / REQUEST CHANGES; all findings fixed and re-verified. dev CI run #479: all jobs success (lint, build-and-test, sanitizers address+undefined, fuzz-build, coverage, valgrind). Closing without server merge.Pull request closed