rsync 3.4.1 drop-in parity (#285-#297) + parity completion (protocol 2.26.0) #298
@@ -3099,10 +3099,35 @@ static bool delete_missing_args_budgeted(const Config* config, DeleteManifest* m
|
|||||||
free(leaf);
|
free(leaf);
|
||||||
leaf = NULL;
|
leaf = NULL;
|
||||||
if (config->use_delete || config->force_delete) {
|
if (config->use_delete || config->force_delete) {
|
||||||
if (!file_remove_tree_secure(full))
|
/* Remove the contents entry-by-entry through the budgeted extras
|
||||||
|
walker so every deleted file/dir counts toward --max-delete (rsync
|
||||||
|
parity); the now-empty directory itself costs one more. A run that
|
||||||
|
hits the cap leaves the remaining entries in place. */
|
||||||
|
ArrayList* no_keeps = array_list_create(free);
|
||||||
|
size_t remaining = budget->max_delete - budget->deleted;
|
||||||
|
size_t contents_deleted = 0;
|
||||||
|
size_t contents_skipped = 0;
|
||||||
|
DeleteWalkResult walk =
|
||||||
|
no_keeps ? delete_extras_limited(full, no_keeps, NULL, remaining, NULL, 0,
|
||||||
|
&contents_deleted, &contents_skipped)
|
||||||
|
: DELETE_WALK_ERROR;
|
||||||
|
if (no_keeps)
|
||||||
|
array_list_delete(no_keeps);
|
||||||
|
budget->deleted += contents_deleted;
|
||||||
|
budget->skipped += contents_skipped;
|
||||||
|
if (walk == DELETE_WALK_LIMIT_REACHED) {
|
||||||
|
budget->limit_hit = true;
|
||||||
|
} else if (walk != DELETE_WALK_OK) {
|
||||||
ok = false;
|
ok = false;
|
||||||
else
|
} else if (budget->deleted >= budget->max_delete) {
|
||||||
|
budget->limit_hit = true;
|
||||||
|
budget->skipped++;
|
||||||
|
} else if (file_remove_tree_secure(full)) {
|
||||||
|
budget->deleted++;
|
||||||
removed = true;
|
removed = true;
|
||||||
|
} else {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||||
log_message(LOG_LEVEL_WARNING,
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
|||||||
@@ -2853,6 +2853,36 @@ class TestMissingArgs:
|
|||||||
f"missing-args deletions ignored the --max-delete budget: {remaining}"
|
f"missing-args deletions ignored the --max-delete budget: {remaining}"
|
||||||
assert os.path.isfile(os.path.join(received, "a.txt"))
|
assert os.path.isfile(os.path.join(received, "a.txt"))
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
|
def test_delete_missing_nonempty_dir_counts_each_entry_against_budget(self, mt):
|
||||||
|
"""A non-empty missing-arg directory with --force/--delete is removed
|
||||||
|
entry-by-entry, each counting toward --max-delete (rsync parity): with a
|
||||||
|
small cap the run stops after N files and leaves the rest in place."""
|
||||||
|
source = self._make_source("mg_dirbudget_src")
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, "mg_dirbudget_dst")
|
||||||
|
clean_dir(dest)
|
||||||
|
with ServerManager() as server:
|
||||||
|
server.start(extra_args=["--allow-delete"])
|
||||||
|
result, _ = run_client(source, dest, port=server.port)
|
||||||
|
assert result.returncode == 0, f"seed failed: {result.stderr[:200]}"
|
||||||
|
received = get_dest_received_dir(dest, source)
|
||||||
|
gone = os.path.join(received, "gone")
|
||||||
|
os.makedirs(gone)
|
||||||
|
for i in range(4):
|
||||||
|
with open(os.path.join(gone, f"f{i}"), "w") as fh:
|
||||||
|
fh.write("stale")
|
||||||
|
lst = _write_rel_list(b"a.txt\ngone\n")
|
||||||
|
flags = ["--files-from", lst, "--delete-missing-args", "--force",
|
||||||
|
"--max-delete=2"] + (["--threads"] if mt else [])
|
||||||
|
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||||
|
assert result.returncode == 25, \
|
||||||
|
f"non-empty missing-arg dir should cap at 2 and exit 25: {result.stderr[:300]}"
|
||||||
|
assert os.path.isdir(gone), \
|
||||||
|
"the non-empty missing-arg directory should survive a capped run"
|
||||||
|
remaining = len(os.listdir(gone))
|
||||||
|
assert remaining == 2, f"expected 2 entries left, found {remaining}"
|
||||||
|
assert os.path.isfile(os.path.join(received, "a.txt"))
|
||||||
|
|
||||||
@pytest.mark.parametrize("mt", [False, True])
|
@pytest.mark.parametrize("mt", [False, True])
|
||||||
def test_delete_missing_args_not_blocked_by_exclude_protection(self, mt):
|
def test_delete_missing_args_not_blocked_by_exclude_protection(self, mt):
|
||||||
"""A missing-arg mirror that sits under a filter-excluded directory is an
|
"""A missing-arg mirror that sits under a filter-excluded directory is an
|
||||||
|
|||||||
Reference in New Issue
Block a user