Merge branch 'feat/parity-delete' into feat/rsync-parity

This commit is contained in:
2026-09-15 23:24:24 +02:00
28 changed files with 1216 additions and 440 deletions
+28 -3
View File
@@ -248,6 +248,25 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio
return 0;
}
/* Parse a signed integer, clamping every negative value to -1. rsync's
--max-delete treats a negative argument (the deprecated -1 spelling) as "no
client limit", so -2/-5 must behave identically rather than being rejected. */
static int set_signed_clamped_int_option(int* dest, const char* value, const char* option_name) {
if (!value || *value == '\0') {
log_message(LOG_LEVEL_ERROR, "%s must be an integer", option_name);
return -1;
}
char* endptr;
errno = 0;
long parsed = strtol(value, &endptr, 10);
if (errno != 0 || *endptr != '\0' || parsed < INT_MIN || parsed > INT_MAX) {
log_message(LOG_LEVEL_ERROR, "%s must be an integer", option_name);
return -1;
}
*dest = parsed < 0 ? -1 : (int)parsed;
return 0;
}
/* Forward decl: config_add_pattern is defined below, but the --remote-option
* helper above needs it. */
static int config_add_pattern(char*** patterns, int* count, const char* value, const char* optname);
@@ -616,6 +635,9 @@ typedef enum {
OPT_POS_INT,
OPT_NONNEG_INT,
OPT_ULL,
/* A signed integer whose negative values are clamped to -1 (rsync's
"no limit" spelling for --max-delete). */
OPT_SIGNED_INT,
} OptKind;
typedef struct {
@@ -730,7 +752,7 @@ static const OptionEntry OPTION_TABLE[] = {
{"--delete-delay", NULL, OPT_FLAG, offsetof(Config, delete_delay)},
{"--delete-after", NULL, OPT_FLAG, offsetof(Config, delete_after)},
{"--delete-excluded", NULL, OPT_FLAG, offsetof(Config, delete_excluded)},
{"--max-delete", NULL, OPT_NONNEG_INT, offsetof(Config, max_delete)},
{"--max-delete", NULL, OPT_SIGNED_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", "-m", OPT_FLAG, offsetof(Config, prune_empty_dirs)},
@@ -854,7 +876,8 @@ static const OptionEntry* find_table_option_with_equals(const char* arg, const c
(entry->alias && strlen(entry->alias) == name_len &&
strncmp(arg, entry->alias, name_len) == 0)) {
if (entry->kind == OPT_STRING || entry->kind == OPT_POS_INT ||
entry->kind == OPT_NONNEG_INT || entry->kind == OPT_ULL) {
entry->kind == OPT_NONNEG_INT || entry->kind == OPT_ULL ||
entry->kind == OPT_SIGNED_INT) {
*value = equals + 1;
return entry;
}
@@ -922,6 +945,8 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch
return set_positive_int_option((int*)field, value, entry->name);
case OPT_NONNEG_INT:
return set_nonneg_int_option((int*)field, value, entry->name);
case OPT_SIGNED_INT:
return set_signed_clamped_int_option((int*)field, value, entry->name);
case OPT_ULL: {
unsigned long long v;
/* Size-limit options accept rsync-style suffixes (e.g. --max-size=2G); a
@@ -2158,7 +2183,7 @@ static bool cli_long_takes_separate_value(const char* arg) {
const OptionEntry* entry = find_table_option(arg);
if (entry)
return entry->kind == OPT_STRING || entry->kind == OPT_POS_INT ||
entry->kind == OPT_NONNEG_INT || entry->kind == OPT_ULL;
entry->kind == OPT_NONNEG_INT || entry->kind == OPT_ULL || entry->kind == OPT_SIGNED_INT;
static const char* const extra[] = {
"--ssh-port", "--exclude", "--include", "--exclude-from",
"--include-from", "--files-from", "--filter", "--delta-block",
+148 -37
View File
@@ -175,6 +175,8 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
options->ignore_missing_args = config->ignore_missing_args || config->delete_missing_args;
options->excluded_paths = NULL;
options->excluded_mutex = NULL;
options->size_skipped_paths = NULL;
options->synced_dirs = NULL;
options->hardlinks = NULL;
/* P7 Wave D: capture source directory metadata when a directory attribute is
requested (-p for modes, -t for times unless -O omits them). Whether they
@@ -654,7 +656,10 @@ static void mark_sender_done(PipelineContextSender* context) {
file it processed, in send order: STATUS_NEXT means the file was written,
STATUS_OK means the file was skipped/unchanged. Skipped sources are marked
so the later removal pass keeps them. */
static bool finalize_transfer(Client* client, const Config* config, ArrayList* remove_sources) {
static bool finalize_transfer(Client* client, const Config* config, ArrayList* remove_sources,
bool* delete_limit_out) {
if (delete_limit_out)
*delete_limit_out = false;
if (!send_status(client->file_descriptor, STATUS_FINISHED))
return false;
if (config->remove_source_files && remove_sources) {
@@ -677,6 +682,15 @@ static bool finalize_transfer(Client* client, const Config* config, ArrayList* r
Status status;
if (!receive_status(client->file_descriptor, &status))
return false;
/* A capped --max-delete commit is a successful transfer that the client must
report with rsync's exit code 25 (not an error). */
if (status == STATUS_DELETE_LIMIT) {
log_message(LOG_LEVEL_ERROR,
"Deletions stopped due to --max-delete limit; some deletions were skipped");
if (delete_limit_out)
*delete_limit_out = true;
return true;
}
if (status != STATUS_OK) {
log_server_rejection("Receiver reported transfer failure");
return false;
@@ -910,7 +924,8 @@ static int send_list_only(const Config* config) {
frame. A heavily filtered source whose exclusion list is large therefore
fails the run cleanly on the receiver rather than being truncated. */
static int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protected_prefixes,
ArrayList* missing_args) {
ArrayList* size_skipped, ArrayList* missing_args,
ArrayList* synced_dirs) {
if (!send_status(fd, STATUS_MANIFEST))
return -1;
int keep_count = manifest ? manifest->size : 0;
@@ -920,12 +935,24 @@ static int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protecte
if (!send_wire_str(fd, (char*)manifest->items[i]))
return -1;
}
int protected_count = protected_prefixes ? protected_prefixes->size : 0;
/* The receiver has ONE protected-prefix section; filter-excluded prefixes
(dropped under --delete-excluded) and size-pruned prefixes (always
protected) are concatenated into it. */
int protected_count =
(protected_prefixes ? protected_prefixes->size : 0) + (size_skipped ? size_skipped->size : 0);
if (!send_int(fd, protected_count))
return -1;
for (int i = 0; i < protected_count; i++) {
if (!send_wire_str(fd, (char*)protected_prefixes->items[i]))
return -1;
if (protected_prefixes) {
for (int i = 0; i < protected_prefixes->size; i++) {
if (!send_wire_str(fd, (char*)protected_prefixes->items[i]))
return -1;
}
}
if (size_skipped) {
for (int i = 0; i < size_skipped->size; i++) {
if (!send_wire_str(fd, (char*)size_skipped->items[i]))
return -1;
}
}
int missing_count = missing_args ? missing_args->size : 0;
if (!send_int(fd, missing_count))
@@ -934,6 +961,13 @@ static int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protecte
if (!send_wire_str(fd, (char*)missing_args->items[i]))
return -1;
}
int dirs_count = synced_dirs ? synced_dirs->size : 0;
if (!send_int(fd, dirs_count))
return -1;
for (int i = 0; i < dirs_count; i++) {
if (!send_wire_str(fd, (char*)synced_dirs->items[i]))
return -1;
}
return 0;
}
@@ -953,11 +987,12 @@ static int send_delete_manifest(int fd, ArrayList* manifest, ArrayList* protecte
#define DELETE_ACK_KEEPALIVE_SEC 10
static bool send_delete_manifest_early(Client* client, ArrayList* manifest,
ArrayList* protected_prefixes, ArrayList* missing_args) {
ArrayList* protected_prefixes, ArrayList* size_skipped,
ArrayList* missing_args, ArrayList* synced_dirs) {
if (!client || !manifest)
return false;
if (send_delete_manifest(client->file_descriptor, manifest, protected_prefixes, missing_args) !=
0)
if (send_delete_manifest(client->file_descriptor, manifest, protected_prefixes, size_skipped,
missing_args, synced_dirs) != 0)
return false;
Status ack;
/* The wait is long (up to an hour) and runs inline on this thread: a helper
@@ -1761,7 +1796,8 @@ static int send_chunks_multithreaded(void* pipeline_context) {
/* The keep-set manifest was prebuilt by a path-only pre-scan. Transmit it
and wait for the receiver to delete extras before streaming any data. */
if (!send_delete_manifest_early(client, context->manifest, context->excluded_paths,
context->missing_args)) {
context->size_skipped_paths, context->missing_args,
context->synced_dirs)) {
pipeline_cancel(context);
disconnect_transfer_client(client);
mark_sender_done(context);
@@ -1868,13 +1904,15 @@ static int send_chunks_multithreaded(void* pipeline_context) {
goto send_fail;
}
if (send_delete_manifest(client->file_descriptor, context->manifest, context->excluded_paths,
context->missing_args) != 0)
context->size_skipped_paths, context->missing_args,
context->synced_dirs) != 0)
goto send_fail;
} else if (context->config->delete_missing_args && !context->early_delete) {
/* --delete-missing-args without --delete: no keep-set is built, but the
exact-delete paths still ride the same manifest frame (commit once the
transfer succeeded). */
if (send_delete_manifest(client->file_descriptor, NULL, NULL, context->missing_args) != 0)
if (send_delete_manifest(client->file_descriptor, NULL, NULL, NULL, context->missing_args,
NULL) != 0)
goto send_fail;
}
/* P7 Wave D: transmit the captured directory times last. The scanner thread
@@ -1884,11 +1922,12 @@ static int send_chunks_multithreaded(void* pipeline_context) {
if (!context->scan_stopped_early &&
!send_dir_times(client, context->config, context->dir_entries))
goto send_fail;
bool ok = finalize_transfer(client, context->config, context->remove_source_files);
bool delete_limit = false;
bool ok = finalize_transfer(client, context->config, context->remove_source_files, &delete_limit);
context->delete_limit = delete_limit;
if (!ok && context->config->use_delete)
log_message(LOG_LEVEL_ERROR,
"server reported a deletion failure (--delete); see the server log for the "
"reason (a --max-delete limit that the run would exceed deletes nothing)");
"server reported a deletion failure (--delete); see the server log for the reason");
if (ok)
remove_transferred_sources(context->config, context->remove_source_files);
mtx_lock(&context->mutex_progress);
@@ -1931,11 +1970,18 @@ static int scan_directory_multithreaded(void* pipeline_context) {
prepared.options.dir_entries = context->dir_entries;
prepared.options.dir_entries_mutex = &context->dir_entries_mutex;
/* The keep-set manifest for the late modes is built from this data pass, so
the parallel scanner records the protected excluded prefixes here. The
early modes already transmitted the pre-scan keep-set and its protected
list, so the data pass must not append to it again. */
if (!context->early_delete)
the parallel scanner records the protected excluded prefixes and the
synchronized directories here (the size-prune protection is collected in
every mode). The early modes already transmitted the pre-scan keep-set and
its protected lists, so the data pass must not append to them again. */
if (!context->early_delete) {
prepared.options.excluded_paths = context->excluded_paths;
/* The root marker for a full recursive transfer is already in the list; do
not let the scanner append every directory to it. */
if (context->config->files_from_set != NULL)
prepared.options.synced_dirs = context->synced_dirs;
}
prepared.options.size_skipped_paths = context->size_skipped_paths;
bool dirs_mode = prepared.options.dirs;
/* -H also selects the sequential scanner (see the comment at the branch),
* so the loop below must choose the scanner by which object exists, not by
@@ -2242,6 +2288,9 @@ int send_files(Config* config) {
ArrayList* dir_entries = NULL;
/* Protected excluded prefixes (delete-excluded default protection). */
ArrayList* excluded = NULL;
/* Size-pruned prefixes (always protected) and synchronized directories. */
ArrayList* size_skipped = NULL;
ArrayList* synced_dirs = NULL;
bool delete_early = config->use_delete && config_delete_timing_early(config);
bool send_failed = false;
bool had_scan_io = false;
@@ -2265,11 +2314,31 @@ int send_files(Config* config) {
by user-selection rules so the receiver protects their destination mirrors
from --delete (rsync's default). Only scans that build the keep-set get the
sink attached (prescan for early timing, the streaming data pass otherwise). */
if (config->use_delete && !config->delete_excluded) {
excluded = array_list_create(free);
if (!excluded)
if (config->use_delete) {
if (!config->delete_excluded) {
excluded = array_list_create(free);
if (!excluded)
goto send_fail;
prepared.options.excluded_paths = excluded;
}
size_skipped = array_list_create(free);
synced_dirs = array_list_create(free);
if (!size_skipped || !synced_dirs)
goto send_fail;
prepared.options.excluded_paths = excluded;
prepared.options.size_skipped_paths = size_skipped;
/* Only a --files-from subset confines the extras walk to the directories
the scan synchronized; a full recursive transfer deletes throughout the
receive root, so mark the root itself (the "." sentinel) and let the
scanner record nothing extra. */
if (config->files_from_set == NULL) {
char* root_marker = str_dup(".");
if (!root_marker || !array_list_add(synced_dirs, root_marker)) {
free(root_marker);
goto send_fail;
}
} else {
prepared.options.synced_dirs = synced_dirs;
}
}
/* The late-timing modes (plain --delete / --delete-after / --delete-delay)
build the manifest while streaming and send it after the last data frame.
@@ -2296,13 +2365,16 @@ int send_files(Config* config) {
"with an empty keep-set (--delete)");
prescan_ok = false;
} else {
early_ok = send_delete_manifest_early(client, early_manifest, excluded, missing_args);
early_ok = send_delete_manifest_early(client, early_manifest, excluded, size_skipped,
missing_args, synced_dirs);
}
}
array_list_delete(early_manifest);
/* The keep-set (and its protected prefixes) are already on the wire; the
data pass must not append to the exclusion list again. */
/* The keep-set (and its protected prefixes and synchronized directories) are
already on the wire; the data pass must not append to those lists again. */
prepared.options.excluded_paths = NULL;
prepared.options.size_skipped_paths = NULL;
prepared.options.synced_dirs = NULL;
if (!prescan_ok || !early_ok)
goto send_fail;
} else if (config->use_delete) {
@@ -2446,7 +2518,8 @@ int send_files(Config* config) {
--delete-missing-args exact-path deletions only after the transfer
succeeds. In the early modes (--delete-before/--delete-during) the
manifest already went out up front, so nothing is re-sent here. */
if (send_delete_manifest(client->file_descriptor, manifest, excluded, missing_args) != 0) {
if (send_delete_manifest(client->file_descriptor, manifest, excluded, size_skipped,
missing_args, synced_dirs) != 0) {
if (manifest) {
array_list_delete(manifest);
manifest = NULL;
@@ -2464,11 +2537,11 @@ int send_files(Config* config) {
applying them until after its own deletion/publication phase. */
if (!send_dir_times(client, config, dir_entries))
goto send_fail;
bool ok = finalize_transfer(client, config, remove_sources);
bool delete_limit = false;
bool ok = finalize_transfer(client, config, remove_sources, &delete_limit);
if (!ok && config->use_delete)
log_message(LOG_LEVEL_ERROR,
"server reported a deletion failure (--delete); see the server log for the "
"reason (a --max-delete limit that the run would exceed deletes nothing)");
"server reported a deletion failure (--delete); see the server log for the reason");
if (ok)
remove_transferred_sources(config, remove_sources);
if (config->show_progress && !config->quiet)
@@ -2477,8 +2550,13 @@ int send_files(Config* config) {
log_info_message(LOG_INFO_STATS, "Transfer summary: %d files, %.1f MB", total_files,
(double)total_bytes / (double)BYTES_PER_MIB);
/* --ignore-errors: an unreadable source directory was skipped but the run
still completed (and deleted); report the run as errored like rsync does. */
ret = (ok && !had_scan_io) ? 0 : 1;
still completed (and deleted); report the run as errored like rsync does.
A --max-delete-capped commit is a successful transfer that rsync reports
with exit code 25. */
if (!ok || had_scan_io)
ret = 1;
else
ret = delete_limit ? 25 : 0;
send_fail:
/* Single cleanup path for all exits. The manifest is intentionally deleted
@@ -2487,6 +2565,10 @@ send_fail:
array_list_delete(manifest);
if (excluded)
array_list_delete(excluded);
if (size_skipped)
array_list_delete(size_skipped);
if (synced_dirs)
array_list_delete(synced_dirs);
if (missing_args)
array_list_delete(missing_args);
if (remove_sources)
@@ -2592,16 +2674,41 @@ int send_files_multithreaded(Config** config_ptr) {
return 1;
}
}
/* Size-pruned mirrors stay protected under every mode (even
--delete-excluded); synchronized directories confine the walk. A full
recursive transfer marks the receive root itself (".") so the walk is not
confined; only a --files-from subset records concrete directories. */
context->size_skipped_paths = array_list_create(free);
context->synced_dirs = array_list_create(free);
if (!context->size_skipped_paths || !context->synced_dirs) {
pipeline_context_sender_destroy(context);
return 1;
}
if (config->files_from_set == NULL) {
char* root_marker = str_dup(".");
if (!root_marker || !array_list_add(context->synced_dirs, root_marker)) {
free(root_marker);
pipeline_context_sender_destroy(context);
return 1;
}
}
if (config_delete_timing_early(config)) {
/* --delete-before/--delete-during: build the complete keep-set manifest
(paths only, nothing loaded or sent) up front so the sender thread can
transmit it before the first data byte. The path-only pre-scan also
fills the protected excluded prefixes. */
fills the protected excluded prefixes and synchronized directories. */
PreparedScanner prepared;
memset(&prepared, 0, sizeof(prepared));
bool prepared_ok = prepare_scanner(config, config->scanner_threads, &prepared);
if (prepared_ok && context->excluded_paths)
prepared.options.excluded_paths = context->excluded_paths;
if (prepared_ok) {
if (context->excluded_paths)
prepared.options.excluded_paths = context->excluded_paths;
prepared.options.size_skipped_paths = context->size_skipped_paths;
/* The root marker for a full recursive transfer is already in the list;
only a --files-from subset needs the scanner to record directories. */
if (config->files_from_set != NULL)
prepared.options.synced_dirs = context->synced_dirs;
}
bool prebuilt = prepared_ok && scan_paths_only(config, &prepared.options, context->manifest,
&context->scan_had_io_error);
prepared_scanner_destroy(&prepared);
@@ -2683,9 +2790,13 @@ int send_files_multithreaded(Config** config_ptr) {
scan_io = context->scan_had_io_error;
mtx_unlock(&context->mutex_scanner);
bool sender_ok = sender_result == thrd_success;
bool delete_limit = context->delete_limit;
/* --ignore-errors: the run completed (and deleted) past an unreadable source
directory; report it as errored like rsync does. */
directory; report it as errored like rsync does. A --max-delete-capped
commit is a successful transfer that rsync reports with exit code 25. */
pipeline_context_sender_destroy(context);
client_set_abort_armed(false);
return sender_ok && !scan_io ? 0 : 1;
if (!sender_ok || scan_io)
return 1;
return delete_limit ? 25 : 0;
}
+81 -19
View File
@@ -156,9 +156,13 @@ typedef struct {
bool is_symlink;
char* link_target;
/* True when the entry was pruned by a user selection rule (--filter/-C/per-dir
rules, the --exclude/--include layer, or --max-size/--min-size) rather than
skipped for another reason (unreadable, symlink policy, not applicable). */
rules or the --exclude/--include layer) rather than skipped for another
reason (unreadable, symlink policy, not applicable). */
bool excluded;
/* True when the entry was skipped specifically by --max-size/--min-size.
Size pruning protects the destination mirror even under --delete-excluded,
so it is recorded into a separate sink from `excluded`. */
bool size_excluded;
} ScannerEntry;
/* --one-file-system (-x) decision. Only directories can carry a different
@@ -302,19 +306,49 @@ static bool excluded_sink_append(ArrayList* list, mtx_t* mtx, const char* rel) {
return ok;
}
/* Record one pruned-by-user-selection filesystem path in the scanner's
exclusion sink (see ScannerOptions.excluded_paths). The stored form is the
entry's wire/destination-relative path (a single leading '/' removed, exactly
how manifest keep entries are stored), so the receiver's walker prefixes
match the destination layout. An allocation failure is a fatal scan error. */
static void scanner_record_excluded(DirectoryScanner* scanner, const char* fs_path) {
if (!scanner->options.excluded_paths || !fs_path)
/* Record one pruned filesystem path in a delete-protection sink. The stored
form is the entry's wire/destination-relative path (a single leading '/'
removed, exactly how manifest keep entries are stored), so the receiver's
walker prefixes match the destination layout. An allocation failure is a
fatal scan error. */
static void scanner_record_protected(DirectoryScanner* scanner, const char* fs_path,
ArrayList* sink) {
if (!sink || !fs_path)
return;
const char* rel = *fs_path == '/' ? fs_path + 1 : fs_path;
if (!excluded_sink_append(scanner->options.excluded_paths, scanner->options.excluded_mutex, rel))
if (!excluded_sink_append(sink, scanner->options.excluded_mutex, rel))
scanner->failed = true;
}
/* A user-selection exclusion (--filter/-C/per-dir or --exclude/--include). */
static void scanner_record_excluded(DirectoryScanner* scanner, const char* fs_path) {
scanner_record_protected(scanner, fs_path, scanner->options.excluded_paths);
}
/* A --max-size/--min-size prune (always protected, even under --delete-excluded). */
static void scanner_record_size_skipped(DirectoryScanner* scanner, const char* fs_path) {
scanner_record_protected(scanner, fs_path, scanner->options.size_skipped_paths);
}
/* Record a directory the scan synchronized. `fs_path` is its absolute path and
`rel` its path relative to the transfer root ("" for the root); the stored
form matches the wire layout (the bare relative path in -R+--files-from, else
the source path with a leading '/' removed, with "." for the receive root).
Returns false on allocation failure. */
static bool scanner_record_synced_dir(const ScannerOptions* options, const char* fs_path,
const char* rel, bool relative_mode) {
if (!options->synced_dirs)
return true;
if (!file_list_dir_in_scope(options->file_list, rel))
return true;
const char* dest = relative_mode ? rel : fs_path;
if (dest[0] == '/')
dest++;
if (dest[0] == '\0')
dest = ".";
return excluded_sink_append(options->synced_dirs, options->excluded_mutex, dest);
}
/* Merge the open directory's own .rsync-filter rules into the inherited
* context, returning the context used for this directory's entries. On a parse
* error the scanner is marked failed. Returns 0 on success, -1 on failure. */
@@ -357,6 +391,7 @@ static int open_directory_filter_context(DirectoryScanner* scanner, const Filter
static int scanner_inspect_entry(const ScannerOptions* options, const char* containing_dir,
const char* link_rel, const char* name, ScannerEntry* entry) {
entry->excluded = false;
entry->size_excluded = false;
entry->is_symlink = false;
entry->link_target = NULL;
entry->path = path_cat(containing_dir, name);
@@ -443,6 +478,7 @@ apply_filters:
if ((options->max_size > 0 && (unsigned long long)entry->stats.st_size > options->max_size) ||
(options->min_size > 0 && (unsigned long long)entry->stats.st_size < options->min_size)) {
entry->excluded = true;
entry->size_excluded = true;
goto skip;
}
return 1;
@@ -722,6 +758,18 @@ static int open_next_directory(DirectoryScanner* scanner) {
scanner->current_path = NULL;
return -1;
}
/* A successfully opened directory is synchronized for --delete: record it
so the receiver confines its extras walk to these (and the root sentinel
".") instead of the whole receive root. */
if (!scanner_record_synced_dir(&scanner->options, scanner->current_path, scanner->current_rel,
scanner->relative_mode)) {
closedir(scanner->current_dir);
scanner->current_dir = NULL;
free(scanner->current_path);
scanner->current_path = NULL;
scanner->failed = true;
return -1;
}
if (scanner->options.capture_dir_times &&
!scanner_capture_dir_time(scanner->options.dir_entries, scanner->options.dir_entries_mutex,
scanner->root_path, scanner->current_path, scanner->relative_mode,
@@ -1041,16 +1089,19 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
break;
}
if (inspection == 0) {
/* The entry was pruned by a user selection rule (exclude/include/size) or
skipped for another reason; only the user-selection prunes protect the
corresponding destination mirror from --delete. */
/* A user-selection exclude protects its destination mirror from --delete
unless --delete-excluded; a size prune is always protected. Other
skips (unreadable, symlink policy) protect nothing. */
if (inspected.excluded) {
char* abs_path = path_cat(scanner->current_path, entry->d_name);
if (!abs_path) {
scanner->failed = true;
break;
}
scanner_record_excluded(scanner, abs_path);
if (inspected.size_excluded)
scanner_record_size_skipped(scanner, abs_path);
else
scanner_record_excluded(scanner, abs_path);
free(abs_path);
}
continue;
@@ -1404,16 +1455,19 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
return;
}
if (inspection == 0) {
if (inspected.excluded && options->excluded_paths) {
/* A root-level user-selection prune protects the destination mirror of
the same-named wire path (at the root the bare name is the wire path in
every layout). */
ArrayList* sink = NULL;
if (inspected.excluded)
sink = inspected.size_excluded ? options->size_skipped_paths : options->excluded_paths;
if (sink) {
/* A root-level prune protects the destination mirror of the same-named
wire path (at the root the bare name is the wire path in every
layout). */
char* abs_path = path_cat(root_directory, entry->d_name);
if (!abs_path) {
ps->failed = true;
} else {
const char* rel = *abs_path == '/' ? abs_path + 1 : abs_path;
if (!excluded_sink_append(options->excluded_paths, options->excluded_mutex, rel))
if (!excluded_sink_append(sink, options->excluded_mutex, rel))
ps->failed = true;
free(abs_path);
}
@@ -1534,6 +1588,14 @@ static bool scan_root_directory(ParallelScanner* ps, const char* root_directory,
log_perror("Could not open root directory for parallel scan");
return false;
}
/* The parallel scanner opens the transfer root directly (not through
open_next_directory), so record it as synchronized here. */
if (!scanner_record_synced_dir(options, root_directory, "",
options->relative && options->file_list != NULL)) {
closedir(dir);
ps->failed = true;
return false;
}
const struct dirent* entry;
while ((entry = readdir(dir)) != NULL) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
+24 -9
View File
@@ -73,17 +73,32 @@ typedef struct {
bool prune_empty_dirs;
/* Delete-excluded protection sink (optional): when non-NULL the scanner
* appends the destination-relative path of every entry it prunes because a
* USER SELECTION rule excluded it (--filter/-C/per-dir rules, the legacy
* --exclude/--include layer, and --max-size/--min-size). The sender turns
* this list into the manifest's protected prefixes so `--delete` leaves the
* destination mirror of excluded source paths alone (rsync's default), and
* empties it when --delete-excluded opts back into deleting them. NOT
* recorded for --files-from subset pruning (whose delete semantics stay
* keep-set-only) or for -R/--files-from relative wire paths. When
* `excluded_mutex` is non-NULL it is taken around every append (the parallel
* scanner shares one list across its worker threads). */
* USER SELECTION rule excluded it (--filter/-C/per-dir rules and the legacy
* --exclude/--include layer). The sender turns this list into the manifest's
* protected prefixes so `--delete` leaves the destination mirror of excluded
* source paths alone (rsync's default), and drops it when --delete-excluded
* opts back into deleting them. NOT recorded for --files-from subset pruning
* (whose delete semantics derive from the synchronized-directory set) or for
* -R/--files-from relative wire paths. When `excluded_mutex` is non-NULL it
* is taken around every append (the parallel scanner shares one list across
* its worker threads). */
ArrayList* excluded_paths;
mtx_t* excluded_mutex;
/* Size-prune protection sink (optional): when non-NULL the scanner appends
* the destination-relative path of every entry it skipped because of
* --max-size/--min-size. rsync never deletes a size-skipped source mirror,
* even under --delete-excluded, so the sender always transmits this list as
* protected prefixes (unlike excluded_paths, which --delete-excluded drops).
* Guarded by `excluded_mutex` like excluded_paths. */
ArrayList* size_skipped_paths;
/* Synchronized-directory sink (optional): when non-NULL the scanner appends
* the destination-relative path of every directory it is about to traverse
* that lies inside a --files-from listed directory (or of every traversed
* directory when there is no list). The sender sends this set with the delete
* manifest so the receiver confines its extras walk to synchronized
* directories, exactly like rsync; the receive root is the "." sentinel.
* Guarded by `excluded_mutex`. */
ArrayList* synced_dirs;
/* --ignore-errors: an unreadable directory during the scan is recorded as an
* I/O error and skipped instead of aborting the scan. Client-only. */
bool ignore_io_errors;