Release v2.26.0 #284

Merged
TapTap merged 210 commits from dev into main 2026-09-18 19:05:52 +02:00
2 changed files with 53 additions and 3 deletions
Showing only changes of commit 695b5c8c25 - Show all commits
+23 -3
View File
@@ -1289,9 +1289,17 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
wire path, not its source path (which would not match the destination
layout and would leave the mirror deletable). */
if (inspected.excluded) {
char* protected_path = scanner->relative_mode
? child_rel_path(scanner->current_rel, entry->d_name)
: path_cat(scanner->current_path, entry->d_name);
char* protected_path;
if (scanner->relative_mode) {
protected_path = child_rel_path(scanner->current_rel, entry->d_name);
} else if (scanner->options.relative_prefix) {
char* relc = child_rel_path(scanner->current_rel, entry->d_name);
protected_path =
relc ? scanner_prefix_send_path(scanner->options.relative_prefix, relc) : NULL;
free(relc);
} else {
protected_path = path_cat(scanner->current_path, entry->d_name);
}
if (!protected_path) {
scanner->failed = true;
break;
@@ -1753,8 +1761,20 @@ static void scan_root_entry(const ScannerOptions* options, const FilterNode* roo
bool files_from_prune = options->file_list && !file_list_affects(options->file_list, rel);
if (!files_from_prune && !use_rel && options->excluded_paths) {
const char* rel_path = *cur_path == '/' ? cur_path + 1 : cur_path;
char* prefixed = NULL;
if (options->relative_prefix) {
prefixed = scanner_prefix_send_path(options->relative_prefix, entry->d_name);
if (!prefixed) {
free(rel);
free(cur_path);
ps->failed = true;
return;
}
rel_path = prefixed;
}
if (!excluded_sink_append(options->excluded_paths, options->excluded_mutex, rel_path))
ps->failed = true;
free(prefixed);
}
free(rel);
free(cur_path);
@@ -169,6 +169,36 @@ class TestDirsOneLevel:
== os.path.isfile(os.path.join(rdst, "unrelated", "keep.txt")))
assert _tree(dest) == _tree(rdst)
@requires_rsync
@pytest.mark.ci
@pytest.mark.parametrize("mt", [False, True])
def test_relative_delete_protects_excluded_mirror(self, mt):
"""-R --delete with --exclude must protect the destination mirror of an
excluded source path (recorded as a prefix-relative wire path)."""
source = _make_tree(os.path.join(TEST_DATA_DIR, "sel_delexc_src"))
with open(os.path.join(source, "foo", "secret.tmp"), "wb") as fh:
fh.write(b"secret\n")
dest = os.path.join(TEST_DATA_DIR, "sel_delexc_dst")
rdst = os.path.join(TEST_DATA_DIR, "sel_delexc_rdst")
for root in (dest, rdst):
clean_dir(root)
os.makedirs(os.path.join(root, "foo"))
with open(os.path.join(root, "foo", "secret.tmp"), "wb") as fh:
fh.write(b"secret\n")
with open(os.path.join(root, "foo", "extra.txt"), "wb") as fh:
fh.write(b"extra\n")
spec = source + "/./foo"
with ServerManager() as server:
server.start(extra_args=["--allow-delete"])
r = _rsync(["-aR", "--delete", "--exclude=*.tmp", spec, rdst + "/"])
assert r.returncode == 0, r.stderr
flags = ["-a", "-R", "--delete", "--exclude=*.tmp"] + (["--threads"] if mt else [])
result, _ = run_client(spec, dest, flags=flags, port=server.port)
assert result.returncode == 0, result.stderr[:300]
assert _tree(dest) == _tree(rdst)
assert os.path.isfile(os.path.join(dest, "foo", "secret.tmp"))
assert not os.path.exists(os.path.join(dest, "foo", "extra.txt"))
class TestClientAliases:
"""#5: safe rsync option aliases accepted client-side."""