feat(shared): --delete-missing-args config/wire, manifest third section, receiver exact-path deletions
PROTOCOL_VERSION 2.9.0 -> 2.10.0. The STATUS_MANIFEST frame gains a third section carrying destination-relative exact-delete paths (the missing --files-from entries' mirrors); the config frame gains a delete_missing_args bool (ignore_missing_args stays client-only). The receiver validates the third section like the keep-set and commits it with manifest_delete_all(): manifest_delete_missing_args runs first (explicit user requests, never blocked by protected-prefix exclusion protection; staging/basis protected; a non-empty directory mirror removed only under --force/--delete, rsync parity) and then the ordinary extras walk. Server --allow-delete gates it like --delete.
This commit is contained in:
@@ -191,7 +191,9 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
so the sender only starts streaming once the deletion committed (or
|
so the sender only starts streaming once the deletion committed (or
|
||||||
failed). This is the rsync delete-before/delete-during window: a
|
failed). This is the rsync delete-before/delete-during window: a
|
||||||
later transfer failure does not restore these deletions. */
|
later transfer failure does not restore these deletions. */
|
||||||
bool deletion_ok = config->use_delete ? manifest_delete_extras(config, manifest) : true;
|
bool deletion_ok = (config->use_delete || config->delete_missing_args)
|
||||||
|
? manifest_delete_all(config, manifest)
|
||||||
|
: true;
|
||||||
delete_manifest_free(manifest);
|
delete_manifest_free(manifest);
|
||||||
if (!deletion_ok) {
|
if (!deletion_ok) {
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
@@ -199,9 +201,10 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
}
|
}
|
||||||
if (!send_status(file_descriptor, STATUS_OK))
|
if (!send_status(file_descriptor, STATUS_OK))
|
||||||
goto fail;
|
goto fail;
|
||||||
} else if (config->use_delete) {
|
} else if (config->use_delete || config->delete_missing_args) {
|
||||||
/* Plain --delete / --delete-after / --delete-delay: hold the keep-set
|
/* Plain --delete / --delete-after / --delete-delay and the
|
||||||
and commit the deletion only after STATUS_FINISHED. */
|
--delete-missing-args exact-path deletions: hold the manifest and
|
||||||
|
commit it only after STATUS_FINISHED. */
|
||||||
if (deferred_manifest) {
|
if (deferred_manifest) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Received a second delete manifest");
|
log_message(LOG_LEVEL_ERROR, "Received a second delete manifest");
|
||||||
delete_manifest_free(deferred_manifest);
|
delete_manifest_free(deferred_manifest);
|
||||||
@@ -246,7 +249,7 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
*pending_manifest = deferred_manifest;
|
*pending_manifest = deferred_manifest;
|
||||||
deferred_manifest = NULL;
|
deferred_manifest = NULL;
|
||||||
} else {
|
} else {
|
||||||
bool deletion_ok = manifest_delete_extras(config, deferred_manifest);
|
bool deletion_ok = manifest_delete_all(config, deferred_manifest);
|
||||||
delete_manifest_free(deferred_manifest);
|
delete_manifest_free(deferred_manifest);
|
||||||
deferred_manifest = NULL;
|
deferred_manifest = NULL;
|
||||||
if (!deletion_ok) {
|
if (!deletion_ok) {
|
||||||
|
|||||||
+6
-1
@@ -180,6 +180,11 @@ void handler(int file_descriptor) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
config->use_delete = config->use_delete && allow_delete;
|
config->use_delete = config->use_delete && allow_delete;
|
||||||
|
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
|
||||||
|
deletion and stays gated by the same --allow-delete server policy. When
|
||||||
|
the server policy is off the flag is inert (the missing entries are still
|
||||||
|
skipped via its implied --ignore-missing-args, but nothing is deleted). */
|
||||||
|
config->delete_missing_args = config->delete_missing_args && allow_delete;
|
||||||
/* --mkpath: create the destination root (and its missing leading components)
|
/* --mkpath: create the destination root (and its missing leading components)
|
||||||
before anything else; without it the root must pre-exist. A failure here
|
before anything else; without it the root must pre-exist. A failure here
|
||||||
aborts the connection cleanly before any file data is exchanged. */
|
aborts the connection cleanly before any file data is exchanged. */
|
||||||
@@ -262,7 +267,7 @@ void handler(int file_descriptor) {
|
|||||||
known to have succeeded. Remove the extras before publishing a
|
known to have succeeded. Remove the extras before publishing a
|
||||||
--delay-updates run; the walker skips the staging directory. */
|
--delay-updates run; the walker skips the staging directory. */
|
||||||
if (context->deferred_manifest) {
|
if (context->deferred_manifest) {
|
||||||
if (!manifest_delete_extras(config, context->deferred_manifest)) {
|
if (!manifest_delete_all(config, context->deferred_manifest)) {
|
||||||
transfer_ok = false;
|
transfer_ok = false;
|
||||||
}
|
}
|
||||||
delete_manifest_free(context->deferred_manifest);
|
delete_manifest_free(context->deferred_manifest);
|
||||||
|
|||||||
+23
-10
@@ -96,6 +96,8 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->max_delete = -1;
|
config->max_delete = -1;
|
||||||
config->ignore_errors = false;
|
config->ignore_errors = false;
|
||||||
config->force_delete = false;
|
config->force_delete = false;
|
||||||
|
config->ignore_missing_args = false;
|
||||||
|
config->delete_missing_args = false;
|
||||||
config->filters = NULL;
|
config->filters = NULL;
|
||||||
config->files_from = NULL;
|
config->files_from = NULL;
|
||||||
config->files_from_set = NULL;
|
config->files_from_set = NULL;
|
||||||
@@ -165,10 +167,11 @@ static bool validate_received_config(const Config* config) {
|
|||||||
valid_wire_bool(config->inplace) && valid_wire_bool(config->append) &&
|
valid_wire_bool(config->inplace) && valid_wire_bool(config->append) &&
|
||||||
valid_wire_bool(config->use_fsync) && valid_wire_bool(config->append_verify) &&
|
valid_wire_bool(config->use_fsync) && valid_wire_bool(config->append_verify) &&
|
||||||
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->force_delete) &&
|
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->force_delete) &&
|
||||||
valid_wire_bool(config->delete_after) && valid_wire_bool(config->delete_delay) &&
|
valid_wire_bool(config->delete_missing_args) && valid_wire_bool(config->delete_after) &&
|
||||||
valid_wire_bool(config->delete_during) && valid_wire_bool(config->relative) &&
|
valid_wire_bool(config->delete_delay) && valid_wire_bool(config->delete_during) &&
|
||||||
valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->delay_updates) &&
|
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) &&
|
||||||
valid_wire_bool(config->mkpath) && !(config->delay_updates && config->inplace) &&
|
valid_wire_bool(config->delay_updates) && valid_wire_bool(config->mkpath) &&
|
||||||
|
!(config->delay_updates && config->inplace) &&
|
||||||
!(config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) &&
|
!(config->delay_updates && delay_updates_staging_name_conflict(config->backup_dir)) &&
|
||||||
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
||||||
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
|
valid_wire_bool(config->checksum) && valid_wire_bool(config->eight_bit_output) &&
|
||||||
@@ -421,9 +424,10 @@ static bool send_selection_options(int fd, const Config* c) {
|
|||||||
send_int(fd, c->inplace) && send_int(fd, c->delay_updates) && send_int(fd, c->append) &&
|
send_int(fd, c->inplace) && send_int(fd, c->delay_updates) && send_int(fd, c->append) &&
|
||||||
send_int(fd, c->use_fsync) && send_int(fd, c->append_verify) &&
|
send_int(fd, c->use_fsync) && send_int(fd, c->append_verify) &&
|
||||||
send_int(fd, c->delete_excluded) && send_int(fd, c->force_delete) &&
|
send_int(fd, c->delete_excluded) && send_int(fd, c->force_delete) &&
|
||||||
send_int(fd, c->delete_after) && send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) &&
|
send_int(fd, c->delete_missing_args) && send_int(fd, c->delete_after) &&
|
||||||
send_int(fd, c->relative) && send_int(fd, c->prune_empty_dirs) &&
|
send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) && send_int(fd, c->relative) &&
|
||||||
send_int(fd, c->mkpath) && send_int(fd, c->delete_during) && send_int(fd, c->delete_delay);
|
send_int(fd, c->prune_empty_dirs) && send_int(fd, c->mkpath) &&
|
||||||
|
send_int(fd, c->delete_during) && send_int(fd, c->delete_delay);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool send_skip_compress_options(int fd, const Config* c) {
|
static bool send_skip_compress_options(int fd, const Config* c) {
|
||||||
@@ -532,9 +536,18 @@ static bool receive_file_options(int fd, Config* c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_selection_options(int fd, Config* c) {
|
static bool receive_selection_options(int fd, Config* c) {
|
||||||
bool* flags[] = {&c->ignore_existing, &c->existing, &c->update, &c->inplace,
|
bool* flags[] = {&c->ignore_existing,
|
||||||
&c->delay_updates, &c->append, &c->use_fsync, &c->append_verify,
|
&c->existing,
|
||||||
&c->delete_excluded, &c->force_delete, &c->delete_after};
|
&c->update,
|
||||||
|
&c->inplace,
|
||||||
|
&c->delay_updates,
|
||||||
|
&c->append,
|
||||||
|
&c->use_fsync,
|
||||||
|
&c->append_verify,
|
||||||
|
&c->delete_excluded,
|
||||||
|
&c->force_delete,
|
||||||
|
&c->delete_missing_args,
|
||||||
|
&c->delete_after};
|
||||||
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
||||||
if (!receive_wire_bool(fd, flags[i]))
|
if (!receive_wire_bool(fd, flags[i]))
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
+14
-1
@@ -145,6 +145,19 @@ typedef struct Config {
|
|||||||
* directory by removing that (possibly non-empty, symlink-safe) directory
|
* directory by removing that (possibly non-empty, symlink-safe) directory
|
||||||
* tree first, instead of failing the write. Crosses the wire. */
|
* tree first, instead of failing the write. Crosses the wire. */
|
||||||
bool force_delete;
|
bool force_delete;
|
||||||
|
/* --ignore-missing-args (client-only, never serialized): a --files-from
|
||||||
|
* entry that does not exist under the source is silently skipped instead of
|
||||||
|
* failing the run. Sender-side only: nothing is sent for it and it never
|
||||||
|
* enters the keep-set. Implied by --delete-missing-args. */
|
||||||
|
bool ignore_missing_args;
|
||||||
|
/* --delete-missing-args: implies --ignore-missing-args; additionally each
|
||||||
|
* missing entry's destination mirror (computed like a present entry's wire
|
||||||
|
* path) is deleted receiver-side. Crosses the wire and is gated by the
|
||||||
|
* server's --allow-delete policy like --delete. rsync-parity: independent
|
||||||
|
* of ordinary --delete processing (it does not imply --delete); a non-empty
|
||||||
|
* directory mirror is only removed with --force or --delete in effect, and
|
||||||
|
* the missing-args deletions are not counted toward --max-delete. */
|
||||||
|
bool delete_missing_args;
|
||||||
|
|
||||||
// Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are
|
// Issue #129: Advanced file selection. These fields are CLIENT-ONLY: they are
|
||||||
// never serialized to the wire (the receiver must not learn them).
|
// never serialized to the wire (the receiver must not learn them).
|
||||||
@@ -231,7 +244,7 @@ typedef struct Config {
|
|||||||
DelayUpdatesContext* delay_context;
|
DelayUpdatesContext* delay_context;
|
||||||
} Config;
|
} Config;
|
||||||
|
|
||||||
#define PROTOCOL_VERSION "2.9.0"
|
#define PROTOCOL_VERSION "2.10.0"
|
||||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||||
#define MAX_BASIS_DIRS 64
|
#define MAX_BASIS_DIRS 64
|
||||||
|
|||||||
+159
-8
@@ -1380,12 +1380,16 @@ File* file_receive_directory(int file_descriptor) {
|
|||||||
/* Read a delete-manifest frame (the STATUS_MANIFEST leading code has already
|
/* Read a delete-manifest frame (the STATUS_MANIFEST leading code has already
|
||||||
been consumed): a keep-set entry count followed by that many
|
been consumed): a keep-set entry count followed by that many
|
||||||
destination-relative paths, then a protected-prefix count followed by that
|
destination-relative paths, then a protected-prefix count followed by that
|
||||||
many destination-relative prefixes. The frame is self-delimiting (the counts
|
many destination-relative prefixes, then (protocol 2.10.0+) a missing-args
|
||||||
are authoritative), so the caller decides what to do next and continues
|
count followed by that many destination-relative delete paths. The frame is
|
||||||
reading the following STATUS_* frame. Returns an owned DeleteManifest, or
|
self-delimiting (the counts are authoritative), so the caller decides what to
|
||||||
NULL after sending STATUS_ERROR when the frame is malformed (bad count,
|
do next and continues reading the following STATUS_* frame. Every section is
|
||||||
empty/absolute path, path traversal, or an aggregate size beyond
|
validated identically: an entry must be non-empty, relative and traversal-free
|
||||||
MAX_MANIFEST_BYTES). */
|
and the aggregate length across ALL sections is capped by MAX_MANIFEST_BYTES
|
||||||
|
(so the missing-args deletion requests are confined like the rest of the
|
||||||
|
manifest). Returns an owned DeleteManifest, or NULL after sending STATUS_ERROR
|
||||||
|
when the frame is malformed (bad count, empty/absolute path, path traversal,
|
||||||
|
or an aggregate size beyond MAX_MANIFEST_BYTES). */
|
||||||
static bool receive_manifest_section(int fd, ArrayList* list, size_t* manifest_bytes) {
|
static bool receive_manifest_section(int fd, ArrayList* list, size_t* manifest_bytes) {
|
||||||
int count;
|
int count;
|
||||||
if (!receive_int(fd, &count)) {
|
if (!receive_int(fd, &count)) {
|
||||||
@@ -1418,14 +1422,16 @@ DeleteManifest* receive_manifest_entries(int fd) {
|
|||||||
}
|
}
|
||||||
manifest->keeps = array_list_create(free);
|
manifest->keeps = array_list_create(free);
|
||||||
manifest->protected = array_list_create(free);
|
manifest->protected = array_list_create(free);
|
||||||
if (!manifest->keeps || !manifest->protected) {
|
manifest->missing = array_list_create(free);
|
||||||
|
if (!manifest->keeps || !manifest->protected || !manifest->missing) {
|
||||||
delete_manifest_free(manifest);
|
delete_manifest_free(manifest);
|
||||||
send_status(fd, STATUS_ERROR);
|
send_status(fd, STATUS_ERROR);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
size_t manifest_bytes = 0;
|
size_t manifest_bytes = 0;
|
||||||
if (!receive_manifest_section(fd, manifest->keeps, &manifest_bytes) ||
|
if (!receive_manifest_section(fd, manifest->keeps, &manifest_bytes) ||
|
||||||
!receive_manifest_section(fd, manifest->protected, &manifest_bytes)) {
|
!receive_manifest_section(fd, manifest->protected, &manifest_bytes) ||
|
||||||
|
!receive_manifest_section(fd, manifest->missing, &manifest_bytes)) {
|
||||||
delete_manifest_free(manifest);
|
delete_manifest_free(manifest);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -1437,6 +1443,7 @@ void delete_manifest_free(DeleteManifest* manifest) {
|
|||||||
return;
|
return;
|
||||||
array_list_delete(manifest->keeps);
|
array_list_delete(manifest->keeps);
|
||||||
array_list_delete(manifest->protected);
|
array_list_delete(manifest->protected);
|
||||||
|
array_list_delete(manifest->missing);
|
||||||
free(manifest);
|
free(manifest);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1518,3 +1525,147 @@ bool manifest_delete_extras(const Config* config, DeleteManifest* manifest) {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --delete-missing-args exact-path deletions: each destination mirror in
|
||||||
|
manifest->missing is an explicit user request, so it is removed even when the
|
||||||
|
ordinary extras walk (with its protected prefixes) would leave it alone. The
|
||||||
|
--delay-updates staging directory and basis snapshots are receiver artifacts
|
||||||
|
and stay protected exactly as in the extras walker. A regular file or
|
||||||
|
symlink is unlinked, an empty directory removed, and a NON-empty directory is
|
||||||
|
removed recursively only when --delete or --force is in effect (rsync parity:
|
||||||
|
the man page says a non-empty directory mirror is only deleted with --force
|
||||||
|
or --delete); otherwise it is left with a warning and the run continues. A
|
||||||
|
mirror that does not exist is a no-op. Returns false only on a genuine error
|
||||||
|
(a confinement failure on a validated path or an I/O error), which fails the
|
||||||
|
run. */
|
||||||
|
bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest) {
|
||||||
|
if (!config || !manifest)
|
||||||
|
return false;
|
||||||
|
if (!manifest->missing || manifest->missing->size == 0)
|
||||||
|
return true;
|
||||||
|
fprintf(stderr, "Deleting destination mirrors of missing source arguments...\n");
|
||||||
|
int skip_count = (config->delay_updates ? 1 : 0) + config->basis_count;
|
||||||
|
DeleteSkipEntry* skips = NULL;
|
||||||
|
if (skip_count > 0) {
|
||||||
|
skips = calloc((size_t)skip_count, sizeof(DeleteSkipEntry));
|
||||||
|
if (!skips)
|
||||||
|
return false;
|
||||||
|
int idx = 0;
|
||||||
|
if (config->delay_updates) {
|
||||||
|
skips[idx].prefix = DELAY_UPDATES_STAGING_DIR;
|
||||||
|
skips[idx].top_level_only = true;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < config->basis_count; i++) {
|
||||||
|
skips[idx].prefix = config->basis_dirs[i].path;
|
||||||
|
skips[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bool ok = true;
|
||||||
|
for (int i = 0; i < manifest->missing->size; i++) {
|
||||||
|
const char* rel = (const char*)manifest->missing->items[i];
|
||||||
|
if (!rel || *rel == '\0' || *rel == '/' || has_path_traversal(rel)) {
|
||||||
|
/* Defensive only: receive_manifest_entries already validated every
|
||||||
|
section identically, so a controlled peer never reaches this branch. */
|
||||||
|
log_message(LOG_LEVEL_ERROR, "invalid missing-args delete path");
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool at_root = strchr(rel, '/') == NULL;
|
||||||
|
if (path_under_skip_prefix(rel, at_root, skips, skip_count)) {
|
||||||
|
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"missing-args path '%s' is protected (staging directory or basis snapshot); "
|
||||||
|
"not deleting",
|
||||||
|
escaped ? escaped : "<allocation failed>");
|
||||||
|
free(escaped);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char* full = path_cat(config->receive_root_directory, rel);
|
||||||
|
if (!full) {
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char* leaf = NULL;
|
||||||
|
int parent_fd = file_open_secure_parent(full, &leaf, false);
|
||||||
|
if (parent_fd < 0) {
|
||||||
|
free(full);
|
||||||
|
free(leaf);
|
||||||
|
ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
struct stat st;
|
||||||
|
if (fstatat(parent_fd, leaf, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
||||||
|
if (errno != ENOENT)
|
||||||
|
ok = false;
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
} 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 {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||||
|
log_message(LOG_LEVEL_WARNING,
|
||||||
|
"missing-args destination '%s' is a non-empty directory; use --force or "
|
||||||
|
"--delete to remove it",
|
||||||
|
escaped ? escaped : "<allocation failed>");
|
||||||
|
free(escaped);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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 {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parent_fd >= 0)
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(full);
|
||||||
|
if (!ok)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
free(skips);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Commit every deletion family the manifest carries. The --delete-missing-args
|
||||||
|
exact-path deletions run FIRST: they are explicit user requests and must not
|
||||||
|
be blocked by the extras walker's filter-exclusion protection (a protected
|
||||||
|
leftover inside a missing-argument directory must not make that user-requested
|
||||||
|
removal fail). The ordinary extras walk then runs when --delete is active.
|
||||||
|
Returns true when there was nothing to do or every requested deletion
|
||||||
|
committed. */
|
||||||
|
bool manifest_delete_all(const Config* config, DeleteManifest* manifest) {
|
||||||
|
if (!config || !manifest)
|
||||||
|
return false;
|
||||||
|
if (config->delete_missing_args && !manifest_delete_missing_args(config, manifest))
|
||||||
|
return false;
|
||||||
|
if (config->use_delete && !manifest_delete_extras(config, manifest))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,17 +16,22 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
|||||||
prefixes the sender asks the receiver never to delete (paths excluded on the
|
prefixes the sender asks the receiver never to delete (paths excluded on the
|
||||||
source, protected at any depth). When --delete-excluded is given the sender
|
source, protected at any depth). When --delete-excluded is given the sender
|
||||||
transmits an empty protected list so excluded destination mirrors are treated
|
transmits an empty protected list so excluded destination mirrors are treated
|
||||||
as ordinary extras. */
|
as ordinary extras. With --delete-missing-args a third section (`missing`)
|
||||||
|
carries the destination mirrors of explicitly-listed source entries that do
|
||||||
|
not exist: each is an exact deletion request, independent of the ordinary
|
||||||
|
extras walk (never blocked by the protected prefixes) and processed when the
|
||||||
|
manifest is committed. */
|
||||||
typedef struct DeleteManifest {
|
typedef struct DeleteManifest {
|
||||||
ArrayList* keeps;
|
ArrayList* keeps;
|
||||||
ArrayList* protected;
|
ArrayList* protected;
|
||||||
|
ArrayList* missing;
|
||||||
} DeleteManifest;
|
} DeleteManifest;
|
||||||
|
|
||||||
void delete_manifest_free(DeleteManifest* manifest);
|
void delete_manifest_free(DeleteManifest* manifest);
|
||||||
/* Read a delete-manifest frame: keep count + keeps, then protected count +
|
/* Read a delete-manifest frame: keep count + keeps, then protected count +
|
||||||
protected prefixes (self-delimiting; the leading STATUS_MANIFEST code has been
|
protected prefixes, then missing count + missing paths (self-delimiting; the
|
||||||
consumed). Returns an owned DeleteManifest, or NULL after signalling
|
leading STATUS_MANIFEST code has been consumed). Returns an owned
|
||||||
STATUS_ERROR on a malformed frame. */
|
DeleteManifest, or NULL after signalling STATUS_ERROR on a malformed frame. */
|
||||||
DeleteManifest* receive_manifest_entries(int fd);
|
DeleteManifest* receive_manifest_entries(int fd);
|
||||||
/* Remove destination entries under config->receive_root_directory that are not
|
/* Remove destination entries under config->receive_root_directory that are not
|
||||||
in `manifest` (bounded, all-or-nothing walk; staging-dir, basis-dir and
|
in `manifest` (bounded, all-or-nothing walk; staging-dir, basis-dir and
|
||||||
@@ -34,6 +39,20 @@ DeleteManifest* receive_manifest_entries(int fd);
|
|||||||
caller decides WHEN to run it based on the negotiated delete timing. Returns
|
caller decides WHEN to run it based on the negotiated delete timing. Returns
|
||||||
false (and the transfer fails) when the deletion cannot be committed. */
|
false (and the transfer fails) when the deletion cannot be committed. */
|
||||||
bool manifest_delete_extras(const Config* config, DeleteManifest* manifest);
|
bool manifest_delete_extras(const Config* config, DeleteManifest* manifest);
|
||||||
|
/* --delete-missing-args exact-path deletions: remove each destination mirror
|
||||||
|
in `manifest->missing` (never blocked by the protected prefixes, staging dir
|
||||||
|
and basis dirs excluded). A regular file/symlink is unlinked; an empty
|
||||||
|
directory is removed; a NON-empty directory is removed recursively only when
|
||||||
|
--delete or --force is in effect, otherwise it is left with a warning (rsync
|
||||||
|
parity). A missing path is a no-op. Returns false only on a genuine
|
||||||
|
confinement or I/O error (the run then fails); tolerated per-path cases are
|
||||||
|
reported and skipped. */
|
||||||
|
bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest);
|
||||||
|
/* Run every deletion family the manifest carries: the --delete-missing-args
|
||||||
|
exact-path deletions first (user requests are not blocked by exclusion
|
||||||
|
protection), then the ordinary extras walk when --delete is active. Returns
|
||||||
|
true when nothing to do or everything committed. */
|
||||||
|
bool manifest_delete_all(const Config* config, DeleteManifest* manifest);
|
||||||
|
|
||||||
/* Outcome of a single file_save_to_disk operation. The receiver needs to
|
/* Outcome of a single file_save_to_disk operation. The receiver needs to
|
||||||
distinguish "written" from "skipped" so --remove-source-files can be told
|
distinguish "written" from "skipped" so --remove-source-files can be told
|
||||||
|
|||||||
+2
-2
@@ -208,8 +208,8 @@ static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) {
|
|||||||
therefore protects "a" and "a/b/c" but not "ab". Entries with top_level_only
|
therefore protects "a" and "a/b/c" but not "ab". Entries with top_level_only
|
||||||
set only protect DIRECT children of the receive root (at_root); nested
|
set only protect DIRECT children of the receive root (at_root); nested
|
||||||
directories that share such a name stay ordinary destination content. */
|
directories that share such a name stay ordinary destination content. */
|
||||||
static bool path_under_skip_prefix(const char* child_rel, bool at_root,
|
bool path_under_skip_prefix(const char* child_rel, bool at_root, const DeleteSkipEntry* skips,
|
||||||
const DeleteSkipEntry* skips, int skip_count) {
|
int skip_count) {
|
||||||
for (int i = 0; i < skip_count; i++) {
|
for (int i = 0; i < skip_count; i++) {
|
||||||
if (skips[i].top_level_only && !at_root)
|
if (skips[i].top_level_only && !at_root)
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ typedef struct {
|
|||||||
const char* prefix;
|
const char* prefix;
|
||||||
bool top_level_only;
|
bool top_level_only;
|
||||||
} DeleteSkipEntry;
|
} DeleteSkipEntry;
|
||||||
|
/* True when child_rel is, or lies below, one of the protected entries (a prefix
|
||||||
|
"a" protects "a" and "a/b/c" but not "ab"; top_level_only entries protect
|
||||||
|
only DIRECT children of the destination root, i.e. child_rel has no '/'). */
|
||||||
|
bool path_under_skip_prefix(const char* child_rel, bool at_root, const DeleteSkipEntry* skips,
|
||||||
|
int skip_count);
|
||||||
/* Remove files/dirs under dest_root that are not listed in manifest without
|
/* Remove files/dirs under dest_root that are not listed in manifest without
|
||||||
ever descending into a protected prefix (see DeleteSkipEntry). When
|
ever descending into a protected prefix (see DeleteSkipEntry). When
|
||||||
max_delete is not SIZE_MAX the run is all-or-nothing: extras are counted
|
max_delete is not SIZE_MAX the run is all-or-nothing: extras are counted
|
||||||
|
|||||||
Reference in New Issue
Block a user