feat: split delete-manifest frame into keep-set + protected prefixes; enforce --max-delete and --force on the receiver

The STATUS_MANIFEST frame now carries two count-delimited sections: the kept
paths and a protected-prefix list (excluded-on-source paths the walker must not
delete unless --delete-excluded opted out).  The receiver's DeleteManifest is
passed through the commit/early paths unchanged.  manifest_delete_extras
honors a client --max-delete (all-or-nothing) and produces a distinct error for
it versus the 100000-entry server bound.  --force clears a non-empty directory
that blocks an incoming regular file (confined, symlink-safe) via a new
file_remove_tree_secure helper.
This commit is contained in:
2026-09-06 21:50:02 +02:00
parent 1c9b660ac0
commit c4e0de8f08
7 changed files with 170 additions and 63 deletions
+15 -3
View File
@@ -25,6 +25,18 @@ typedef struct {
cnd_t condition_not_empty_loader;
bool loader_done;
ArrayList* manifest;
/* Protected prefixes (paths the source scan excluded by user rules) sent
with the keep-set manifest so --delete leaves them alone unless
--delete-excluded is set. NULL when not collecting. Populated by the
scanner thread (parallel workers append under mutex_scanner via the
scanner's exclusion sink) or, in the early modes, by the path-only pre-scan
on the calling thread before the pipeline starts. */
ArrayList* excluded_paths;
/* A source I/O error (unreadable directory) was recorded during the scan.
Set by the pre-scan (before the threads start) or by the scanner thread
under mutex_scanner; the caller turns it into a non-zero exit when
--ignore-errors kept the run going. */
bool scan_had_io_error;
ArrayList* remove_source_files;
/* True when --delete-before/--delete-during require the keep-set manifest to
be transmitted before any file data: context->manifest is then prebuilt by
@@ -65,9 +77,9 @@ typedef struct PipelineContextReceiver {
protocol stream but hands the manifest here instead of deleting while the
disk writer may still be draining; the caller (server.c) commits the
deletion after both threads have joined, so no extra is removed unless the
transfer truly succeeded. NULL in the early delete modes (which delete at
the manifest). */
ArrayList* deferred_manifest;
transfer truly succeeded. NULL in the early delete modes (which delete at
the manifest). */
DeleteManifest* deferred_manifest;
} PipelineContextReceiver;
PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* queue_scanner,