Merge branch 'feat/parity-b3' into feat/parity-next

# Conflicts:
#	RSYNC_COMPAT.md
This commit is contained in:
2026-09-22 22:07:45 +02:00
5 changed files with 173 additions and 36 deletions
+25 -9
View File
@@ -381,21 +381,28 @@ bool files_from_list_check(const Config* config, ArrayList* missing_dest, int* s
paths, loading and sending nothing. --delete-before/--delete-during need the
complete keep-set manifest before the first data byte, so it is built by a
dedicated pre-scan pass and transmitted early; the data pass then re-scans
with a fresh scanner. A source I/O error is fatal unless the options carry
--ignore-errors, in which case the scan continues past the unreadable
directory and *io_error_out reports it (the caller still performs the
deletion but reports the run as errored). */
with a fresh scanner. --delete-before additionally replays this very scan as
its data pass (rsync's single file list), so `chunks_out` (optional) retains
the scanned Chunk objects for the caller to send instead of destroying them;
the caller owns the list and must give it a chunk_destroy destructor. A
source I/O error is fatal unless the options carry --ignore-errors, in which
case the scan continues past the unreadable directory and *io_error_out
reports it (the caller still performs the deletion but reports the run as
errored). */
bool scan_paths_only(const Config* config, const ScannerOptions* options, ArrayList* manifest,
DeletePlanSender* plans, bool* io_error_out,
unsigned long long* non_dir_count_out) {
unsigned long long* non_dir_count_out, ArrayList* chunks_out,
bool emit_nonreg) {
if (io_error_out)
*io_error_out = false;
if (non_dir_count_out)
*non_dir_count_out = 0;
ScannerOptions local = *options;
/* The pre-scan is a paths-only pass with no client output; it must not emit
--info=nonreg lines (the data pass does that once). */
local.note_nonreg = false;
/* The pre-scan is normally a paths-only pass with no client output: it must
not emit --info=nonreg lines because the data pass re-scans and emits them
once. When the caller replays this scan as the data pass (--delete-before)
there is no later scan, so it opts in and the lines are emitted here. */
local.note_nonreg = emit_nonreg && options->note_nonreg;
DirectoryScanner* scanner = directory_scanner_create_with_options(config->send_directory, &local);
if (!scanner)
return false;
@@ -430,7 +437,16 @@ bool scan_paths_only(const Config* config, const ScannerOptions* options, ArrayL
break;
}
}
chunk_destroy(chunk);
if (chunks_out) {
/* Retain the chunk for the caller's data pass; ownership moves with it. */
if (!array_list_add(chunks_out, chunk)) {
ok = false;
chunk_destroy(chunk);
break;
}
} else {
chunk_destroy(chunk);
}
}
if (ok) {
/* Keep every traversed source directory, including empty ones, so a plan
+66 -21
View File
@@ -1394,6 +1394,10 @@ typedef struct {
Client* client;
DirectoryScanner* scanner;
ArrayList* manifest;
/* --delete-before: the pre-scan that built the keep-set, retained as the data
pass's file list (owning Chunk*; consumed chunks are NULLed as they are
sent). NULL in every other mode, where the data pass scans normally. */
ArrayList* prescan_chunks;
DeletePlanSender* plan_sender;
ArrayList* remove_sources;
ArrayList* dir_entries;
@@ -1478,12 +1482,23 @@ static bool send_files_prepare_delete(Config* config, SendFilesState* state) {
if (state->delete_early) {
/* Pass 1: collect the complete keep-set (paths only, no data loaded) and
transmit it now, before any file data. The receiver removes extras and
acks; the transfer aborts here if the deletion could not commit. */
acks; the transfer aborts here if the deletion could not commit. The
scanned chunks are retained so the data pass can replay this exact list
instead of re-reading the source (rsync builds one file list and never
transfers a file created after it). */
ArrayList* early_manifest = array_list_create(free);
if (!early_manifest)
ArrayList* prescan_chunks = array_list_create(chunk_destroy);
if (!early_manifest || !prescan_chunks) {
array_list_delete(early_manifest);
array_list_delete(prescan_chunks);
return false;
}
/* No later scan runs for --delete-before, so this pass must also capture the
deferred directory times and the --stats directory count. */
state->prepared.options.dir_entries = state->dir_entries;
state->prepared.options.dir_count = config->stats ? &state->dir_count : NULL;
bool prescan_ok = scan_paths_only(config, &state->prepared.options, early_manifest, NULL,
&state->had_scan_io, NULL);
&state->had_scan_io, NULL, prescan_chunks, true);
bool early_ok = false;
bool skip_delete = false;
if (prescan_ok) {
@@ -1514,8 +1529,13 @@ static bool send_files_prepare_delete(Config* config, SendFilesState* state) {
state->prepared.options.excluded_paths = NULL;
state->prepared.options.size_skipped_paths = NULL;
state->prepared.options.synced_dirs = NULL;
if (!prescan_ok || (!early_ok && !skip_delete))
if (!prescan_ok || (!early_ok && !skip_delete)) {
array_list_delete(prescan_chunks);
return false;
}
/* Adopt the captured scan as the data pass's file list (including when an
I/O error suppressed only the deletion: the list is still complete). */
state->prescan_chunks = prescan_chunks;
} else if (state->delete_per_dir) {
/* --delete-during/--delete-delay: build one plan per source directory from a
path-only pre-scan and transmit the COMPLETE plan set now, before any data,
@@ -1527,8 +1547,9 @@ static bool send_files_prepare_delete(Config* config, SendFilesState* state) {
if (!state->plan_sender || !state->plan_dirs)
return false;
state->prepared.options.plan_dirs = state->plan_dirs;
bool prescan_ok = scan_paths_only(config, &state->prepared.options, NULL, state->plan_sender,
&state->had_scan_io, &state->per_dir_non_dir_count);
bool prescan_ok =
scan_paths_only(config, &state->prepared.options, NULL, state->plan_sender,
&state->had_scan_io, &state->per_dir_non_dir_count, NULL, false);
bool plans_ok = false;
bool skip_delete = false;
if (prescan_ok) {
@@ -1592,24 +1613,42 @@ static bool send_files_run(Config* config, SendFilesState* state) {
state->stop = stop_condition_make(config->stop_after_mins > 0, config->stop_after_mins,
config->cli.stop_at_set, config->stop_at, now_mono);
state->prepared.options.stop_condition = &state->stop;
/* The early-delete pre-scan above already ran; only the data pass should feed
the directory-time list (otherwise every directory would be captured
twice). */
state->prepared.options.dir_entries = state->dir_entries;
state->prepared.options.dir_count = config->stats ? &state->dir_count : NULL;
state->scanner =
directory_scanner_create_with_options(config->send_directory, &state->prepared.options);
if (!state->scanner)
return false;
/* --delete-before reuses the pre-scan that built the keep-set as the data
pass's file list, so a source file created after that scan is neither
transferred nor kept (rsync builds one file list). That pre-scan captured
the deferred directory times and the --stats directory count because no
later scan runs; every other mode opens a fresh data scanner here. */
if (state->prescan_chunks == NULL) {
state->prepared.options.dir_entries = state->dir_entries;
state->prepared.options.dir_count = config->stats ? &state->dir_count : NULL;
state->scanner =
directory_scanner_create_with_options(config->send_directory, &state->prepared.options);
if (!state->scanner)
return false;
}
Chunk* current_chunk;
int prescan_index = 0;
memset(&state->transfer_stats, 0, sizeof(state->transfer_stats));
state->start = time(NULL);
client_progress_begin(config);
/* True when the stop deadline cut the scan short so the keep-set manifest is
only a prefix of the source. */
bool send_failed = false;
while ((current_chunk = directory_scanner_next(state->scanner)) != NULL) {
while (true) {
if (state->prescan_chunks != NULL) {
if (prescan_index >= state->prescan_chunks->size)
break;
/* Move ownership out of the retained list so chunk_destroy below (and the
cleanup tail for an early exit) never double-frees it. */
current_chunk = (Chunk*)state->prescan_chunks->items[prescan_index];
state->prescan_chunks->items[prescan_index] = NULL;
prescan_index++;
} else {
current_chunk = directory_scanner_next(state->scanner);
if (current_chunk == NULL)
break;
}
/* Graceful abort (Ctrl-C/SIGTERM): notify the receiver and clean up. The
session is active (config_send already succeeded); a send failure here is
fine because the client is exiting anyway. */
@@ -1667,10 +1706,14 @@ static bool send_files_run(Config* config, SendFilesState* state) {
* Returns the rsync-compatible exit code. */
static int send_files_finalize(const Config* config, SendFilesState* state) {
Client* client = state->client;
if (directory_scanner_failed(state->scanner))
return 1;
if (directory_scanner_had_io_error(state->scanner))
state->had_scan_io = true;
/* A --delete-before run replays the pre-scan and owns no data scanner; its
I/O-error verdict was already recorded by that pre-scan. */
if (state->scanner != NULL) {
if (directory_scanner_failed(state->scanner))
return 1;
if (directory_scanner_had_io_error(state->scanner))
state->had_scan_io = true;
}
/* An abort that arrived after the last chunk must still stop the completion
tail (manifest/finalize) rather than let it run to success. */
if (client_abort_pending()) {
@@ -1774,6 +1817,8 @@ static int send_files_finalize(const Config* config, SendFilesState* state) {
static void send_files_cleanup(SendFilesState* state) {
if (state->manifest)
array_list_delete(state->manifest);
if (state->prescan_chunks)
array_list_delete(state->prescan_chunks);
if (state->plan_sender)
delete_plan_sender_destroy(state->plan_sender);
if (state->excluded)
@@ -1993,7 +2038,7 @@ int send_files_multithreaded(Config* config) {
bool prebuilt =
prepared_ok &&
scan_paths_only(config, &prepared.options, context->manifest, context->delete_plans,
&context->scan_had_io_error, &pre_scan_non_dir);
&context->scan_had_io_error, &pre_scan_non_dir, NULL, false);
prepared_scanner_destroy(&prepared);
if (per_dir && prebuilt) {
const char* walk_root = delete_plan_walk_root(config, context->synced_dirs);
+2 -1
View File
@@ -44,7 +44,8 @@ const char* delete_plan_walk_root(const Config* config, const ArrayList* synced_
bool files_from_list_check(const Config* config, ArrayList* missing_dest, int* skipped_out);
bool scan_paths_only(const Config* config, const ScannerOptions* options, ArrayList* manifest,
DeletePlanSender* plans, bool* io_error_out,
unsigned long long* non_dir_count_out);
unsigned long long* non_dir_count_out, ArrayList* chunks_out,
bool emit_nonreg);
/* client_report.c */
void log_server_rejection(const char* context);