Merge branch 'fix/parity-fs' into feat/parity-fixes

# Conflicts:
#	RSYNC_COMPAT.md
This commit is contained in:
2026-09-18 21:19:31 +02:00
22 changed files with 1684 additions and 175 deletions
+8 -67
View File
@@ -472,6 +472,13 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
options->exclude_per_dir_filter_files = config->per_dir_filter_count >= 2;
options->dirs = config->dirs;
options->relative = config->relative;
/* A real recursive transfer recreates empty source directories (rsync
parity); low-level scanner users leave this off. */
options->emit_empty_dirs = true;
/* --no-implied-dirs only has meaning with -R (rsync): without it the option
is a documented no-op, so the scanner must not suppress directory
metadata. */
options->no_implied_dirs = config->no_implied_dirs && config->relative;
/* -R/--relative outside --files-from reconstructs every destination path from
* the source spec (rsync's '/./' cut point). With --files-from the listed
* entry already supplies the bare relative path, so no prefix is built. */
@@ -635,72 +642,6 @@ static const char* delete_plan_walk_root(const Config* config, const ArrayList*
return marker;
}
/* True when some --files-from entry is an ancestor-or-equal directory of
* `rel` (an empty entry -- the whole tree "." -- counts as the root). */
static bool file_list_ancestor_listed(const FileListSet* set, const char* rel) {
if (!set)
return true;
for (int i = 0; i < set->count; i++) {
const char* listed = set->entries[i];
if (listed[0] == '\0')
return true;
size_t n = strlen(listed);
if (strncmp(rel, listed, n) == 0 && (rel[n] == '/' || rel[n] == '\0'))
return true;
}
return false;
}
/* --no-implied-dirs (meaningful only with -R + --files-from): a listed file
* may only be placed when its parent directory (or one of its ancestors) is
* itself an explicitly listed entry. rsync omits a file whose implied parent
* directory is suppressed, and an explicitly listed file that cannot be placed
* fails the transfer; FastSync fails the whole run up front with a clear error
* (it has no per-entry skip channel). Without -R or --files-from the option
* has no effect. */
static bool no_implied_dirs_files_from_valid(const Config* config) {
if (!config->no_implied_dirs || !config->relative)
return true;
const FileListSet* set = (const FileListSet*)config->files_from_set;
if (!set)
return true;
for (int i = 0; i < set->count; i++) {
const char* entry = set->entries[i];
if (entry[0] == '\0')
continue;
char* full = path_cat(config->send_directory, entry);
if (!full)
return false;
struct stat st;
bool is_file = lstat(full, &st) == 0 && S_ISREG(st.st_mode);
free(full);
if (!is_file)
continue;
const char* slash = strrchr(entry, '/');
if (!slash)
continue; /* top-level file: its parent is the receive root */
size_t parent_len = (size_t)(slash - entry);
if (parent_len == 0)
continue;
char* parent = malloc(parent_len + 1);
if (!parent)
return false;
memcpy(parent, entry, parent_len);
parent[parent_len] = '\0';
bool listed = file_list_ancestor_listed(set, parent);
if (!listed) {
log_message(LOG_LEVEL_ERROR,
"--no-implied-dirs: cannot place file '%s': parent directory '%s' is not "
"explicitly listed (list the directory or drop --no-implied-dirs)",
entry, parent);
}
free(parent);
if (!listed)
return false;
}
return true;
}
/* The destination-relative mirror path for a missing --files-from entry: where
a PRESENT entry with the same name would have been written. With -R that is
the entry's bare relative path (the bare wire path the receiver uses);
@@ -814,7 +755,7 @@ static bool files_from_list_check(const Config* config, ArrayList* missing_dest,
"--ignore-missing-args: ignored %d missing --files-from entr%s", *skipped_out,
*skipped_out == 1 ? "y" : "ies");
}
return no_implied_dirs_files_from_valid(config);
return true;
}
/* Basis directories are honored by the receiver's per-file incremental check,
+69 -7
View File
@@ -854,7 +854,8 @@ static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const
const char* fs_path, bool relative_mode,
const char* relative_prefix, bool preserve_atimes,
bool preserve_crtimes, bool preserve_xattrs,
bool preserve_acls) {
bool preserve_acls, bool no_implied_dirs,
const FileListSet* file_list) {
if (!dir_entries || !root_path || !fs_path)
return true;
struct stat st;
@@ -863,6 +864,13 @@ static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const
char* rel = scanner_path_relative(root_path, fs_path);
if (!rel)
return true;
/* --no-implied-dirs: an implied parent directory (not listed, and not under
a listed directory) keeps the destination's own/default attributes, so its
source metadata is not transmitted. */
if (no_implied_dirs && file_list && !file_list_dir_in_scope(file_list, rel)) {
free(rel);
return true;
}
if (relative_mode && rel[0] == '\0') {
/* -R + --files-from: the transfer root itself has no bare relative wire
path (matches the -R scan, which never emits the root). */
@@ -926,6 +934,43 @@ static bool scanner_capture_dir_time(ArrayList* dir_entries, mtx_t* mutex, const
return true;
}
/* Recursive scan: emit a payload-less directory entry for the directory that
* just finished scanning. rsync creates every source directory at the
* destination; FastSync otherwise creates one only implicitly through a
* transferred child, so a directory emptied on the transfer side (physically
* empty, or all of its entries filtered out) would never appear. The transfer
* root is skipped (it maps to the receive root, which already exists), as are
* --files-from (only listed items and their implied parents transfer),
* --list-only (directory lines are emitted by the caller) and
* -m/--prune-empty-dirs. Returns false on allocation failure. */
static bool scanner_emit_empty_dir(DirectoryScanner* scanner, ArrayList* chunk_data) {
if (!scanner->current_path || !scanner->current_rel || scanner->current_rel[0] == '\0')
return true;
struct stat st;
if (stat(scanner->current_path, &st) != 0 || !S_ISDIR(st.st_mode))
return true;
File* dir = scanner_build_dir_file(scanner->current_path, &st, &scanner->options);
if (!dir)
return false;
if (scanner->relative_mode) {
dir->send_path = str_dup(scanner->current_rel);
} else if (scanner->options.relative_prefix) {
dir->send_path =
scanner_prefix_send_path(scanner->options.relative_prefix, scanner->current_rel);
}
if ((scanner->relative_mode || scanner->options.relative_prefix) && !dir->send_path) {
file_destroy(dir);
return false;
}
if (scanner->options.preserve_xattrs || scanner->options.preserve_acls)
dir->xattrs = xattr_capture_path(scanner->current_path, scanner->options.preserve_acls);
if (!array_list_add(chunk_data, dir)) {
file_destroy(dir);
return false;
}
return true;
}
/* Open the next queued directory and set up its filter context. Returns 1 when
a directory is open, 0 when the queue is exhausted, and -1 on a fatal error.
A directory that cannot be opened is an I/O error: it is recorded on the
@@ -949,6 +994,7 @@ static int open_next_directory(DirectoryScanner* scanner) {
* the directory that enqueued them. */
const FilterNode* inherited = scanner->at_seed_dir ? scanner->seed_node : de->context;
scanner->at_seed_dir = false;
scanner->current_dir_produced = false;
free(de);
free(scanner->current_rel);
@@ -1016,7 +1062,8 @@ static int open_next_directory(DirectoryScanner* scanner) {
scanner->options.dir_entries, scanner->options.dir_entries_mutex, scanner->root_path,
scanner->current_path, scanner->relative_mode, scanner->options.relative_prefix,
scanner->options.preserve_atimes, scanner->options.preserve_crtimes,
scanner->options.preserve_xattrs, scanner->options.preserve_acls)) {
scanner->options.preserve_xattrs, scanner->options.preserve_acls,
scanner->options.no_implied_dirs, scanner->options.file_list)) {
closedir(scanner->current_dir);
scanner->current_dir = NULL;
free(scanner->current_path);
@@ -1381,10 +1428,22 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
const struct dirent* entry = readdir(scanner->current_dir);
if (entry == NULL) {
/* The directory is exhausted: if nothing was transferred or descended
from it, recreate it at the destination as an explicit entry. */
if (scanner->options.emit_empty_dirs && !scanner->current_dir_produced &&
!scanner->options.prune_empty_dirs && !scanner->options.list_dirs &&
scanner->options.file_list == NULL) {
if (!scanner_emit_empty_dir(scanner, chunk_data))
scanner->failed = true;
}
closedir(scanner->current_dir);
scanner->current_dir = NULL;
free(scanner->current_path);
scanner->current_path = NULL;
if (scanner->failed) {
array_list_delete(chunk_data);
return NULL;
}
continue;
}
@@ -1519,6 +1578,7 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
scanner->failed = true;
break;
}
scanner->current_dir_produced = true;
free(cur_path);
continue;
}
@@ -1533,6 +1593,7 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
break;
}
}
scanner->current_dir_produced = true;
int next_depth = scanner->current_depth + 1;
if (scanner->options.max_depth <= 0 || next_depth < scanner->options.max_depth) {
DirEntry* de = dir_entry_create(cur_path, next_depth, scanner->current_node);
@@ -1609,6 +1670,7 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
scanner->failed = true;
break;
}
scanner->current_dir_produced = true;
chunk_data_size += file->data->size;
if (chunk_data_size > scanner->options.chunk_size) {
free(rel_copy);
@@ -2236,11 +2298,11 @@ ParallelScanner* parallel_scanner_create_with_options(const char* root_directory
transfer root itself (it hands the root's immediate subdirectories to
workers), so capture the root's directory time here. */
if (options->capture_dir_times &&
!scanner_capture_dir_time(options->dir_entries, options->dir_entries_mutex, root_directory,
root_directory, options->relative && options->file_list != NULL,
options->relative_prefix, options->preserve_atimes,
options->preserve_crtimes, options->preserve_xattrs,
options->preserve_acls)) {
!scanner_capture_dir_time(
options->dir_entries, options->dir_entries_mutex, root_directory, root_directory,
options->relative && options->file_list != NULL, options->relative_prefix,
options->preserve_atimes, options->preserve_crtimes, options->preserve_xattrs,
options->preserve_acls, options->no_implied_dirs, options->file_list)) {
array_list_delete(root_files);
array_list_delete(subdirs);
parallel_scanner_destroy(ps);
+16
View File
@@ -160,6 +160,17 @@ typedef struct {
bool capture_dir_times;
ArrayList* dir_entries;
mtx_t* dir_entries_mutex;
/* Recreate empty source directories on a recursive transfer: emit a
* payload-less directory entry for every traversed directory that produced
* no transferred/descended child. Off by default so low-level scanner users
* (unit helpers, --list-only) see only the historical file list; the real
* sender sets it in prepare_scanner. */
bool emit_empty_dirs;
/* --no-implied-dirs with -R + --files-from: a directory that is only an
* implied parent of a listed entry (not itself listed, nor below a listed
* directory) must not carry source metadata; it is created with default
* attributes at the destination, matching rsync. */
bool no_implied_dirs;
} ScannerOptions;
/* Internal per-scanner filter state. FilterNode chains represent the ordered
@@ -177,6 +188,11 @@ typedef struct {
int current_depth;
dev_t root_dev;
bool failed;
/* Recursive scan: whether the open directory yielded any transferred or
descended entry. When it did not, closing it emits a directory entry so
the empty source directory is recreated at the destination (rsync
parity). */
bool current_dir_produced;
/* Phase 2 (files-from / filter layer). */
char* root_path; /* transfer root (fs path) for rel computation */
char* current_rel; /* rel path of the open directory ("" == root) */
+5 -4
View File
@@ -90,8 +90,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(" -m, --prune-empty-dirs Do not transfer empty directory entries (--dirs mode);\n");
printf(" recursive transfers never send empty dirs\n");
printf(" -m, --prune-empty-dirs Do not create empty directories (a recursive transfer\n");
printf(" otherwise recreates them, like rsync)\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");
@@ -103,8 +103,9 @@ void print_usage(void) {
printf(" -R, --relative With --files-from, preserve each listed entry's relative path\n");
printf(" below the destination root instead of mirroring the full\n");
printf(" source path (no effect without --files-from)\n");
printf(" --no-implied-dirs With -R --files-from, refuse to place a listed file whose\n");
printf(" parent directory is not itself listed\n");
printf(" --no-implied-dirs With -R, do not apply the source metadata of a listed file's\n");
printf(" implied parent directories (they are still created with\n");
printf(" default attributes)\n");
printf(" --mkpath Create the destination root directory on the server when it\n");
printf(" does not exist yet\n");
printf(" --exclude <pattern>, --exclude=<pattern> Exclude files matching pattern\n");