Release v2.26.0 #284
@@ -3099,10 +3099,35 @@ static bool delete_missing_args_budgeted(const Config* config, DeleteManifest* m
|
||||
free(leaf);
|
||||
leaf = NULL;
|
||||
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;
|
||||
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;
|
||||
} else {
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
|
||||
@@ -2853,6 +2853,36 @@ class TestMissingArgs:
|
||||
f"missing-args deletions ignored the --max-delete budget: {remaining}"
|
||||
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])
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user