Compare commits
2
Commits
0feb545008
...
e754f0d4eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e754f0d4eb | ||
|
|
4b74f6a41d |
No files matched your search
@@ -262,12 +262,26 @@ static bool files_from_list_check(const Config* config, ArrayList* missing_dest,
|
||||
free(full);
|
||||
}
|
||||
if (*skipped_out > 0) {
|
||||
if (config->delete_missing_args)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"--delete-missing-args: %d missing --files-from entr%s will be deleted from the "
|
||||
"destination",
|
||||
*skipped_out, *skipped_out == 1 ? "y" : "ies");
|
||||
else if (config->ignore_missing_args)
|
||||
if (config->delete_missing_args) {
|
||||
/* --list-only never deletes and a --dry-run only shows intent, so the
|
||||
summary must not claim a real deletion happened in those modes. */
|
||||
if (config->list_only)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"--delete-missing-args: %d missing --files-from entr%s skipped (--list-only "
|
||||
"never deletes)",
|
||||
*skipped_out, *skipped_out == 1 ? "y" : "ies");
|
||||
else if (config->dry_run)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"--delete-missing-args: %d missing --files-from entr%s would be deleted from "
|
||||
"the destination (dry run)",
|
||||
*skipped_out, *skipped_out == 1 ? "y" : "ies");
|
||||
else
|
||||
log_message(
|
||||
LOG_LEVEL_WARNING,
|
||||
"--delete-missing-args: %d missing --files-from entr%s will be deleted from the "
|
||||
"destination",
|
||||
*skipped_out, *skipped_out == 1 ? "y" : "ies");
|
||||
} else if (config->ignore_missing_args)
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
"--ignore-missing-args: ignored %d missing --files-from entr%s", *skipped_out,
|
||||
*skipped_out == 1 ? "y" : "ies");
|
||||
|
||||
+24
-17
@@ -1590,13 +1590,21 @@ bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(full, &leaf, false);
|
||||
if (parent_fd < 0) {
|
||||
/* The mirror's parent directory may itself not exist on the destination
|
||||
(a deeper missing entry whose leading directories were never created).
|
||||
That is a no-op -- there is nothing to delete -- matching
|
||||
file_remove_tree_secure's absent-path handling; only a genuine I/O
|
||||
error (EACCES, a symlink loop, ...) fails the run. */
|
||||
bool absent = errno == ENOENT || errno == ENOTDIR;
|
||||
free(full);
|
||||
free(leaf);
|
||||
ok = false;
|
||||
if (!absent)
|
||||
ok = false;
|
||||
continue;
|
||||
}
|
||||
struct stat st;
|
||||
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
||||
/* Already absent: nothing to delete (a no-op, not a deletion). */
|
||||
if (errno != ENOENT)
|
||||
ok = false;
|
||||
close(parent_fd);
|
||||
@@ -1604,24 +1612,20 @@ bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest
|
||||
free(full);
|
||||
continue;
|
||||
}
|
||||
bool removed = false;
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) == 0 || errno == ENOENT) {
|
||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
if (unlinkat(parent_fd, leaf, AT_REMOVEDIR) == 0) {
|
||||
removed = true;
|
||||
} else if (errno == ENOTEMPTY || errno == EEXIST) {
|
||||
close(parent_fd);
|
||||
parent_fd = -1;
|
||||
free(leaf);
|
||||
leaf = NULL;
|
||||
if (config->use_delete || config->force_delete) {
|
||||
if (file_remove_tree_secure(full)) {
|
||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
} else {
|
||||
if (!file_remove_tree_secure(full))
|
||||
ok = false;
|
||||
}
|
||||
else
|
||||
removed = true;
|
||||
} else {
|
||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_WARNING,
|
||||
@@ -1630,18 +1634,21 @@ bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest
|
||||
escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
}
|
||||
} else {
|
||||
} else if (errno != ENOENT) {
|
||||
ok = false;
|
||||
}
|
||||
} else {
|
||||
if (unlinkat(parent_fd, leaf, 0) == 0 || errno == ENOENT) {
|
||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
} else {
|
||||
if (unlinkat(parent_fd, leaf, 0) == 0) {
|
||||
removed = true;
|
||||
} else if (errno != ENOENT) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
if (removed) {
|
||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||
fprintf(stderr, " Deleted: %s\n", escaped ? escaped : "<allocation failed>");
|
||||
free(escaped);
|
||||
}
|
||||
if (parent_fd >= 0)
|
||||
close(parent_fd);
|
||||
free(leaf);
|
||||
|
||||
@@ -1955,6 +1955,64 @@ class TestMissingArgs:
|
||||
assert not os.path.exists(os.path.join(dest, "extra.txt")), \
|
||||
"--delete-before implies --delete: unrelated extras must go"
|
||||
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
@pytest.mark.parametrize("relative", [False, True])
|
||||
def test_delete_missing_deep_entry_with_absent_parent(self, mt, relative):
|
||||
"""A missing entry whose destination mirror's parent directory does not
|
||||
exist is a no-op (nothing to delete), never a run failure: the
|
||||
exact-path deletions must not abort the --delete extras walk. Covers
|
||||
the -R bare-relative layout and the full source-mirror layout."""
|
||||
source = self._make_source("mg_deep_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "mg_deep_dst")
|
||||
clean_dir(dest)
|
||||
rel_flags = ["-R"] if relative else []
|
||||
with ServerManager() as server:
|
||||
server.start(extra_args=["--allow-delete"])
|
||||
if relative:
|
||||
target_root = dest
|
||||
else:
|
||||
# Non-relative layout: seed a.txt so the receive-root mirror
|
||||
# tree exists (its sub/ sibling deliberately does not).
|
||||
seed = _write_rel_list(b"a.txt\n")
|
||||
result, _ = run_client(source, dest,
|
||||
flags=["--files-from", seed] + rel_flags,
|
||||
port=server.port)
|
||||
assert result.returncode == 0, f"seed sync failed: {result.stderr[:200]}"
|
||||
target_root = get_dest_received_dir(dest, source)
|
||||
assert os.path.isfile(os.path.join(target_root, "a.txt"))
|
||||
with open(os.path.join(target_root, "extra.txt"), "w") as fh:
|
||||
fh.write("extra")
|
||||
|
||||
lst = _write_rel_list(b"a.txt\nsub/gone.txt\n")
|
||||
flags = ["--files-from", lst, "--delete-missing-args", "--delete"] + rel_flags + \
|
||||
(["-m"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=server.port)
|
||||
assert result.returncode == 0, \
|
||||
f"deep missing-entry sync failed: {result.stderr[:300]}"
|
||||
assert _read_file(os.path.join(target_root, "a.txt")) == b"a\n"
|
||||
assert not os.path.exists(os.path.join(target_root, "extra.txt")), \
|
||||
"--delete extras walk was aborted by the absent-parent missing entry"
|
||||
assert not os.path.exists(os.path.join(target_root, "sub")), \
|
||||
"the absent parent directory of the missing entry was created"
|
||||
|
||||
@pytest.mark.parametrize("mt", [False, True])
|
||||
def test_dirs_missing_entry_skipped_in_scanner(self, shared_server, mt):
|
||||
"""--dirs + --files-from: a listed-but-missing entry is skipped in the
|
||||
--dirs generator (which would otherwise hard-fail), transferring the
|
||||
rest of the list."""
|
||||
source = self._make_source("mg_dirs_src")
|
||||
dest = os.path.join(TEST_DATA_DIR, "mg_dirs_dst")
|
||||
clean_dir(dest)
|
||||
lst = _write_rel_list(b"a.txt\ngone.txt\n")
|
||||
flags = ["--files-from", lst, "--dirs", "-R", "--ignore-missing-args"] + \
|
||||
(["-m"] if mt else [])
|
||||
result, _ = run_client(source, dest, flags=flags, port=shared_server.port)
|
||||
assert result.returncode == 0, f"--dirs ignore-missing sync failed: {result.stderr[:300]}"
|
||||
assert _read_file(os.path.join(dest, "a.txt")) == b"a\n", \
|
||||
"the listed present file was not transferred"
|
||||
assert not os.path.exists(os.path.join(dest, "gone.txt")), \
|
||||
"a directory/file was created for the missing --dirs entry"
|
||||
|
||||
|
||||
class TestNoImpliedDirs:
|
||||
"""--no-implied-dirs (only meaningful with -R + --files-from) refuses to
|
||||
|
||||
@@ -638,6 +638,9 @@ static void test_manifest_delete_missing_args() {
|
||||
EXPECT_TRUE(array_list_add(manifest->missing, str_dup("empty_dir")));
|
||||
EXPECT_TRUE(array_list_add(manifest->missing, str_dup("full_dir")));
|
||||
EXPECT_TRUE(array_list_add(manifest->missing, str_dup("never_here.txt")));
|
||||
/* A deeper entry whose destination parent directory does not exist is a
|
||||
no-op (nothing to delete), never a failure. */
|
||||
EXPECT_TRUE(array_list_add(manifest->missing, str_dup("no_parent_here/gone.txt")));
|
||||
|
||||
/* Without --delete/--force the non-empty directory survives (rsync parity). */
|
||||
EXPECT_TRUE(manifest_delete_missing_args(cfg, manifest));
|
||||
@@ -655,6 +658,8 @@ static void test_manifest_delete_missing_args() {
|
||||
EXPECT_TRUE(array_list_add(manifest->missing, str_dup("full_dir")));
|
||||
EXPECT_TRUE(manifest_delete_missing_args(cfg, manifest));
|
||||
EXPECT_EQ_INT(access(full_dir, F_OK), -1);
|
||||
snprintf(path, sizeof(path), "%s/no_parent_here", root);
|
||||
EXPECT_EQ_INT(access(path, F_OK), -1);
|
||||
|
||||
delete_manifest_free(manifest);
|
||||
config_delete(cfg);
|
||||
|
||||
Reference in new issue
Block a user