Release v2.29.0 #312
@@ -99,17 +99,20 @@ set(SHARED_SRCS
|
|||||||
src/shared/daemon_limits.c
|
src/shared/daemon_limits.c
|
||||||
src/shared/data.c
|
src/shared/data.c
|
||||||
src/shared/delay_updates.c
|
src/shared/delay_updates.c
|
||||||
|
src/shared/delete_commit.c
|
||||||
src/shared/delete_plan.c
|
src/shared/delete_plan.c
|
||||||
src/shared/delta.c
|
src/shared/delta.c
|
||||||
src/shared/file.c
|
src/shared/file.c
|
||||||
src/shared/file_list.c
|
src/shared/file_list.c
|
||||||
src/shared/file_receive.c
|
src/shared/file_receive.c
|
||||||
|
src/shared/file_save.c
|
||||||
src/shared/file_send.c
|
src/shared/file_send.c
|
||||||
src/shared/file_store.c
|
src/shared/file_store.c
|
||||||
src/shared/filter.c
|
src/shared/filter.c
|
||||||
src/shared/format.c
|
src/shared/format.c
|
||||||
src/shared/hardlink.c
|
src/shared/hardlink.c
|
||||||
src/shared/identity.c
|
src/shared/identity.c
|
||||||
|
src/shared/incremental_check.c
|
||||||
src/shared/log.c
|
src/shared/log.c
|
||||||
src/shared/metadata.c
|
src/shared/metadata.c
|
||||||
src/shared/motd.c
|
src/shared/motd.c
|
||||||
|
|||||||
@@ -0,0 +1,635 @@
|
|||||||
|
#include <errno.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <libgen.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/sysmacros.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "array_list.h"
|
||||||
|
#include "charset.h"
|
||||||
|
#include "chmod.h"
|
||||||
|
#include "chunk.h"
|
||||||
|
#include "compression.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "data.h"
|
||||||
|
#include "delay_updates.h"
|
||||||
|
#include "delete_commit.h"
|
||||||
|
#include "delta.h"
|
||||||
|
#include "file.h"
|
||||||
|
#include "format.h"
|
||||||
|
#include "identity.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "metadata.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include "utils.h"
|
||||||
|
#include "xattr.h"
|
||||||
|
|
||||||
|
#define MAX_SERVER_DELETE_COUNT 100000U
|
||||||
|
/* Retained cost of one delete-manifest entry beyond its path bytes: the
|
||||||
|
ArrayList pointer slot plus an approximate malloc header/rounding for the
|
||||||
|
heap copy. Charged against MAX_MANIFEST_BYTES so a frame full of tiny paths
|
||||||
|
cannot retain far more than the byte budget (B5). */
|
||||||
|
#define MANIFEST_ENTRY_OVERHEAD (sizeof(char*) + 16)
|
||||||
|
|
||||||
|
/* Read a delete-manifest frame (the STATUS_MANIFEST leading code has already
|
||||||
|
been consumed): a keep-set entry count followed by that many
|
||||||
|
destination-relative paths, then a protected-prefix count followed by that
|
||||||
|
many destination-relative prefixes, then a missing-args count followed by that
|
||||||
|
many destination-relative delete paths, then (protocol 2.23.0) a
|
||||||
|
synchronized-directory count followed by that many destination-relative
|
||||||
|
directory paths (the receive root is the "." sentinel). The frame is
|
||||||
|
self-delimiting (the counts are authoritative), so the caller decides what to
|
||||||
|
do next and continues reading the following STATUS_* frame. Every section is
|
||||||
|
validated identically: an entry must be non-empty, relative and traversal-free
|
||||||
|
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,
|
||||||
|
size_t* manifest_entries) {
|
||||||
|
int count;
|
||||||
|
if (!receive_int(fd, &count)) {
|
||||||
|
send_status(fd, STATUS_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (count < 0 || count > MAX_MANIFEST_ENTRIES ||
|
||||||
|
(size_t)count > MAX_MANIFEST_ENTRIES - *manifest_entries) {
|
||||||
|
send_status(fd, STATUS_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
char* s = receive_wire_str(fd);
|
||||||
|
size_t entry_size = s ? strlen(s) + MANIFEST_ENTRY_OVERHEAD : 0;
|
||||||
|
if (!s || s[0] == '\0' || s[0] == '/' || has_path_traversal(s) ||
|
||||||
|
entry_size > MAX_MANIFEST_BYTES - *manifest_bytes ||
|
||||||
|
(*manifest_bytes += entry_size) > MAX_MANIFEST_BYTES || !array_list_add(list, s)) {
|
||||||
|
free(s);
|
||||||
|
send_status(fd, STATUS_ERROR);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*manifest_entries += (size_t)count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteManifest* receive_manifest_entries(int fd) {
|
||||||
|
DeleteManifest* manifest = calloc(1, sizeof(DeleteManifest));
|
||||||
|
if (!manifest) {
|
||||||
|
send_status(fd, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
manifest->keeps = array_list_create(free);
|
||||||
|
manifest->protected = array_list_create(free);
|
||||||
|
manifest->missing = array_list_create(free);
|
||||||
|
manifest->dirs = array_list_create(free);
|
||||||
|
if (!manifest->keeps || !manifest->protected || !manifest->missing || !manifest->dirs) {
|
||||||
|
delete_manifest_free(manifest);
|
||||||
|
send_status(fd, STATUS_ERROR);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
size_t manifest_bytes = 0;
|
||||||
|
size_t manifest_entries = 0;
|
||||||
|
if (!receive_manifest_section(fd, manifest->keeps, &manifest_bytes, &manifest_entries) ||
|
||||||
|
!receive_manifest_section(fd, manifest->protected, &manifest_bytes, &manifest_entries) ||
|
||||||
|
!receive_manifest_section(fd, manifest->missing, &manifest_bytes, &manifest_entries) ||
|
||||||
|
!receive_manifest_section(fd, manifest->dirs, &manifest_bytes, &manifest_entries)) {
|
||||||
|
delete_manifest_free(manifest);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return manifest;
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete_manifest_free(DeleteManifest* manifest) {
|
||||||
|
if (!manifest)
|
||||||
|
return;
|
||||||
|
array_list_delete(manifest->keeps);
|
||||||
|
array_list_delete(manifest->protected);
|
||||||
|
array_list_delete(manifest->missing);
|
||||||
|
array_list_delete(manifest->dirs);
|
||||||
|
free(manifest);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared --max-delete budget for one receiver-side deletion commit. Both the
|
||||||
|
--delete-missing-args exact-path removals and the ordinary extras walk draw
|
||||||
|
from the same tally, matching rsync (whose --max-delete counts every deleted
|
||||||
|
file or directory). `max_delete` is SIZE_MAX for an unlimited budget. */
|
||||||
|
typedef struct {
|
||||||
|
size_t max_delete;
|
||||||
|
size_t deleted;
|
||||||
|
size_t skipped;
|
||||||
|
bool limit_hit;
|
||||||
|
} DeleteBudgetState;
|
||||||
|
|
||||||
|
/* Build the delete-walk protection prefix for one basis directory. The walker
|
||||||
|
compares paths relative to the receive root, so a relative entry is already
|
||||||
|
in the right form; an absolute entry that lies below the root is converted to
|
||||||
|
its root-relative form, and one outside the root returns NULL (the walk
|
||||||
|
cannot reach it, and it is not protected data beneath the root). Exposed so
|
||||||
|
tests can exercise the root-of-"/" child mapping directly. */
|
||||||
|
char* file_receive_basis_delete_relative(const Config* config, const char* path) {
|
||||||
|
if (!path)
|
||||||
|
return NULL;
|
||||||
|
if (path[0] != '/')
|
||||||
|
return str_dup(path);
|
||||||
|
const char* root = config->receive_root_directory;
|
||||||
|
if (!root || root[0] != '/')
|
||||||
|
return NULL;
|
||||||
|
size_t root_len = strlen(root);
|
||||||
|
while (root_len > 1 && root[root_len - 1] == '/')
|
||||||
|
root_len--;
|
||||||
|
if (strncmp(path, root, root_len) != 0)
|
||||||
|
return NULL;
|
||||||
|
if (root_len == 1) {
|
||||||
|
/* `root` is "/" (the only single-character absolute root): every absolute
|
||||||
|
path is below it, and the child relative form is everything after the
|
||||||
|
leading '/'. */
|
||||||
|
if (path[1] == '\0')
|
||||||
|
return NULL; /* identical to the root, not a child */
|
||||||
|
return str_dup(path + 1);
|
||||||
|
}
|
||||||
|
if (path[root_len] != '/')
|
||||||
|
return NULL; /* identical or a sibling sharing a name prefix */
|
||||||
|
return str_dup(path + root_len + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove every destination entry under the receive root that is not in the
|
||||||
|
keep-set, bounded by the shared budget (a smaller client --max-delete=NUM
|
||||||
|
replaces the server hard bound; rsync deletes up to the bound and skips the
|
||||||
|
rest). With --delay-updates the not-yet-published staging directory is a
|
||||||
|
direct child of the receive root and must not be treated as a set of extras;
|
||||||
|
the manifest's protected prefixes (paths excluded on the source), the
|
||||||
|
size-pruned prefixes (--max-size/--min-size, always protected) and the
|
||||||
|
alternate basis directories are never destination content and are skipped at
|
||||||
|
any depth. Returns true unless a traversal/unlink error aborted the walk;
|
||||||
|
the budget's limit_hit/skipped fields report a cap-stopped run. */
|
||||||
|
static bool delete_extras_budgeted_observed(const Config* config, DeleteManifest* manifest,
|
||||||
|
DeleteBudgetState* budget, DeletePathObserver observer,
|
||||||
|
void* observer_context) {
|
||||||
|
if (!config || !manifest || !manifest->keeps)
|
||||||
|
return false;
|
||||||
|
fprintf(stderr, "Deleting files not in manifest...\n");
|
||||||
|
/* Protected entries:
|
||||||
|
- the --delay-updates staging name, protected only as a DIRECT child of the
|
||||||
|
receive root (a nested destination directory that happens to be named
|
||||||
|
.fastsync-stage is ordinary content);
|
||||||
|
- alternate basis directories (--compare-dest / --copy-dest / --link-dest)
|
||||||
|
at any depth: they are extra comparison snapshots the user pointed at,
|
||||||
|
not destination content, and deleting them would destroy the very files a
|
||||||
|
--link-dest run just linked into place;
|
||||||
|
- the sender-side protected prefixes (source paths excluded by filters and
|
||||||
|
paths pruned by --max-size/--min-size), at any depth, so their destination
|
||||||
|
mirror survives --delete unless --delete-excluded opts back into removing
|
||||||
|
the filter-excluded ones (size-pruned entries are always protected). */
|
||||||
|
int skip_count = (config->delay_updates ? 1 : 0) + config->basis_count +
|
||||||
|
(manifest->protected ? manifest->protected->size : 0);
|
||||||
|
DeleteSkipEntry* skips = NULL;
|
||||||
|
char** owned_prefixes = NULL;
|
||||||
|
int used = 0;
|
||||||
|
if (skip_count > 0) {
|
||||||
|
skips = calloc((size_t)skip_count, sizeof(DeleteSkipEntry));
|
||||||
|
owned_prefixes = calloc((size_t)config->basis_count, sizeof(char*));
|
||||||
|
if (!skips || (config->basis_count > 0 && !owned_prefixes)) {
|
||||||
|
free(skips);
|
||||||
|
free(owned_prefixes);
|
||||||
|
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++) {
|
||||||
|
/* An absolute basis outside the receive root is unreachable by this walk,
|
||||||
|
so it contributes no protection prefix (and no slot). */
|
||||||
|
char* prefix = file_receive_basis_delete_relative(config, config->basis_dirs[i].path);
|
||||||
|
if (!prefix)
|
||||||
|
continue;
|
||||||
|
owned_prefixes[i] = prefix;
|
||||||
|
skips[idx].prefix = prefix;
|
||||||
|
skips[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < manifest->protected->size; i++) {
|
||||||
|
skips[idx].prefix = (const char*)manifest->protected->items[i];
|
||||||
|
skips[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
used = idx;
|
||||||
|
}
|
||||||
|
/* Clamp rather than subtract: an accounting bug where deleted already exceeds
|
||||||
|
max_delete must never underflow into an effectively unlimited budget. */
|
||||||
|
size_t remaining;
|
||||||
|
if (budget->max_delete == SIZE_MAX)
|
||||||
|
remaining = SIZE_MAX;
|
||||||
|
else if (budget->deleted >= budget->max_delete)
|
||||||
|
remaining = 0;
|
||||||
|
else
|
||||||
|
remaining = budget->max_delete - budget->deleted;
|
||||||
|
size_t deleted = 0;
|
||||||
|
size_t skipped = 0;
|
||||||
|
DeleteWalkResult result = delete_extras_limited_observed(
|
||||||
|
config->receive_root_directory, manifest->keeps, manifest->dirs, remaining, skips, used,
|
||||||
|
config->protect_rules, &deleted, &skipped, observer, observer_context);
|
||||||
|
if (owned_prefixes) {
|
||||||
|
for (int i = 0; i < config->basis_count; i++)
|
||||||
|
free(owned_prefixes[i]);
|
||||||
|
}
|
||||||
|
free(owned_prefixes);
|
||||||
|
free(skips);
|
||||||
|
budget->deleted += deleted;
|
||||||
|
budget->skipped += skipped;
|
||||||
|
if (result == DELETE_WALK_LIMIT_REACHED) {
|
||||||
|
budget->limit_hit = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (result != DELETE_WALK_OK) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "deletion failed while removing extraneous files");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool delete_extras_budgeted(const Config* config, DeleteManifest* manifest,
|
||||||
|
DeleteBudgetState* budget) {
|
||||||
|
return delete_extras_budgeted_observed(config, manifest, budget, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prefixes every observed path with a fixed subtree root, so a nested walk
|
||||||
|
(a recursively removed missing-arg directory) reports receive-root-relative
|
||||||
|
names like the rest of the delete output. */
|
||||||
|
typedef struct {
|
||||||
|
DeletePathObserver inner;
|
||||||
|
void* inner_context;
|
||||||
|
const char* prefix;
|
||||||
|
} PrefixedDeleteObserver;
|
||||||
|
|
||||||
|
static void prefixed_delete_observer(void* context, const char* rel) {
|
||||||
|
PrefixedDeleteObserver* prefixed = context;
|
||||||
|
if (!prefixed->inner || !rel)
|
||||||
|
return;
|
||||||
|
char* joined = path_cat((char*)prefixed->prefix, rel);
|
||||||
|
if (joined) {
|
||||||
|
prefixed->inner(prefixed->inner_context, joined);
|
||||||
|
free(joined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --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. Each removal draws from the shared
|
||||||
|
--max-delete budget: once it is exhausted the remaining requests are skipped
|
||||||
|
and counted. Returns false only on a genuine error (a confinement failure on
|
||||||
|
a validated path or an I/O error), which fails the run. */
|
||||||
|
static bool delete_missing_args_budgeted_observed(const Config* config, DeleteManifest* manifest,
|
||||||
|
DeleteBudgetState* budget,
|
||||||
|
DeletePathObserver observer,
|
||||||
|
void* observer_context) {
|
||||||
|
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;
|
||||||
|
char** owned_prefixes = NULL;
|
||||||
|
int used = 0;
|
||||||
|
if (skip_count > 0) {
|
||||||
|
skips = calloc((size_t)skip_count, sizeof(DeleteSkipEntry));
|
||||||
|
owned_prefixes = calloc((size_t)config->basis_count, sizeof(char*));
|
||||||
|
if (!skips || (config->basis_count > 0 && !owned_prefixes)) {
|
||||||
|
free(skips);
|
||||||
|
free(owned_prefixes);
|
||||||
|
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++) {
|
||||||
|
char* prefix = file_receive_basis_delete_relative(config, config->basis_dirs[i].path);
|
||||||
|
if (!prefix)
|
||||||
|
continue;
|
||||||
|
owned_prefixes[i] = prefix;
|
||||||
|
skips[idx].prefix = prefix;
|
||||||
|
skips[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
used = 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, used)) {
|
||||||
|
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) {
|
||||||
|
/* 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);
|
||||||
|
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);
|
||||||
|
free(leaf);
|
||||||
|
free(full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* An entry that exists is one deletion: skip it (and count it) when the
|
||||||
|
shared --max-delete budget is already exhausted. */
|
||||||
|
if (budget->deleted >= budget->max_delete) {
|
||||||
|
budget->limit_hit = true;
|
||||||
|
budget->skipped++;
|
||||||
|
close(parent_fd);
|
||||||
|
free(leaf);
|
||||||
|
free(full);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool removed = false;
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
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) {
|
||||||
|
/* 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);
|
||||||
|
/* Never let an accounting slip (deleted > max_delete) underflow the
|
||||||
|
remaining budget into SIZE_MAX, which would grant unlimited
|
||||||
|
deletions. */
|
||||||
|
size_t remaining =
|
||||||
|
budget->deleted >= budget->max_delete ? 0 : budget->max_delete - budget->deleted;
|
||||||
|
size_t contents_deleted = 0;
|
||||||
|
size_t contents_skipped = 0;
|
||||||
|
PrefixedDeleteObserver nested = {observer, observer_context, rel};
|
||||||
|
DeleteWalkResult walk =
|
||||||
|
no_keeps ? delete_extras_limited_observed(full, no_keeps, NULL, remaining, NULL, 0,
|
||||||
|
NULL, &contents_deleted, &contents_skipped,
|
||||||
|
observer ? prefixed_delete_observer : NULL,
|
||||||
|
observer ? &nested : NULL)
|
||||||
|
: 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 if (budget->deleted >= budget->max_delete) {
|
||||||
|
budget->limit_hit = true;
|
||||||
|
budget->skipped++;
|
||||||
|
} else if (file_remove_tree_secure(full)) {
|
||||||
|
/* The shared `if (removed)` tail charges this directory exactly
|
||||||
|
once; counting it here too would consume two budget units. */
|
||||||
|
removed = true;
|
||||||
|
} 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 if (errno != ENOENT) {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (unlinkat(parent_fd, leaf, 0) == 0) {
|
||||||
|
removed = true;
|
||||||
|
} else if (errno != ENOENT) {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removed) {
|
||||||
|
budget->deleted++;
|
||||||
|
if (observer)
|
||||||
|
observer(observer_context, rel);
|
||||||
|
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);
|
||||||
|
free(full);
|
||||||
|
if (!ok)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (owned_prefixes) {
|
||||||
|
for (int i = 0; i < config->basis_count; i++)
|
||||||
|
free(owned_prefixes[i]);
|
||||||
|
}
|
||||||
|
free(owned_prefixes);
|
||||||
|
free(skips);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Public wrappers used outside the commit path (and by unit tests): no
|
||||||
|
--max-delete budget. */
|
||||||
|
bool manifest_would_delete_list(const Config* config, DeleteManifest* manifest, ArrayList* out,
|
||||||
|
size_t* count_out) {
|
||||||
|
if (count_out)
|
||||||
|
*count_out = 0;
|
||||||
|
if (!config || !manifest || !manifest->keeps || !out)
|
||||||
|
return false;
|
||||||
|
int skip_count = (config->delay_updates ? 1 : 0) + config->basis_count +
|
||||||
|
(manifest->protected ? manifest->protected->size : 0);
|
||||||
|
DeleteSkipEntry* skips = NULL;
|
||||||
|
char** owned_prefixes = NULL;
|
||||||
|
int used = 0;
|
||||||
|
if (skip_count > 0) {
|
||||||
|
skips = calloc((size_t)skip_count, sizeof(DeleteSkipEntry));
|
||||||
|
owned_prefixes = calloc((size_t)config->basis_count, sizeof(char*));
|
||||||
|
if (!skips || (config->basis_count > 0 && !owned_prefixes)) {
|
||||||
|
free(skips);
|
||||||
|
free(owned_prefixes);
|
||||||
|
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++) {
|
||||||
|
/* Normalize exactly like the real commit path: a relative entry is
|
||||||
|
already root-relative, an absolute one inside the receive root is
|
||||||
|
converted, and one outside contributes no protection prefix. */
|
||||||
|
char* prefix = file_receive_basis_delete_relative(config, config->basis_dirs[i].path);
|
||||||
|
if (!prefix)
|
||||||
|
continue;
|
||||||
|
owned_prefixes[i] = prefix;
|
||||||
|
skips[idx].prefix = prefix;
|
||||||
|
skips[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < manifest->protected->size; i++) {
|
||||||
|
skips[idx].prefix = (const char*)manifest->protected->items[i];
|
||||||
|
skips[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
used = idx;
|
||||||
|
}
|
||||||
|
bool ok = delete_extras_list(config->receive_root_directory, manifest->keeps, manifest->dirs,
|
||||||
|
skips, used, config->protect_rules, out, count_out);
|
||||||
|
if (owned_prefixes) {
|
||||||
|
for (int i = 0; i < config->basis_count; i++)
|
||||||
|
free(owned_prefixes[i]);
|
||||||
|
}
|
||||||
|
free(owned_prefixes);
|
||||||
|
free(skips);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool manifest_delete_extras(const Config* config, DeleteManifest* manifest) {
|
||||||
|
DeleteBudgetState budget = {
|
||||||
|
.max_delete = SIZE_MAX, .deleted = 0, .skipped = 0, .limit_hit = false};
|
||||||
|
return delete_extras_budgeted(config, manifest, &budget);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest) {
|
||||||
|
DeleteBudgetState budget = {
|
||||||
|
.max_delete = SIZE_MAX, .deleted = 0, .skipped = 0, .limit_hit = false};
|
||||||
|
return delete_missing_args_budgeted_observed(config, manifest, &budget, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool manifest_delete_missing_args_limited(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t max_delete, size_t* deleted, size_t* skipped,
|
||||||
|
bool* limit_hit) {
|
||||||
|
return manifest_delete_missing_args_limited_observed(config, manifest, max_delete, deleted,
|
||||||
|
skipped, limit_hit, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool manifest_delete_missing_args_limited_observed(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t max_delete, size_t* deleted,
|
||||||
|
size_t* skipped, bool* limit_hit,
|
||||||
|
DeletePathObserver observer,
|
||||||
|
void* observer_context) {
|
||||||
|
DeleteBudgetState budget = {
|
||||||
|
.max_delete = max_delete, .deleted = 0, .skipped = 0, .limit_hit = false};
|
||||||
|
bool ok =
|
||||||
|
delete_missing_args_budgeted_observed(config, manifest, &budget, observer, observer_context);
|
||||||
|
if (deleted)
|
||||||
|
*deleted = budget.deleted;
|
||||||
|
if (skipped)
|
||||||
|
*skipped = budget.skipped;
|
||||||
|
if (limit_hit)
|
||||||
|
*limit_hit = budget.limit_hit;
|
||||||
|
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.
|
||||||
|
Both draw from one --max-delete budget; the result reports a cap-stopped
|
||||||
|
(partial) commit distinctly so the client can exit 25 like rsync. */
|
||||||
|
DeleteCommitResult manifest_delete_all(const Config* config, DeleteManifest* manifest) {
|
||||||
|
return manifest_delete_all_counted(config, manifest, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteCommitResult manifest_delete_all_counted(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t* deleted) {
|
||||||
|
return manifest_delete_all_observed(config, manifest, deleted, NULL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteCommitResult manifest_delete_all_observed(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t* deleted, DeletePathObserver observer,
|
||||||
|
void* observer_context) {
|
||||||
|
if (deleted)
|
||||||
|
*deleted = 0;
|
||||||
|
if (!config || !manifest)
|
||||||
|
return DELETE_COMMIT_ERROR;
|
||||||
|
/* Central no-mutation guard: a dry-run never deletes. No manifest is sent on
|
||||||
|
the dry-run path, but a hostile/buggy peer could; treat it as a no-op so
|
||||||
|
the receiver can never remove anything. */
|
||||||
|
if (config->dry_run)
|
||||||
|
return DELETE_COMMIT_OK;
|
||||||
|
/* A client --max-delete=NUM smaller than the server's hard bound replaces it
|
||||||
|
for this run; both still bound the commit. */
|
||||||
|
bool user_limited =
|
||||||
|
config->max_delete >= 0 && (size_t)config->max_delete < MAX_SERVER_DELETE_COUNT;
|
||||||
|
DeleteBudgetState budget = {.max_delete = user_limited ? (size_t)config->max_delete
|
||||||
|
: MAX_SERVER_DELETE_COUNT,
|
||||||
|
.deleted = 0,
|
||||||
|
.skipped = 0,
|
||||||
|
.limit_hit = false};
|
||||||
|
if (config->delete_missing_args &&
|
||||||
|
!delete_missing_args_budgeted_observed(config, manifest, &budget, observer, observer_context))
|
||||||
|
return DELETE_COMMIT_ERROR;
|
||||||
|
if (config->use_delete &&
|
||||||
|
!delete_extras_budgeted_observed(config, manifest, &budget, observer, observer_context))
|
||||||
|
return DELETE_COMMIT_ERROR;
|
||||||
|
if (deleted)
|
||||||
|
*deleted = budget.deleted;
|
||||||
|
if (budget.limit_hit) {
|
||||||
|
if (user_limited) {
|
||||||
|
log_message(LOG_LEVEL_ERROR, "Deletions stopped due to --max-delete limit (%zu skipped)",
|
||||||
|
budget.skipped);
|
||||||
|
} else {
|
||||||
|
log_message(LOG_LEVEL_ERROR,
|
||||||
|
"Deletions stopped due to the server deletion limit of %u (%zu skipped)",
|
||||||
|
(unsigned)MAX_SERVER_DELETE_COUNT, budget.skipped);
|
||||||
|
}
|
||||||
|
return DELETE_COMMIT_LIMIT_REACHED;
|
||||||
|
}
|
||||||
|
return DELETE_COMMIT_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#ifndef DELETE_COMMIT_H
|
||||||
|
#define DELETE_COMMIT_H
|
||||||
|
|
||||||
|
#include "array_list.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include "utils.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* Delete-commit module: delete-manifest receive plus the budgeted extras and
|
||||||
|
* --delete-missing-args walkers. These declarations are re-exported by the
|
||||||
|
* file_receive.h facade. */
|
||||||
|
|
||||||
|
/* A received delete-manifest frame: the keep-set (`keeps`, destination-relative
|
||||||
|
paths the sender transferred/keeps) plus `protected`, destination-relative
|
||||||
|
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
|
||||||
|
transmits an empty protected list so excluded destination mirrors are treated
|
||||||
|
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 {
|
||||||
|
ArrayList* keeps;
|
||||||
|
ArrayList* protected;
|
||||||
|
ArrayList* missing;
|
||||||
|
/* Destination-relative paths of the directories the sender synchronized for
|
||||||
|
this run. The extras walker only removes entries directly inside one of
|
||||||
|
these (the receive root is the "." sentinel); `--files-from` runs therefore
|
||||||
|
leave untransmitted directories and the unlisted parts of listed ones
|
||||||
|
alone, matching rsync's "delete only in synchronized directories". */
|
||||||
|
ArrayList* dirs;
|
||||||
|
} DeleteManifest;
|
||||||
|
|
||||||
|
void delete_manifest_free(DeleteManifest* manifest);
|
||||||
|
/* Read a delete-manifest frame (protocol 2.23.0): keep count + keeps, then
|
||||||
|
protected count + protected prefixes, then missing count + missing paths,
|
||||||
|
then synchronized-directory count + directory paths (self-delimiting; the
|
||||||
|
leading STATUS_MANIFEST code has been consumed). Returns an owned
|
||||||
|
DeleteManifest, or NULL after signalling STATUS_ERROR on a malformed frame. */
|
||||||
|
DeleteManifest* receive_manifest_entries(int fd);
|
||||||
|
/* Remove destination entries under config->receive_root_directory that are not
|
||||||
|
in `manifest` (bounded, all-or-nothing walk; staging-dir, basis-dir and
|
||||||
|
protected-prefix skips). `--max-delete` and `--force` are honored here. The
|
||||||
|
caller decides WHEN to run it based on the negotiated delete timing. Returns
|
||||||
|
false (and the transfer fails) when the deletion cannot be committed. */
|
||||||
|
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);
|
||||||
|
/* Budgeted form of manifest_delete_missing_args for the per-directory delete
|
||||||
|
session: each removed mirror draws from `max_delete` (SIZE_MAX = unlimited)
|
||||||
|
and the tallies are accumulated into `*deleted`/`*skipped`. `*limit_hit` is set
|
||||||
|
when the budget stopped the pass with entries left over. Returns false only
|
||||||
|
on a genuine deletion error. */
|
||||||
|
bool manifest_delete_missing_args_limited(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t max_delete, size_t* deleted, size_t* skipped,
|
||||||
|
bool* limit_hit);
|
||||||
|
/* Observer-aware form of manifest_delete_missing_args_limited: `observer` (may
|
||||||
|
be NULL) is invoked for every destination-relative path truly removed. */
|
||||||
|
bool manifest_delete_missing_args_limited_observed(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t max_delete, size_t* deleted,
|
||||||
|
size_t* skipped, bool* limit_hit,
|
||||||
|
DeletePathObserver observer,
|
||||||
|
void* observer_context);
|
||||||
|
/* Outcome of committing a delete manifest. LIMIT_REACHED reports rsync's
|
||||||
|
partial --max-delete result: the budget allowed some deletions and the rest
|
||||||
|
were skipped (the run still stores all file data but the client exits 25). */
|
||||||
|
typedef enum {
|
||||||
|
DELETE_COMMIT_OK = 0,
|
||||||
|
DELETE_COMMIT_LIMIT_REACHED,
|
||||||
|
DELETE_COMMIT_ERROR
|
||||||
|
} DeleteCommitResult;
|
||||||
|
|
||||||
|
/* 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. Both
|
||||||
|
share one --max-delete budget. Returns DELETE_COMMIT_OK when nothing was to
|
||||||
|
do or everything committed, DELETE_COMMIT_LIMIT_REACHED when the budget
|
||||||
|
stopped part of the work, or DELETE_COMMIT_ERROR on a genuine failure. */
|
||||||
|
DeleteCommitResult manifest_delete_all(const Config* config, DeleteManifest* manifest);
|
||||||
|
/* Like manifest_delete_all, but reports how many destination entries the commit
|
||||||
|
removed (for the end-of-transfer wire stats). `deleted` may be NULL. */
|
||||||
|
DeleteCommitResult manifest_delete_all_counted(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t* deleted);
|
||||||
|
/* Observer-aware form of manifest_delete_all_counted: `observer` (may be NULL)
|
||||||
|
is invoked for every destination-relative path truly removed. */
|
||||||
|
DeleteCommitResult manifest_delete_all_observed(const Config* config, DeleteManifest* manifest,
|
||||||
|
size_t* deleted, DeletePathObserver observer,
|
||||||
|
void* observer_context);
|
||||||
|
|
||||||
|
/* -n/--dry-run --delete would-delete reporting: walk the destination exactly as
|
||||||
|
the delete pass would and append (strdup'd) destination-relative paths that
|
||||||
|
WOULD be removed to `out`, without touching disk. Uses the same staging-dir,
|
||||||
|
basis-dir and protected-prefix skips as the real commit. Returns true on a
|
||||||
|
clean walk; `*count_out` receives the number of paths appended. */
|
||||||
|
bool manifest_would_delete_list(const Config* config, DeleteManifest* manifest, ArrayList* out,
|
||||||
|
size_t* count_out);
|
||||||
|
/* Convert one basis-directory path to the receive-root-relative protection
|
||||||
|
prefix the delete walker uses (NULL when it lies outside the root). Exposed
|
||||||
|
for unit tests of the root-of-"/" and normalization edge cases. */
|
||||||
|
char* file_receive_basis_delete_relative(const Config* config, const char* path);
|
||||||
|
|
||||||
|
#endif
|
||||||
+2
-3285
File diff suppressed because it is too large
Load Diff
+9
-142
@@ -2,11 +2,19 @@
|
|||||||
#define FILE_RECEIVE_H
|
#define FILE_RECEIVE_H
|
||||||
|
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "delete_commit.h"
|
||||||
|
#include "file_save.h"
|
||||||
#include "file_types.h"
|
#include "file_types.h"
|
||||||
|
#include "incremental_check.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
/* Server-side file receive/save path. */
|
/* Server-side file receive/save path.
|
||||||
|
*
|
||||||
|
* This header is the public facade for the file_receive module family: the
|
||||||
|
* wire receive dispatch (this file) plus the save-to-disk (file_save.h), the
|
||||||
|
* incremental check (incremental_check.h) and the delete-commit
|
||||||
|
* (delete_commit.h) modules. */
|
||||||
|
|
||||||
/* Cumulative caps for the deferred directory-time accumulator. The sender may
|
/* Cumulative caps for the deferred directory-time accumulator. The sender may
|
||||||
* legitimately split a large tree across repeated STATUS_DIR_TIMES frames, so a
|
* legitimately split a large tree across repeated STATUS_DIR_TIMES frames, so a
|
||||||
@@ -23,25 +31,6 @@ File* file_receive_dir_time(int file_descriptor, const Config* config);
|
|||||||
File* file_receive_hardlink(int file_descriptor);
|
File* file_receive_hardlink(int file_descriptor);
|
||||||
File* file_receive_symlink(int file_descriptor, const Config* config);
|
File* file_receive_symlink(int file_descriptor, const Config* config);
|
||||||
File* file_receive_special(int file_descriptor);
|
File* file_receive_special(int file_descriptor);
|
||||||
bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode);
|
|
||||||
/* Testable basis quick-check / verification policy. file_basis_quick_match is
|
|
||||||
* rsync's metadata quick-check for a basis candidate (equal size is required
|
|
||||||
* separately by the caller; this adds the --size-only / mtime / --modify-window
|
|
||||||
* leg). file_basis_content_required reports whether a hit must ALSO be
|
|
||||||
* confirmed by a whole-file content digest (--verify-basis; false is the
|
|
||||||
* default rsync-parity behavior). */
|
|
||||||
bool file_basis_quick_match(const Config* config, const struct stat* st, time_t check_mtime,
|
|
||||||
long check_mtime_nsec);
|
|
||||||
bool file_basis_content_required(const Config* config);
|
|
||||||
|
|
||||||
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
|
||||||
/* Extended variant used by the receiver. `would_transfer` (may be NULL) is set
|
|
||||||
* true only on the server-contacting --dry-run path when the file is not up to
|
|
||||||
* date: the receiver has already sent STATUS_DRY_RUN_TRANSFER and returns NULL
|
|
||||||
* without storing anything. On that path `*skipped` is true for an up-to-date
|
|
||||||
* (STATUS_OK) file and both flags are false for a genuine error. */
|
|
||||||
File* receive_incremental_check_ex(int fd, const Config* config, bool* skipped,
|
|
||||||
bool* would_transfer);
|
|
||||||
|
|
||||||
/* P7 Wave D directory-time accumulator. The receiver collects the metadata of
|
/* P7 Wave D directory-time accumulator. The receiver collects the metadata of
|
||||||
* every directory it creates/receives (STATUS_MKDIR with metadata and/or the
|
* every directory it creates/receives (STATUS_MKDIR with metadata and/or the
|
||||||
@@ -85,126 +74,4 @@ bool dir_time_list_add(DirTimeList* list, const char* wire_path, const FileMetad
|
|||||||
void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory,
|
void dir_metadata_list_apply(const DirTimeList* list, const char* root_directory,
|
||||||
const Config* config);
|
const Config* config);
|
||||||
|
|
||||||
/* A received delete-manifest frame: the keep-set (`keeps`, destination-relative
|
|
||||||
paths the sender transferred/keeps) plus `protected`, destination-relative
|
|
||||||
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
|
|
||||||
transmits an empty protected list so excluded destination mirrors are treated
|
|
||||||
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 {
|
|
||||||
ArrayList* keeps;
|
|
||||||
ArrayList* protected;
|
|
||||||
ArrayList* missing;
|
|
||||||
/* Destination-relative paths of the directories the sender synchronized for
|
|
||||||
this run. The extras walker only removes entries directly inside one of
|
|
||||||
these (the receive root is the "." sentinel); `--files-from` runs therefore
|
|
||||||
leave untransmitted directories and the unlisted parts of listed ones
|
|
||||||
alone, matching rsync's "delete only in synchronized directories". */
|
|
||||||
ArrayList* dirs;
|
|
||||||
} DeleteManifest;
|
|
||||||
|
|
||||||
void delete_manifest_free(DeleteManifest* manifest);
|
|
||||||
/* Read a delete-manifest frame (protocol 2.23.0): keep count + keeps, then
|
|
||||||
protected count + protected prefixes, then missing count + missing paths,
|
|
||||||
then synchronized-directory count + directory paths (self-delimiting; the
|
|
||||||
leading STATUS_MANIFEST code has been consumed). Returns an owned
|
|
||||||
DeleteManifest, or NULL after signalling STATUS_ERROR on a malformed frame. */
|
|
||||||
DeleteManifest* receive_manifest_entries(int fd);
|
|
||||||
/* Remove destination entries under config->receive_root_directory that are not
|
|
||||||
in `manifest` (bounded, all-or-nothing walk; staging-dir, basis-dir and
|
|
||||||
protected-prefix skips). `--max-delete` and `--force` are honored here. The
|
|
||||||
caller decides WHEN to run it based on the negotiated delete timing. Returns
|
|
||||||
false (and the transfer fails) when the deletion cannot be committed. */
|
|
||||||
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);
|
|
||||||
/* Budgeted form of manifest_delete_missing_args for the per-directory delete
|
|
||||||
session: each removed mirror draws from `max_delete` (SIZE_MAX = unlimited)
|
|
||||||
and the tallies are accumulated into `*deleted`/`*skipped`. `*limit_hit` is set
|
|
||||||
when the budget stopped the pass with entries left over. Returns false only
|
|
||||||
on a genuine deletion error. */
|
|
||||||
bool manifest_delete_missing_args_limited(const Config* config, DeleteManifest* manifest,
|
|
||||||
size_t max_delete, size_t* deleted, size_t* skipped,
|
|
||||||
bool* limit_hit);
|
|
||||||
/* Observer-aware form of manifest_delete_missing_args_limited: `observer` (may
|
|
||||||
be NULL) is invoked for every destination-relative path truly removed. */
|
|
||||||
bool manifest_delete_missing_args_limited_observed(const Config* config, DeleteManifest* manifest,
|
|
||||||
size_t max_delete, size_t* deleted,
|
|
||||||
size_t* skipped, bool* limit_hit,
|
|
||||||
DeletePathObserver observer,
|
|
||||||
void* observer_context);
|
|
||||||
/* Outcome of committing a delete manifest. LIMIT_REACHED reports rsync's
|
|
||||||
partial --max-delete result: the budget allowed some deletions and the rest
|
|
||||||
were skipped (the run still stores all file data but the client exits 25). */
|
|
||||||
typedef enum {
|
|
||||||
DELETE_COMMIT_OK = 0,
|
|
||||||
DELETE_COMMIT_LIMIT_REACHED,
|
|
||||||
DELETE_COMMIT_ERROR
|
|
||||||
} DeleteCommitResult;
|
|
||||||
|
|
||||||
/* 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. Both
|
|
||||||
share one --max-delete budget. Returns DELETE_COMMIT_OK when nothing was to
|
|
||||||
do or everything committed, DELETE_COMMIT_LIMIT_REACHED when the budget
|
|
||||||
stopped part of the work, or DELETE_COMMIT_ERROR on a genuine failure. */
|
|
||||||
DeleteCommitResult manifest_delete_all(const Config* config, DeleteManifest* manifest);
|
|
||||||
/* Like manifest_delete_all, but reports how many destination entries the commit
|
|
||||||
removed (for the end-of-transfer wire stats). `deleted` may be NULL. */
|
|
||||||
DeleteCommitResult manifest_delete_all_counted(const Config* config, DeleteManifest* manifest,
|
|
||||||
size_t* deleted);
|
|
||||||
/* Observer-aware form of manifest_delete_all_counted: `observer` (may be NULL)
|
|
||||||
is invoked for every destination-relative path truly removed. */
|
|
||||||
DeleteCommitResult manifest_delete_all_observed(const Config* config, DeleteManifest* manifest,
|
|
||||||
size_t* deleted, DeletePathObserver observer,
|
|
||||||
void* observer_context);
|
|
||||||
|
|
||||||
/* -n/--dry-run --delete would-delete reporting: walk the destination exactly as
|
|
||||||
the delete pass would and append (strdup'd) destination-relative paths that
|
|
||||||
WOULD be removed to `out`, without touching disk. Uses the same staging-dir,
|
|
||||||
basis-dir and protected-prefix skips as the real commit. Returns true on a
|
|
||||||
clean walk; `*count_out` receives the number of paths appended. */
|
|
||||||
bool manifest_would_delete_list(const Config* config, DeleteManifest* manifest, ArrayList* out,
|
|
||||||
size_t* count_out);
|
|
||||||
/* Convert one basis-directory path to the receive-root-relative protection
|
|
||||||
prefix the delete walker uses (NULL when it lies outside the root). Exposed
|
|
||||||
for unit tests of the root-of-"/" and normalization edge cases. */
|
|
||||||
char* file_receive_basis_delete_relative(const Config* config, const char* path);
|
|
||||||
|
|
||||||
/* Outcome of a single file_save_to_disk operation. The receiver needs to
|
|
||||||
distinguish "written" from "skipped" so --remove-source-files can be told
|
|
||||||
which sources were actually stored. */
|
|
||||||
typedef enum { FILE_SAVE_ERROR = 0, FILE_SAVE_WRITTEN = 1, FILE_SAVE_SKIPPED = 2 } FileSaveResult;
|
|
||||||
|
|
||||||
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
|
||||||
const Config* config);
|
|
||||||
/* Protocol 2.28.0 variant: also reports through `created` (when non-NULL)
|
|
||||||
* whether the destination entry did not exist before this save, and through
|
|
||||||
* `created_dirs` how many parent directories the confined walk created, so the
|
|
||||||
* receiver can build rsync's `Number of created files` breakdown. The plain
|
|
||||||
* file_save_to_disk_full() is this with both out-params NULL. */
|
|
||||||
FileSaveResult file_save_to_disk_full_ex(const char* root_directory, const File* file,
|
|
||||||
const Config* config, bool* created,
|
|
||||||
unsigned* created_dirs);
|
|
||||||
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config);
|
|
||||||
|
|
||||||
/* Protocol 2.28.0 receiver counter accumulator: fold one successfully saved
|
|
||||||
* entry into `stats`, adding its receiver-observed literal bytes and, when
|
|
||||||
* `created`, the matching created-by-type counter (regular file / symlink /
|
|
||||||
* special) plus `created_dirs` implicitly-created parent directories.
|
|
||||||
* Non-first hardlink siblings contribute no literal bytes. */
|
|
||||||
void receiver_stats_note_saved(ReceiverStats* stats, const File* file, bool created,
|
|
||||||
unsigned created_dirs);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
|||||||
|
#ifndef FILE_SAVE_H
|
||||||
|
#define FILE_SAVE_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "file_types.h"
|
||||||
|
#include "format.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* Save-to-disk module: regular-file/symlink/hardlink/special install, xattr
|
||||||
|
* application, --fake-super and the --delay-updates staging path. These
|
||||||
|
* declarations are re-exported by the file_receive.h facade. */
|
||||||
|
|
||||||
|
/* Outcome of a single file_save_to_disk operation. The receiver needs to
|
||||||
|
distinguish "written" from "skipped" so --remove-source-files can be told
|
||||||
|
which sources were actually stored. */
|
||||||
|
typedef enum { FILE_SAVE_ERROR = 0, FILE_SAVE_WRITTEN = 1, FILE_SAVE_SKIPPED = 2 } FileSaveResult;
|
||||||
|
|
||||||
|
bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode);
|
||||||
|
|
||||||
|
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
||||||
|
const Config* config);
|
||||||
|
/* Protocol 2.28.0 variant: also reports through `created` (when non-NULL)
|
||||||
|
* whether the destination entry did not exist before this save, and through
|
||||||
|
* `created_dirs` how many parent directories the confined walk created, so the
|
||||||
|
* receiver can build rsync's `Number of created files` breakdown. The plain
|
||||||
|
* file_save_to_disk_full() is this with both out-params NULL. */
|
||||||
|
FileSaveResult file_save_to_disk_full_ex(const char* root_directory, const File* file,
|
||||||
|
const Config* config, bool* created,
|
||||||
|
unsigned* created_dirs);
|
||||||
|
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config);
|
||||||
|
|
||||||
|
/* Protocol 2.28.0 receiver counter accumulator: fold one successfully saved
|
||||||
|
* entry into `stats`, adding its receiver-observed literal bytes and, when
|
||||||
|
* `created`, the matching created-by-type counter (regular file / symlink /
|
||||||
|
* special) plus `created_dirs` implicitly-created parent directories.
|
||||||
|
* Non-first hardlink siblings contribute no literal bytes. */
|
||||||
|
void receiver_stats_note_saved(ReceiverStats* stats, const File* file, bool created,
|
||||||
|
unsigned created_dirs);
|
||||||
|
|
||||||
|
#endif
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
|||||||
|
#ifndef INCREMENTAL_CHECK_H
|
||||||
|
#define INCREMENTAL_CHECK_H
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "file_types.h"
|
||||||
|
#include "protocol.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
/* Incremental-check module: the per-file STATUS_CHECK state machine, the
|
||||||
|
* incremental delta / alternate-basis / fuzzy matching helpers and the shared
|
||||||
|
* xattr receive helper. These declarations are re-exported by the
|
||||||
|
* file_receive.h facade. */
|
||||||
|
|
||||||
|
/* Whole-file payload bound shared by the plain receive path and the
|
||||||
|
* incremental check paths. */
|
||||||
|
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_WHOLE_FILE_SIZE
|
||||||
|
|
||||||
|
/* Receive a file's xattr block (when the config enables xattr transport) and
|
||||||
|
* attach it to `file`. Returns false on a malformed/oversized frame. */
|
||||||
|
bool receive_file_xattrs(File* file, int fd, const Config* config);
|
||||||
|
|
||||||
|
File* receive_incremental_check(int fd, const Config* config, bool* skipped);
|
||||||
|
/* Extended variant used by the receiver. `would_transfer` (may be NULL) is set
|
||||||
|
* true only on the server-contacting --dry-run path when the file is not up to
|
||||||
|
* date: the receiver has already sent STATUS_DRY_RUN_TRANSFER and returns NULL
|
||||||
|
* without storing anything. On that path `*skipped` is true for an up-to-date
|
||||||
|
* (STATUS_OK) file and both flags are false for a genuine error. */
|
||||||
|
File* receive_incremental_check_ex(int fd, const Config* config, bool* skipped,
|
||||||
|
bool* would_transfer);
|
||||||
|
|
||||||
|
/* Testable basis quick-check / verification policy. file_basis_quick_match is
|
||||||
|
* rsync's metadata quick-check for a basis candidate (equal size is required
|
||||||
|
* separately by the caller; this adds the --size-only / mtime / --modify-window
|
||||||
|
* leg). file_basis_content_required reports whether a hit must ALSO be
|
||||||
|
* confirmed by a whole-file content digest (--verify-basis; false is the
|
||||||
|
* default rsync-parity behavior). */
|
||||||
|
bool file_basis_quick_match(const Config* config, const struct stat* st, time_t check_mtime,
|
||||||
|
long check_mtime_nsec);
|
||||||
|
bool file_basis_content_required(const Config* config);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user