fix(parity): protect -R prefix-relative excluded and size-skipped mirrors
A -R source prune (--exclude/--max-size) must record the destination wire path below the reconstructed prefix so --delete protects it; the parallel root scan and the sequential skip path used the source path instead.
This commit is contained in:
+23
-3
@@ -1289,9 +1289,17 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
|||||||
wire path, not its source path (which would not match the destination
|
wire path, not its source path (which would not match the destination
|
||||||
layout and would leave the mirror deletable). */
|
layout and would leave the mirror deletable). */
|
||||||
if (inspected.excluded) {
|
if (inspected.excluded) {
|
||||||
char* protected_path = scanner->relative_mode
|
char* protected_path;
|
||||||
? child_rel_path(scanner->current_rel, entry->d_name)
|
if (scanner->relative_mode) {
|
||||||
: path_cat(scanner->current_path, entry->d_name);
|
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) {
|
if (!protected_path) {
|
||||||
scanner->failed = true;
|
scanner->failed = true;
|
||||||
break;
|
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);
|
bool files_from_prune = options->file_list && !file_list_affects(options->file_list, rel);
|
||||||
if (!files_from_prune && !use_rel && options->excluded_paths) {
|
if (!files_from_prune && !use_rel && options->excluded_paths) {
|
||||||
const char* rel_path = *cur_path == '/' ? cur_path + 1 : cur_path;
|
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))
|
if (!excluded_sink_append(options->excluded_paths, options->excluded_mutex, rel_path))
|
||||||
ps->failed = true;
|
ps->failed = true;
|
||||||
|
free(prefixed);
|
||||||
}
|
}
|
||||||
free(rel);
|
free(rel);
|
||||||
free(cur_path);
|
free(cur_path);
|
||||||
|
|||||||
@@ -169,6 +169,36 @@ class TestDirsOneLevel:
|
|||||||
== os.path.isfile(os.path.join(rdst, "unrelated", "keep.txt")))
|
== os.path.isfile(os.path.join(rdst, "unrelated", "keep.txt")))
|
||||||
assert _tree(dest) == _tree(rdst)
|
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:
|
class TestClientAliases:
|
||||||
"""#5: safe rsync option aliases accepted client-side."""
|
"""#5: safe rsync option aliases accepted client-side."""
|
||||||
|
|||||||
Reference in New Issue
Block a user