Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1167e7970b | ||
|
|
e98729f00e | ||
|
|
c0020364b2 | ||
|
|
d7ac940a6b | ||
|
|
be20e836de | ||
|
|
b549887138 | ||
|
|
1ffd4744c6 | ||
|
|
494cef2a0b | ||
|
|
ed7527cc2c | ||
|
|
934defa965 | ||
|
|
ade9be8600 | ||
|
|
707bb659e8 |
@@ -99,6 +99,7 @@ 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.c
|
||||||
src/shared/delete_commit.c
|
src/shared/delete_commit.c
|
||||||
src/shared/delete_plan.c
|
src/shared/delete_plan.c
|
||||||
src/shared/delta.c
|
src/shared/delta.c
|
||||||
|
|||||||
+6
-1
@@ -1013,7 +1013,12 @@ integration tests unless it is explicitly listed as a limitation.
|
|||||||
|
|
||||||
- **`--temp-dir` is confined to the receive root on the receiver:** a relative
|
- **`--temp-dir` is confined to the receive root on the receiver:** a relative
|
||||||
dir resolves below it; an absolute path or one containing `..` is rejected.
|
dir resolves below it; an absolute path or one containing `..` is rejected.
|
||||||
An `EXDEV` install falls back to a non-atomic copy instead of aborting.
|
An `EXDEV` install falls back to a non-atomic copy instead of aborting. (The
|
||||||
|
confined receiver path cannot be mount-tested in the CI container — no
|
||||||
|
`CAP_SYS_ADMIN` and unprivileged user namespaces are disabled — so the
|
||||||
|
cross-filesystem fallback is exercised end-to-end through the unconfined local
|
||||||
|
`--read-batch` apply against a `/dev/shm` scratch dir, in
|
||||||
|
`tests/integration/test_temp_dir_exdev.py`.)
|
||||||
- **Deletion scoping:** the manifest carries the synchronized directories, so
|
- **Deletion scoping:** the manifest carries the synchronized directories, so
|
||||||
the extras walk only visits their subtrees; `--files-from` subsets no longer
|
the extras walk only visits their subtrees; `--files-from` subsets no longer
|
||||||
delete untransmitted paths outside the listed directories.
|
delete untransmitted paths outside the listed directories.
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ static char* change_render_name_uptodate(const ChangeEvent* event) {
|
|||||||
* resolves to xxh128, so an explicit selection and the default both render the
|
* resolves to xxh128, so an explicit selection and the default both render the
|
||||||
* selected algorithm's digest. */
|
* selected algorithm's digest. */
|
||||||
static ChecksumAlgo out_format_checksum_algo(const Config* config) {
|
static ChecksumAlgo out_format_checksum_algo(const Config* config) {
|
||||||
return (ChecksumAlgo)config->checksum_transfer_algo;
|
return (ChecksumAlgo)config->cli.checksum_transfer_algo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Render a digest as rsync's sum_as_hex: xxh128 prints the HIGH 64-bit half
|
/* Render a digest as rsync's sum_as_hex: xxh128 prints the HIGH 64-bit half
|
||||||
|
|||||||
+34
-31
@@ -170,7 +170,7 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt
|
|||||||
* name is a hard error with rsync's exit code 4, never a silent no-op. */
|
* name is a hard error with rsync's exit code 4, never a silent no-op. */
|
||||||
static int set_compression_choice(Config* config, const char* value) {
|
static int set_compression_choice(Config* config, const char* value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int algo;
|
int algo;
|
||||||
@@ -178,7 +178,7 @@ static int set_compression_choice(Config* config, const char* value) {
|
|||||||
algo = compression_choice_resolve();
|
algo = compression_choice_resolve();
|
||||||
if (algo < 0) {
|
if (algo < 0) {
|
||||||
log_message(LOG_LEVEL_ERROR, "RSYNC_COMPRESS_LIST names no supported compression algorithm");
|
log_message(LOG_LEVEL_ERROR, "RSYNC_COMPRESS_LIST names no supported compression algorithm");
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -189,7 +189,7 @@ static int set_compression_choice(Config* config, const char* value) {
|
|||||||
"--compress-choice '%s' is not a supported algorithm; FastSync supports zstd, "
|
"--compress-choice '%s' is not a supported algorithm; FastSync supports zstd, "
|
||||||
"lz4, zlib, zlibx, none or auto",
|
"lz4, zlib, zlibx, none or auto",
|
||||||
value);
|
value);
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
const char* canonical = compression_algo_name((CompressionAlgo)algo);
|
const char* canonical = compression_algo_name((CompressionAlgo)algo);
|
||||||
@@ -226,7 +226,7 @@ static int resolve_checksum_name(const char* name, size_t len, int* out) {
|
|||||||
* resolves to FastSync's negotiated default (xxh128). */
|
* resolves to FastSync's negotiated default (xxh128). */
|
||||||
static int set_checksum_choice(Config* config, const char* value) {
|
static int set_checksum_choice(Config* config, const char* value) {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
const char* comma = strchr(value, ',');
|
const char* comma = strchr(value, ',');
|
||||||
@@ -244,7 +244,7 @@ static int set_checksum_choice(Config* config, const char* value) {
|
|||||||
"--checksum-choice '%s' is invalid; FastSync supports xxh64 (or xxhash), xxh128, "
|
"--checksum-choice '%s' is invalid; FastSync supports xxh64 (or xxhash), xxh128, "
|
||||||
"xxh3, md5, md4, sha1, none or auto, optionally as 'transfer,pre-transfer'",
|
"xxh3, md5, md4, sha1, none or auto, optionally as 'transfer,pre-transfer'",
|
||||||
value);
|
value);
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
int negotiated = -1;
|
int negotiated = -1;
|
||||||
@@ -252,7 +252,7 @@ static int set_checksum_choice(Config* config, const char* value) {
|
|||||||
negotiated = checksum_choice_resolve();
|
negotiated = checksum_choice_resolve();
|
||||||
if (negotiated < 0) {
|
if (negotiated < 0) {
|
||||||
log_message(LOG_LEVEL_ERROR, "RSYNC_CHECKSUM_LIST names no supported checksum algorithm");
|
log_message(LOG_LEVEL_ERROR, "RSYNC_CHECKSUM_LIST names no supported checksum algorithm");
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -264,8 +264,8 @@ static int set_checksum_choice(Config* config, const char* value) {
|
|||||||
pre = negotiated;
|
pre = negotiated;
|
||||||
|
|
||||||
config->checksum_algo = pre;
|
config->checksum_algo = pre;
|
||||||
config->checksum_transfer_algo = transfer;
|
config->cli.checksum_transfer_algo = transfer;
|
||||||
config->checksum_choice_set = true;
|
config->cli.checksum_choice_set = true;
|
||||||
/* rsync: "none" for the transfer checksum forces --whole-file. */
|
/* rsync: "none" for the transfer checksum forces --whole-file. */
|
||||||
if (transfer == (int)CHECKSUM_ALGO_NONE)
|
if (transfer == (int)CHECKSUM_ALGO_NONE)
|
||||||
config->whole_file = true;
|
config->whole_file = true;
|
||||||
@@ -963,7 +963,10 @@ static const OptionEntry OPTION_TABLE[] = {
|
|||||||
* faithful no-op (accepted silently, never consumes an argument). */
|
* faithful no-op (accepted silently, never consumes an argument). */
|
||||||
{"--recursive", "-r", OPT_NOOP, 0},
|
{"--recursive", "-r", OPT_NOOP, 0},
|
||||||
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
|
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
|
||||||
{"--old-args", NULL, OPT_FLAG, offsetof(Config, old_args)},
|
/* rsync's --old-args: accepted for CLI compatibility as a documented no-op
|
||||||
|
* (the remote server path is always safely quoted; see usage.c). It is
|
||||||
|
* recognized but stores no Config field. */
|
||||||
|
{"--old-args", NULL, OPT_NOOP, 0},
|
||||||
{"--rsh", "-e", OPT_STRING, offsetof(Config, rsh_command)},
|
{"--rsh", "-e", OPT_STRING, offsetof(Config, rsh_command)},
|
||||||
{"--blocking-io", NULL, OPT_FLAG, offsetof(Config, blocking_io)},
|
{"--blocking-io", NULL, OPT_FLAG, offsetof(Config, blocking_io)},
|
||||||
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
|
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
|
||||||
@@ -1196,12 +1199,12 @@ static int apply_negation(Config* config, const char* arg) {
|
|||||||
config->preserve_times = false;
|
config->preserve_times = false;
|
||||||
config->preserve_owner = false;
|
config->preserve_owner = false;
|
||||||
config->preserve_group = false;
|
config->preserve_group = false;
|
||||||
config->metadata_explicitly_disabled = true;
|
config->cli.metadata_explicitly_disabled = true;
|
||||||
/* --no-preserve is an explicit opt-out of the whole bundle: record it so
|
/* --no-preserve is an explicit opt-out of the whole bundle: record it so
|
||||||
* the --incremental/--delta auto-preserve in cli_finalize_config does not
|
* the --incremental/--delta auto-preserve in cli_finalize_config does not
|
||||||
* silently re-enable perms/times. */
|
* silently re-enable perms/times. */
|
||||||
config->preserve_perms_explicit_off = true;
|
config->cli.preserve_perms_explicit_off = true;
|
||||||
config->preserve_times_explicit_off = true;
|
config->cli.preserve_times_explicit_off = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
*(bool*)((char*)config + entry->offset) = false;
|
*(bool*)((char*)config + entry->offset) = false;
|
||||||
@@ -1209,9 +1212,9 @@ static int apply_negation(Config* config, const char* arg) {
|
|||||||
* auto-preserve the OTHER attribute without undoing this one. A later
|
* auto-preserve the OTHER attribute without undoing this one. A later
|
||||||
* -p/-t sets the attribute directly; this flag only gates the implication. */
|
* -p/-t sets the attribute directly; this flag only gates the implication. */
|
||||||
if (entry->offset == offsetof(Config, preserve_perms))
|
if (entry->offset == offsetof(Config, preserve_perms))
|
||||||
config->preserve_perms_explicit_off = true;
|
config->cli.preserve_perms_explicit_off = true;
|
||||||
else if (entry->offset == offsetof(Config, preserve_times))
|
else if (entry->offset == offsetof(Config, preserve_times))
|
||||||
config->preserve_times_explicit_off = true;
|
config->cli.preserve_times_explicit_off = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1440,7 +1443,7 @@ static bool cli_handle_range_time_options(CliParseCtx* ctx) {
|
|||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
config->stop_at_set = true;
|
config->cli.stop_at_set = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (strcmp(arg, "--stop-at") == 0) {
|
if (strcmp(arg, "--stop-at") == 0) {
|
||||||
@@ -1455,7 +1458,7 @@ static bool cli_handle_range_time_options(CliParseCtx* ctx) {
|
|||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
config->stop_at_set = true;
|
config->cli.stop_at_set = true;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const char* threads_prefix = "--compress-threads=";
|
const char* threads_prefix = "--compress-threads=";
|
||||||
@@ -1526,7 +1529,7 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (entry->offset == offsetof(Config, compression_level))
|
if (entry->offset == offsetof(Config, compression_level))
|
||||||
config->compression_level_set = true;
|
config->cli.compression_level_set = true;
|
||||||
if (entry->offset == offsetof(Config, chmod_spec)) {
|
if (entry->offset == offsetof(Config, chmod_spec)) {
|
||||||
mode_t ignored;
|
mode_t ignored;
|
||||||
if (!chmod_apply(0, config->chmod_spec, &ignored)) {
|
if (!chmod_apply(0, config->chmod_spec, &ignored)) {
|
||||||
@@ -1539,7 +1542,7 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
|||||||
defaults to 127.0.0.1, so a value check cannot distinguish it). Used
|
defaults to 127.0.0.1, so a value check cannot distinguish it). Used
|
||||||
by --dry-run to route an explicit remote target to the server. */
|
by --dry-run to route an explicit remote target to the server. */
|
||||||
if (entry->offset == offsetof(Config, server_host))
|
if (entry->offset == offsetof(Config, server_host))
|
||||||
config->server_host_set = true;
|
config->cli.server_host_set = true;
|
||||||
}
|
}
|
||||||
} else if (apply_table_option(config, entry, NULL) != 0) {
|
} else if (apply_table_option(config, entry, NULL) != 0) {
|
||||||
ctx->exit_code = -1;
|
ctx->exit_code = -1;
|
||||||
@@ -1813,7 +1816,7 @@ static bool cli_handle_transfer_flags(CliParseCtx* ctx) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
config->compression_level = (int)level;
|
config->compression_level = (int)level;
|
||||||
config->compression_level_set = true;
|
config->cli.compression_level_set = true;
|
||||||
log_info_message(LOG_INFO_MISC, "Set Compression level to %ld", level);
|
log_info_message(LOG_INFO_MISC, "Set Compression level to %ld", level);
|
||||||
ctx->i++;
|
ctx->i++;
|
||||||
}
|
}
|
||||||
@@ -1877,7 +1880,7 @@ static int set_server_port_option(Config* config, const char* value, const char*
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
config->server_port = port;
|
config->server_port = port;
|
||||||
config->server_port_set = true;
|
config->cli.server_port_set = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2648,7 +2651,7 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
|||||||
int resolved = compression_choice_resolve();
|
int resolved = compression_choice_resolve();
|
||||||
if (resolved < 0) {
|
if (resolved < 0) {
|
||||||
log_message(LOG_LEVEL_ERROR, "RSYNC_COMPRESS_LIST names no supported compression algorithm");
|
log_message(LOG_LEVEL_ERROR, "RSYNC_COMPRESS_LIST names no supported compression algorithm");
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
config->compression_algo = resolved;
|
config->compression_algo = resolved;
|
||||||
@@ -2659,7 +2662,7 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
|||||||
* clamped to the codec's range, otherwise the codec's own default is used. */
|
* clamped to the codec's range, otherwise the codec's own default is used. */
|
||||||
if (config->use_compression) {
|
if (config->use_compression) {
|
||||||
CompressionAlgo algo = (CompressionAlgo)config->compression_algo;
|
CompressionAlgo algo = (CompressionAlgo)config->compression_algo;
|
||||||
config->compression_level = config->compression_level_set
|
config->compression_level = config->cli.compression_level_set
|
||||||
? compression_clamp_level(algo, config->compression_level)
|
? compression_clamp_level(algo, config->compression_level)
|
||||||
: compression_default_level(algo);
|
: compression_default_level(algo);
|
||||||
log_debug_message(LOG_DEBUG_UTIL, "Client compression: %s (level %d)",
|
log_debug_message(LOG_DEBUG_UTIL, "Client compression: %s (level %d)",
|
||||||
@@ -2668,22 +2671,22 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
|||||||
/* The negotiated checksum is always resolved (rsync negotiates one for the
|
/* The negotiated checksum is always resolved (rsync negotiates one for the
|
||||||
* delta strong sum even without --checksum): RSYNC_CHECKSUM_LIST first, then
|
* delta strong sum even without --checksum): RSYNC_CHECKSUM_LIST first, then
|
||||||
* the compiled-in order. An explicit --checksum-choice already set it. */
|
* the compiled-in order. An explicit --checksum-choice already set it. */
|
||||||
if (!config->checksum_choice_set) {
|
if (!config->cli.checksum_choice_set) {
|
||||||
int resolved = checksum_choice_resolve();
|
int resolved = checksum_choice_resolve();
|
||||||
if (resolved < 0) {
|
if (resolved < 0) {
|
||||||
log_message(LOG_LEVEL_ERROR, "RSYNC_CHECKSUM_LIST names no supported checksum algorithm");
|
log_message(LOG_LEVEL_ERROR, "RSYNC_CHECKSUM_LIST names no supported checksum algorithm");
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
config->checksum_algo = resolved;
|
config->checksum_algo = resolved;
|
||||||
config->checksum_transfer_algo = resolved;
|
config->cli.checksum_transfer_algo = resolved;
|
||||||
}
|
}
|
||||||
/* rsync parity: "none" as the pre-transfer checksum cannot be combined with
|
/* rsync parity: "none" as the pre-transfer checksum cannot be combined with
|
||||||
* --checksum (exit 4). The check runs here because --checksum may appear on
|
* --checksum (exit 4). The check runs here because --checksum may appear on
|
||||||
* either side of --checksum-choice. */
|
* either side of --checksum-choice. */
|
||||||
if (config->checksum && config->checksum_algo == (int)CHECKSUM_ALGO_NONE) {
|
if (config->checksum && config->checksum_algo == (int)CHECKSUM_ALGO_NONE) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Invalid checksum-choice for --checksum: none");
|
log_message(LOG_LEVEL_ERROR, "Invalid checksum-choice for --checksum: none");
|
||||||
config->cli_exit_code = 4;
|
config->cli.cli_exit_code = 4;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2762,11 +2765,11 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
|||||||
* explicitly negated them (--no-perms/--no-times/--no-preserve). This runs
|
* explicitly negated them (--no-perms/--no-times/--no-preserve). This runs
|
||||||
* BEFORE the derived use_metadata bit so the transport frame is still sent
|
* BEFORE the derived use_metadata bit so the transport frame is still sent
|
||||||
* for the incremental/delta handshake even when both attributes were negated
|
* for the incremental/delta handshake even when both attributes were negated
|
||||||
* via --no-preserve (metadata_explicitly_disabled handles that opt-out). */
|
* via --no-preserve (cli.metadata_explicitly_disabled handles that opt-out). */
|
||||||
if (preserve_implied && !config->metadata_explicitly_disabled) {
|
if (preserve_implied && !config->cli.metadata_explicitly_disabled) {
|
||||||
if (!config->preserve_perms_explicit_off)
|
if (!config->cli.preserve_perms_explicit_off)
|
||||||
config->preserve_perms = true;
|
config->preserve_perms = true;
|
||||||
if (!config->preserve_times_explicit_off)
|
if (!config->cli.preserve_times_explicit_off)
|
||||||
config->preserve_times = true;
|
config->preserve_times = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3153,7 +3156,7 @@ int main(int argc, char* argv[]) {
|
|||||||
int parse_ret = parse_args(config, argc, argv, positional_args, &positional_count);
|
int parse_ret = parse_args(config, argc, argv, positional_args, &positional_count);
|
||||||
if (parse_ret != 0) {
|
if (parse_ret != 0) {
|
||||||
if (parse_ret < 0)
|
if (parse_ret < 0)
|
||||||
exit_code = config->cli_exit_code ? config->cli_exit_code : 1;
|
exit_code = config->cli.cli_exit_code ? config->cli.cli_exit_code : 1;
|
||||||
goto cleanup;
|
goto cleanup;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ bool dry_run_targets_server(const Config* config) {
|
|||||||
return true;
|
return true;
|
||||||
if (config->module && config->module[0] != '\0')
|
if (config->module && config->module[0] != '\0')
|
||||||
return true;
|
return true;
|
||||||
if (config->server_host_set || config->server_port_set)
|
if (config->cli.server_host_set || config->cli.server_port_set)
|
||||||
return true;
|
return true;
|
||||||
if (config->use_tls)
|
if (config->use_tls)
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1578,7 +1578,7 @@ static bool send_files_run(Config* config, SendFilesState* state) {
|
|||||||
now_mono.tv_nsec = 0;
|
now_mono.tv_nsec = 0;
|
||||||
}
|
}
|
||||||
state->stop = stop_condition_make(config->stop_after_mins > 0, config->stop_after_mins,
|
state->stop = stop_condition_make(config->stop_after_mins > 0, config->stop_after_mins,
|
||||||
config->stop_at_set, config->stop_at, now_mono);
|
config->cli.stop_at_set, config->stop_at, now_mono);
|
||||||
state->prepared.options.stop_condition = &state->stop;
|
state->prepared.options.stop_condition = &state->stop;
|
||||||
/* The early-delete pre-scan above already ran; only the data pass should feed
|
/* The early-delete pre-scan above already ran; only the data pass should feed
|
||||||
the directory-time list (otherwise every directory would be captured
|
the directory-time list (otherwise every directory would be captured
|
||||||
@@ -1653,7 +1653,7 @@ static bool send_files_run(Config* config, SendFilesState* state) {
|
|||||||
/* Completion tail: send the late delete manifest and captured directory times,
|
/* Completion tail: send the late delete manifest and captured directory times,
|
||||||
* finalize the receiver handshake, remove transferred sources and report stats.
|
* finalize the receiver handshake, remove transferred sources and report stats.
|
||||||
* Returns the rsync-compatible exit code. */
|
* Returns the rsync-compatible exit code. */
|
||||||
static int send_files_finalize(Config* config, SendFilesState* state) {
|
static int send_files_finalize(const Config* config, SendFilesState* state) {
|
||||||
Client* client = state->client;
|
Client* client = state->client;
|
||||||
if (directory_scanner_failed(state->scanner))
|
if (directory_scanner_failed(state->scanner))
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1916,8 +1916,8 @@ int send_files_multithreaded(Config* config) {
|
|||||||
now_mono.tv_nsec = 0;
|
now_mono.tv_nsec = 0;
|
||||||
}
|
}
|
||||||
context->stop_condition =
|
context->stop_condition =
|
||||||
stop_condition_make(config->stop_after_mins > 0, config->stop_after_mins, config->stop_at_set,
|
stop_condition_make(config->stop_after_mins > 0, config->stop_after_mins,
|
||||||
config->stop_at, now_mono);
|
config->cli.stop_at_set, config->stop_at, now_mono);
|
||||||
bool collect_excluded = config->use_delete && !config->delete_excluded;
|
bool collect_excluded = config->use_delete && !config->delete_excluded;
|
||||||
unsigned long long pre_scan_non_dir = 0;
|
unsigned long long pre_scan_non_dir = 0;
|
||||||
if (config->use_delete) {
|
if (config->use_delete) {
|
||||||
|
|||||||
+258
-149
@@ -303,103 +303,121 @@ int receiver_process(Config* config, int file_descriptor, const ReceiverSink* si
|
|||||||
return receiver_process_pending(config, file_descriptor, sink, NULL, NULL);
|
return receiver_process_pending(config, file_descriptor, sink, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Runs the whole receive loop. The delete manifest may legitimately arrive
|
/* Per-connection state threaded through the status handlers below. The parked
|
||||||
either FIRST (--delete-before / --delete-during: the sender transmits the
|
keep-set / per-directory session live here so one teardown helper can release
|
||||||
validated keep-set before any file data) or LAST (--delete-after /
|
them on every exit path. */
|
||||||
--delete-commit / --delete-delay: the manifest closes the data stream). In
|
typedef struct {
|
||||||
the early modes the receiver deletes as soon as the manifest has been read
|
Config* config;
|
||||||
and acknowledges with STATUS_OK so the sender only starts streaming once the
|
int fd;
|
||||||
deletion has committed (or failed); in the late modes the manifest is held
|
const ReceiverSink* sink;
|
||||||
and the deletion is committed only after the terminal STATUS_FINISHED proves
|
DeleteManifest** pending_manifest;
|
||||||
the whole transfer succeeded. A plain --delete defaults to the per-directory
|
DeletePlanSession** pending_plans;
|
||||||
delete-during plan mode (no manifest at all). See
|
/* Parked keep-set for the late/commit timing. Every exit path frees it
|
||||||
receiver_process_pending() for how the -m receiver defers that commit until
|
|
||||||
its disk writer has drained. */
|
|
||||||
int receiver_process_pending(Config* config, int file_descriptor, const ReceiverSink* sink,
|
|
||||||
DeleteManifest** pending_manifest, DeletePlanSession** pending_plans) {
|
|
||||||
Status status;
|
|
||||||
if (!receive_status(file_descriptor, &status))
|
|
||||||
return -1;
|
|
||||||
/* Wall-clock (=CLOCK_MONOTONIC) anti-slowloris bookkeeping. session_start is
|
|
||||||
* fixed for the whole connection; last_progress is refreshed by every frame
|
|
||||||
* that is not a keepalive/abort. */
|
|
||||||
struct timespec session_start;
|
|
||||||
struct timespec last_progress;
|
|
||||||
clock_gettime(CLOCK_MONOTONIC, &session_start);
|
|
||||||
last_progress = session_start;
|
|
||||||
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
|
||||||
return -1;
|
|
||||||
bool early_delete = config_delete_timing_early(config);
|
|
||||||
bool per_dir_delete = config_delete_timing_per_dir(config);
|
|
||||||
/* Parked keep-set for the late/commit timing. Every exit path below frees it
|
|
||||||
exactly once; the only exception is the successful FINISHED handoff, which
|
exactly once; the only exception is the successful FINISHED handoff, which
|
||||||
transfers ownership to *pending_manifest (used by the -m receiver). */
|
transfers ownership to *pending_manifest (used by the -m receiver). */
|
||||||
DeleteManifest* deferred_manifest = NULL;
|
DeleteManifest* deferred_manifest;
|
||||||
/* Per-directory delete session for --delete-during/--delete-delay. During the
|
/* Per-directory delete session for --delete-during/--delete-delay. During the
|
||||||
loop it applies plans inline (during) or snapshots their extras (delay); on
|
loop it applies plans inline (during) or snapshots their extras (delay); on
|
||||||
a successful FINISHED it is either committed here or handed to
|
a successful FINISHED it is either committed here or handed to
|
||||||
*pending_plans so the -m caller commits after its disk writer drained. */
|
*pending_plans so the -m caller commits after its disk writer drained. */
|
||||||
DeletePlanSession* plan_session = NULL;
|
DeletePlanSession* plan_session;
|
||||||
bool delete_limit_noted = false;
|
bool early_delete;
|
||||||
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
|
bool per_dir_delete;
|
||||||
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH ||
|
bool delete_limit_noted;
|
||||||
status == STATUS_MKDIR || status == STATUS_MANIFEST || status == STATUS_HARDLINK ||
|
} ReceiverPendingState;
|
||||||
status == STATUS_SYMLINK || status == STATUS_SPECIAL || status == STATUS_DIR_TIMES ||
|
|
||||||
status == STATUS_DELETE_PLAN) {
|
/* Outcome of one frame handler. NEXT reads the following status frame; FAIL
|
||||||
if (status == STATUS_KEEPALIVE) {
|
tears the connection down without a peer STATUS_ERROR; ERROR tears it down
|
||||||
if (!send_status(file_descriptor, STATUS_KEEPALIVE))
|
and (when the sink owns error reporting) emits STATUS_ERROR. */
|
||||||
goto fail;
|
typedef enum {
|
||||||
goto next_status;
|
RECEIVER_STEP_NEXT,
|
||||||
|
RECEIVER_STEP_FAIL,
|
||||||
|
RECEIVER_STEP_ERROR,
|
||||||
|
} ReceiverStep;
|
||||||
|
|
||||||
|
static ReceiverStep receiver_handle_keepalive(ReceiverPendingState* state) {
|
||||||
|
if (!send_status(state->fd, STATUS_KEEPALIVE))
|
||||||
|
return RECEIVER_STEP_FAIL;
|
||||||
|
return RECEIVER_STEP_NEXT;
|
||||||
}
|
}
|
||||||
if (status == STATUS_ABORT) {
|
|
||||||
|
static ReceiverStep receiver_handle_abort(ReceiverPendingState* state) {
|
||||||
|
(void)state;
|
||||||
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
|
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
|
||||||
goto fail;
|
return RECEIVER_STEP_FAIL;
|
||||||
}
|
}
|
||||||
if (status == STATUS_CHECK) {
|
|
||||||
|
static ReceiverStep receiver_handle_check(ReceiverPendingState* state) {
|
||||||
bool skipped = false;
|
bool skipped = false;
|
||||||
bool would_transfer = false;
|
bool would_transfer = false;
|
||||||
File* file = receive_incremental_check_ex(file_descriptor, config, &skipped, &would_transfer);
|
File* file = receive_incremental_check_ex(state->fd, state->config, &skipped, &would_transfer);
|
||||||
if (config->dry_run) {
|
if (state->config->dry_run) {
|
||||||
/* Server-contacting --dry-run: the reply has already been sent
|
/* Server-contacting --dry-run: the reply has already been sent
|
||||||
(STATUS_OK = up to date, STATUS_DRY_RUN_TRANSFER = would transfer) and
|
(STATUS_OK = up to date, STATUS_DRY_RUN_TRANSFER = would transfer) and
|
||||||
nothing may be stored. Both flags false means a genuine protocol
|
nothing may be stored. Both flags false means a genuine protocol
|
||||||
error (STATUS_ERROR already sent or sent by receive_error below). */
|
error (STATUS_ERROR already sent or sent by receive_error below). */
|
||||||
if (!skipped && !would_transfer)
|
if (!skipped && !would_transfer)
|
||||||
goto receive_error;
|
return RECEIVER_STEP_ERROR;
|
||||||
} else if (!skipped && (!file || !sink->store_file(file, sink->context))) {
|
} else if (!skipped && (!file || !state->sink->store_file(file, state->sink->context))) {
|
||||||
goto receive_error;
|
return RECEIVER_STEP_ERROR;
|
||||||
}
|
}
|
||||||
} else if (status == STATUS_CHUNK) {
|
return RECEIVER_STEP_NEXT;
|
||||||
Chunk* chunk = receive_chunk_data(file_descriptor, config);
|
}
|
||||||
if (!chunk || !receiver_process_chunk(chunk, sink))
|
|
||||||
goto receive_error;
|
static ReceiverStep receiver_handle_chunk(ReceiverPendingState* state) {
|
||||||
} else if (status == STATUS_CHECK_BATCH) {
|
Chunk* chunk = receive_chunk_data(state->fd, state->config);
|
||||||
if (!receiver_process_batch(config, file_descriptor))
|
if (!chunk || !receiver_process_chunk(chunk, state->sink))
|
||||||
goto fail;
|
return RECEIVER_STEP_ERROR;
|
||||||
goto next_status;
|
return RECEIVER_STEP_NEXT;
|
||||||
} else if (status == STATUS_MKDIR) {
|
}
|
||||||
File* dir = file_receive_directory(file_descriptor, config);
|
|
||||||
if (!dir || !sink->store_file(dir, sink->context))
|
static ReceiverStep receiver_handle_check_batch(ReceiverPendingState* state) {
|
||||||
goto receive_error;
|
if (!receiver_process_batch(state->config, state->fd))
|
||||||
} else if (status == STATUS_DIR_TIMES) {
|
return RECEIVER_STEP_FAIL;
|
||||||
if (!receiver_process_dir_times(file_descriptor, config, sink))
|
return RECEIVER_STEP_NEXT;
|
||||||
goto receive_error;
|
}
|
||||||
} else if (status == STATUS_HARDLINK) {
|
|
||||||
File* file = file_receive_hardlink(file_descriptor);
|
static ReceiverStep receiver_handle_mkdir(ReceiverPendingState* state) {
|
||||||
if (!file || !sink->store_file(file, sink->context))
|
File* dir = file_receive_directory(state->fd, state->config);
|
||||||
goto receive_error;
|
if (!dir || !state->sink->store_file(dir, state->sink->context))
|
||||||
} else if (status == STATUS_SYMLINK) {
|
return RECEIVER_STEP_ERROR;
|
||||||
File* sym = file_receive_symlink(file_descriptor, config);
|
return RECEIVER_STEP_NEXT;
|
||||||
if (!sym || !sink->store_file(sym, sink->context))
|
}
|
||||||
goto receive_error;
|
|
||||||
} else if (status == STATUS_SPECIAL) {
|
static ReceiverStep receiver_handle_dir_times(const ReceiverPendingState* state) {
|
||||||
File* file = file_receive_special(file_descriptor);
|
if (!receiver_process_dir_times(state->fd, state->config, state->sink))
|
||||||
if (!file || !sink->store_file(file, sink->context))
|
return RECEIVER_STEP_ERROR;
|
||||||
goto receive_error;
|
return RECEIVER_STEP_NEXT;
|
||||||
} else if (status == STATUS_MANIFEST) {
|
}
|
||||||
DeleteManifest* manifest = receive_manifest_entries(file_descriptor);
|
|
||||||
|
static ReceiverStep receiver_handle_hardlink(ReceiverPendingState* state) {
|
||||||
|
File* file = file_receive_hardlink(state->fd);
|
||||||
|
if (!file || !state->sink->store_file(file, state->sink->context))
|
||||||
|
return RECEIVER_STEP_ERROR;
|
||||||
|
return RECEIVER_STEP_NEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ReceiverStep receiver_handle_symlink(ReceiverPendingState* state) {
|
||||||
|
File* sym = file_receive_symlink(state->fd, state->config);
|
||||||
|
if (!sym || !state->sink->store_file(sym, state->sink->context))
|
||||||
|
return RECEIVER_STEP_ERROR;
|
||||||
|
return RECEIVER_STEP_NEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ReceiverStep receiver_handle_special(ReceiverPendingState* state) {
|
||||||
|
File* file = file_receive_special(state->fd);
|
||||||
|
if (!file || !state->sink->store_file(file, state->sink->context))
|
||||||
|
return RECEIVER_STEP_ERROR;
|
||||||
|
return RECEIVER_STEP_NEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ReceiverStep receiver_handle_manifest(ReceiverPendingState* state) {
|
||||||
|
Config* config = state->config;
|
||||||
|
int fd = state->fd;
|
||||||
|
const ReceiverSink* sink = state->sink;
|
||||||
|
DeleteManifest* manifest = receive_manifest_entries(fd);
|
||||||
if (!manifest)
|
if (!manifest)
|
||||||
goto fail; /* receive_manifest_entries already sent STATUS_ERROR */
|
return RECEIVER_STEP_FAIL; /* receive_manifest_entries already sent STATUS_ERROR */
|
||||||
if (config->dry_run) {
|
if (config->dry_run) {
|
||||||
/* Server-contacting --dry-run mutates nothing, so a keep-set manifest
|
/* Server-contacting --dry-run mutates nothing, so a keep-set manifest
|
||||||
is consumed and discarded. The early-delete mode still needs its ACK
|
is consumed and discarded. The early-delete mode still needs its ACK
|
||||||
@@ -412,11 +430,11 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
log_message(LOG_LEVEL_WARNING, "dry-run: could not enumerate would-delete paths");
|
log_message(LOG_LEVEL_WARNING, "dry-run: could not enumerate would-delete paths");
|
||||||
}
|
}
|
||||||
delete_manifest_free(manifest);
|
delete_manifest_free(manifest);
|
||||||
if (early_delete && !send_status(file_descriptor, STATUS_OK))
|
if (state->early_delete && !send_status(fd, STATUS_OK))
|
||||||
goto fail;
|
return RECEIVER_STEP_FAIL;
|
||||||
goto next_status;
|
return RECEIVER_STEP_NEXT;
|
||||||
}
|
}
|
||||||
if (early_delete) {
|
if (state->early_delete) {
|
||||||
/* --delete-before: the whole-tree manifest is authoritative the moment
|
/* --delete-before: the whole-tree manifest is authoritative the moment
|
||||||
it arrives, before any file data. Delete now and acknowledge so the
|
it arrives, before any file data. Delete now and acknowledge so the
|
||||||
sender only starts streaming once the deletion committed (or failed).
|
sender only starts streaming once the deletion committed (or failed).
|
||||||
@@ -434,61 +452,164 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
receiver_tally_deleted(sink, deleted);
|
receiver_tally_deleted(sink, deleted);
|
||||||
delete_manifest_free(manifest);
|
delete_manifest_free(manifest);
|
||||||
if (deletion == DELETE_COMMIT_ERROR) {
|
if (deletion == DELETE_COMMIT_ERROR) {
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(fd, STATUS_ERROR);
|
||||||
goto fail;
|
return RECEIVER_STEP_FAIL;
|
||||||
}
|
}
|
||||||
if (deletion == DELETE_COMMIT_LIMIT_REACHED && sink->note_delete_limit)
|
if (deletion == DELETE_COMMIT_LIMIT_REACHED && sink->note_delete_limit)
|
||||||
sink->note_delete_limit(sink->context);
|
sink->note_delete_limit(sink->context);
|
||||||
if (!send_status(file_descriptor, STATUS_OK))
|
if (!send_status(fd, STATUS_OK))
|
||||||
goto fail;
|
return RECEIVER_STEP_FAIL;
|
||||||
} else if (config->use_delete || config->delete_missing_args) {
|
} else if (config->use_delete || config->delete_missing_args) {
|
||||||
/* Plain --delete / --delete-after and the --delete-missing-args
|
/* Plain --delete / --delete-after and the --delete-missing-args
|
||||||
exact-path deletions: hold the manifest and commit it only after
|
exact-path deletions: hold the manifest and commit it only after
|
||||||
STATUS_FINISHED. The per-directory modes never send this frame. */
|
STATUS_FINISHED. The per-directory modes never send this frame. */
|
||||||
if (deferred_manifest) {
|
if (state->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(state->deferred_manifest);
|
||||||
deferred_manifest = NULL;
|
state->deferred_manifest = NULL;
|
||||||
delete_manifest_free(manifest);
|
delete_manifest_free(manifest);
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(fd, STATUS_ERROR);
|
||||||
goto fail;
|
return RECEIVER_STEP_FAIL;
|
||||||
}
|
}
|
||||||
deferred_manifest = manifest;
|
state->deferred_manifest = manifest;
|
||||||
} else {
|
} else {
|
||||||
delete_manifest_free(manifest);
|
delete_manifest_free(manifest);
|
||||||
}
|
}
|
||||||
goto next_status;
|
return RECEIVER_STEP_NEXT;
|
||||||
} else if (status == STATUS_DELETE_PLAN) {
|
}
|
||||||
if (!per_dir_delete) {
|
|
||||||
|
static ReceiverStep receiver_handle_delete_plan(ReceiverPendingState* state) {
|
||||||
|
Config* config = state->config;
|
||||||
|
int fd = state->fd;
|
||||||
|
const ReceiverSink* sink = state->sink;
|
||||||
|
if (!state->per_dir_delete) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Received a per-directory delete plan without a per-dir "
|
log_message(LOG_LEVEL_ERROR, "Received a per-directory delete plan without a per-dir "
|
||||||
"delete timing");
|
"delete timing");
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(fd, STATUS_ERROR);
|
||||||
goto fail;
|
return RECEIVER_STEP_FAIL;
|
||||||
}
|
}
|
||||||
if (!plan_session) {
|
if (!state->plan_session) {
|
||||||
plan_session = delete_plan_session_create(config);
|
state->plan_session = delete_plan_session_create(config);
|
||||||
if (plan_session && config->report_deletes && sink->deleted_paths)
|
if (state->plan_session && config->report_deletes && sink->deleted_paths)
|
||||||
delete_plan_session_set_delete_observer(plan_session, receiver_record_deleted_path,
|
delete_plan_session_set_delete_observer(state->plan_session, receiver_record_deleted_path,
|
||||||
(void*)sink->deleted_paths);
|
(void*)sink->deleted_paths);
|
||||||
}
|
}
|
||||||
if (!plan_session || delete_plan_session_receive(plan_session, config, file_descriptor) != 0)
|
if (!state->plan_session || delete_plan_session_receive(state->plan_session, config, fd) != 0)
|
||||||
goto fail;
|
return RECEIVER_STEP_FAIL;
|
||||||
if (delete_plan_session_limit_reached(plan_session) && !delete_limit_noted &&
|
if (delete_plan_session_limit_reached(state->plan_session) && !state->delete_limit_noted &&
|
||||||
sink->note_delete_limit) {
|
sink->note_delete_limit) {
|
||||||
sink->note_delete_limit(sink->context);
|
sink->note_delete_limit(sink->context);
|
||||||
delete_limit_noted = true;
|
state->delete_limit_noted = true;
|
||||||
}
|
}
|
||||||
goto next_status;
|
return RECEIVER_STEP_NEXT;
|
||||||
} else {
|
}
|
||||||
File* file = file_receive(config, file_descriptor);
|
|
||||||
|
static ReceiverStep receiver_handle_file(ReceiverPendingState* state) {
|
||||||
|
File* file = file_receive(state->config, state->fd);
|
||||||
if (!file) {
|
if (!file) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
|
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
|
||||||
goto receive_error;
|
return RECEIVER_STEP_ERROR;
|
||||||
}
|
}
|
||||||
if (!sink->store_file(file, sink->context))
|
if (!state->sink->store_file(file, state->sink->context))
|
||||||
goto receive_error;
|
return RECEIVER_STEP_ERROR;
|
||||||
|
return RECEIVER_STEP_NEXT;
|
||||||
}
|
}
|
||||||
next_status:
|
|
||||||
|
/* One dispatch per admitted frame type; STATUS_NEXT (and any other
|
||||||
|
data-bearing status) falls through to the regular file receiver. */
|
||||||
|
static ReceiverStep receiver_dispatch_status(ReceiverPendingState* state, Status status) {
|
||||||
|
switch (status) {
|
||||||
|
case STATUS_KEEPALIVE:
|
||||||
|
return receiver_handle_keepalive(state);
|
||||||
|
case STATUS_ABORT:
|
||||||
|
return receiver_handle_abort(state);
|
||||||
|
case STATUS_CHECK:
|
||||||
|
return receiver_handle_check(state);
|
||||||
|
case STATUS_CHUNK:
|
||||||
|
return receiver_handle_chunk(state);
|
||||||
|
case STATUS_CHECK_BATCH:
|
||||||
|
return receiver_handle_check_batch(state);
|
||||||
|
case STATUS_MKDIR:
|
||||||
|
return receiver_handle_mkdir(state);
|
||||||
|
case STATUS_DIR_TIMES:
|
||||||
|
return receiver_handle_dir_times(state);
|
||||||
|
case STATUS_HARDLINK:
|
||||||
|
return receiver_handle_hardlink(state);
|
||||||
|
case STATUS_SYMLINK:
|
||||||
|
return receiver_handle_symlink(state);
|
||||||
|
case STATUS_SPECIAL:
|
||||||
|
return receiver_handle_special(state);
|
||||||
|
case STATUS_MANIFEST:
|
||||||
|
return receiver_handle_manifest(state);
|
||||||
|
case STATUS_DELETE_PLAN:
|
||||||
|
return receiver_handle_delete_plan(state);
|
||||||
|
default:
|
||||||
|
return receiver_handle_file(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Release the parked keep-set / per-directory session exactly once on every
|
||||||
|
failure exit. Never commit a deletion for a failed stream. */
|
||||||
|
static void receiver_drop_pending(ReceiverPendingState* state) {
|
||||||
|
if (state->deferred_manifest) {
|
||||||
|
delete_manifest_free(state->deferred_manifest);
|
||||||
|
state->deferred_manifest = NULL;
|
||||||
|
}
|
||||||
|
if (state->plan_session) {
|
||||||
|
delete_plan_session_destroy(state->plan_session);
|
||||||
|
state->plan_session = NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Runs the whole receive loop. The delete manifest may legitimately arrive
|
||||||
|
either FIRST (--delete-before / --delete-during: the sender transmits the
|
||||||
|
validated keep-set before any file data) or LAST (--delete-after /
|
||||||
|
--delete-commit / --delete-delay: the manifest closes the data stream). In
|
||||||
|
the early modes the receiver deletes as soon as the manifest has been read
|
||||||
|
and acknowledges with STATUS_OK so the sender only starts streaming once the
|
||||||
|
deletion has committed (or failed); in the late modes the manifest is held
|
||||||
|
and the deletion is committed only after the terminal STATUS_FINISHED proves
|
||||||
|
the whole transfer succeeded. A plain --delete defaults to the per-directory
|
||||||
|
delete-during plan mode (no manifest at all). See the per-frame handlers
|
||||||
|
above for how the -m receiver defers that commit until its disk writer has
|
||||||
|
drained. */
|
||||||
|
int receiver_process_pending(Config* config, int file_descriptor, const ReceiverSink* sink,
|
||||||
|
DeleteManifest** pending_manifest, DeletePlanSession** pending_plans) {
|
||||||
|
Status status;
|
||||||
|
if (!receive_status(file_descriptor, &status))
|
||||||
|
return -1;
|
||||||
|
/* Wall-clock (=CLOCK_MONOTONIC) anti-slowloris bookkeeping. session_start is
|
||||||
|
* fixed for the whole connection; last_progress is refreshed by every frame
|
||||||
|
* that is not a keepalive/abort. */
|
||||||
|
struct timespec session_start;
|
||||||
|
struct timespec last_progress;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &session_start);
|
||||||
|
last_progress = session_start;
|
||||||
|
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
||||||
|
return -1;
|
||||||
|
ReceiverPendingState state = {
|
||||||
|
.config = config,
|
||||||
|
.fd = file_descriptor,
|
||||||
|
.sink = sink,
|
||||||
|
.pending_manifest = pending_manifest,
|
||||||
|
.pending_plans = pending_plans,
|
||||||
|
.deferred_manifest = NULL,
|
||||||
|
.plan_session = NULL,
|
||||||
|
.early_delete = config_delete_timing_early(config),
|
||||||
|
.per_dir_delete = config_delete_timing_per_dir(config),
|
||||||
|
.delete_limit_noted = false,
|
||||||
|
};
|
||||||
|
bool notify_peer = false;
|
||||||
|
while (status == STATUS_NEXT || status == STATUS_CHUNK || status == STATUS_CHECK ||
|
||||||
|
status == STATUS_KEEPALIVE || status == STATUS_ABORT || status == STATUS_CHECK_BATCH ||
|
||||||
|
status == STATUS_MKDIR || status == STATUS_MANIFEST || status == STATUS_HARDLINK ||
|
||||||
|
status == STATUS_SYMLINK || status == STATUS_SPECIAL || status == STATUS_DIR_TIMES ||
|
||||||
|
status == STATUS_DELETE_PLAN) {
|
||||||
|
ReceiverStep step = receiver_dispatch_status(&state, status);
|
||||||
|
if (step == RECEIVER_STEP_FAIL)
|
||||||
|
goto fail;
|
||||||
|
if (step == RECEIVER_STEP_ERROR)
|
||||||
|
goto receive_error;
|
||||||
if (!receive_status(file_descriptor, &status))
|
if (!receive_status(file_descriptor, &status))
|
||||||
goto receive_error;
|
goto receive_error;
|
||||||
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
||||||
@@ -507,19 +628,19 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
disk writer may still be draining; the caller commits after the writer has
|
disk writer may still be draining; the caller commits after the writer has
|
||||||
joined so no extra file is removed unless the transfer is known to have
|
joined so no extra file is removed unless the transfer is known to have
|
||||||
succeeded. */
|
succeeded. */
|
||||||
if (deferred_manifest) {
|
if (state.deferred_manifest) {
|
||||||
if (pending_manifest) {
|
if (state.pending_manifest) {
|
||||||
*pending_manifest = deferred_manifest;
|
*state.pending_manifest = state.deferred_manifest;
|
||||||
deferred_manifest = NULL;
|
state.deferred_manifest = NULL;
|
||||||
} else {
|
} else {
|
||||||
size_t deleted = 0;
|
size_t deleted = 0;
|
||||||
DeletePathObserver observer =
|
DeletePathObserver observer =
|
||||||
(config->report_deletes && sink->deleted_paths) ? receiver_record_deleted_path : NULL;
|
(config->report_deletes && sink->deleted_paths) ? receiver_record_deleted_path : NULL;
|
||||||
DeleteCommitResult deletion = manifest_delete_all_observed(
|
DeleteCommitResult deletion = manifest_delete_all_observed(
|
||||||
config, deferred_manifest, &deleted, observer, (void*)sink->deleted_paths);
|
config, state.deferred_manifest, &deleted, observer, (void*)sink->deleted_paths);
|
||||||
receiver_tally_deleted(sink, deleted);
|
receiver_tally_deleted(sink, deleted);
|
||||||
delete_manifest_free(deferred_manifest);
|
delete_manifest_free(state.deferred_manifest);
|
||||||
deferred_manifest = NULL;
|
state.deferred_manifest = NULL;
|
||||||
if (deletion == DELETE_COMMIT_ERROR) {
|
if (deletion == DELETE_COMMIT_ERROR) {
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
goto fail;
|
goto fail;
|
||||||
@@ -533,28 +654,28 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
nothing yet and applies its decompressed snapshot here. The -m receiver
|
nothing yet and applies its decompressed snapshot here. The -m receiver
|
||||||
hands the session to its caller instead, which commits after the disk
|
hands the session to its caller instead, which commits after the disk
|
||||||
writer drained. */
|
writer drained. */
|
||||||
if (plan_session) {
|
if (state.plan_session) {
|
||||||
if (config->report_deletes && sink->deleted_paths)
|
if (config->report_deletes && sink->deleted_paths)
|
||||||
delete_plan_session_set_delete_observer(plan_session, receiver_record_deleted_path,
|
delete_plan_session_set_delete_observer(state.plan_session, receiver_record_deleted_path,
|
||||||
(void*)sink->deleted_paths);
|
(void*)sink->deleted_paths);
|
||||||
if (pending_plans) {
|
if (state.pending_plans) {
|
||||||
*pending_plans = plan_session;
|
*state.pending_plans = state.plan_session;
|
||||||
plan_session = NULL;
|
state.plan_session = NULL;
|
||||||
} else if (config->dry_run) {
|
} else if (config->dry_run) {
|
||||||
/* Central dry-run no-op: never commit a deletion for a -n run. */
|
/* Central dry-run no-op: never commit a deletion for a -n run. */
|
||||||
delete_plan_session_destroy(plan_session);
|
delete_plan_session_destroy(state.plan_session);
|
||||||
plan_session = NULL;
|
state.plan_session = NULL;
|
||||||
} else {
|
} else {
|
||||||
DeleteCommitResult deletion = delete_plan_session_commit(plan_session, config);
|
DeleteCommitResult deletion = delete_plan_session_commit(state.plan_session, config);
|
||||||
bool limit = delete_plan_session_limit_reached(plan_session);
|
bool limit = delete_plan_session_limit_reached(state.plan_session);
|
||||||
receiver_tally_deleted(sink, delete_plan_session_deleted(plan_session));
|
receiver_tally_deleted(sink, delete_plan_session_deleted(state.plan_session));
|
||||||
delete_plan_session_destroy(plan_session);
|
delete_plan_session_destroy(state.plan_session);
|
||||||
plan_session = NULL;
|
state.plan_session = NULL;
|
||||||
if (deletion == DELETE_COMMIT_ERROR) {
|
if (deletion == DELETE_COMMIT_ERROR) {
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
if (limit && !delete_limit_noted && sink->note_delete_limit)
|
if (limit && !state.delete_limit_noted && sink->note_delete_limit)
|
||||||
sink->note_delete_limit(sink->context);
|
sink->note_delete_limit(sink->context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -568,26 +689,14 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|
||||||
|
receive_error:
|
||||||
|
notify_peer = true;
|
||||||
fail:
|
fail:
|
||||||
/* Failure exits that must not (or already did) report a STATUS_ERROR. The
|
/* Failure exits that must not (or already did) report a STATUS_ERROR. The
|
||||||
parked keep-set/session is dropped: never commit a deletion for a failed
|
parked keep-set/session is dropped: never commit a deletion for a failed
|
||||||
stream. */
|
stream. */
|
||||||
if (deferred_manifest) {
|
receiver_drop_pending(&state);
|
||||||
delete_manifest_free(deferred_manifest);
|
if (notify_peer && sink->send_error)
|
||||||
deferred_manifest = NULL;
|
|
||||||
}
|
|
||||||
if (plan_session)
|
|
||||||
delete_plan_session_destroy(plan_session);
|
|
||||||
return -1;
|
|
||||||
|
|
||||||
receive_error:
|
|
||||||
if (deferred_manifest) {
|
|
||||||
delete_manifest_free(deferred_manifest);
|
|
||||||
deferred_manifest = NULL;
|
|
||||||
}
|
|
||||||
if (plan_session)
|
|
||||||
delete_plan_session_destroy(plan_session);
|
|
||||||
if (sink->send_error)
|
|
||||||
send_status(file_descriptor, STATUS_ERROR);
|
send_status(file_descriptor, STATUS_ERROR);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|||||||
+169
-86
@@ -705,69 +705,85 @@ static const char* server_module_gate(const Config* config, void* context) {
|
|||||||
return module_gate_install_root(config, module);
|
return module_gate_install_root(config, module);
|
||||||
}
|
}
|
||||||
|
|
||||||
void handler(int file_descriptor) {
|
/* Per-connection state threaded through the handler phase helpers below. The
|
||||||
SSL* ssl = io_get_ssl();
|
* fields are a faithful split of the former handler() locals: the protocol
|
||||||
|
* session, the config-frame gate context, the accepted config, the optional
|
||||||
|
* multithreaded pipeline context and the teardown bookkeeping all live here so
|
||||||
|
* the single `done` epilogue in handler() can release them exactly as before. */
|
||||||
|
typedef struct ServerSession {
|
||||||
|
int fd;
|
||||||
|
SSL* ssl;
|
||||||
ProtocolSession session;
|
ProtocolSession session;
|
||||||
protocol_session_init(&session, file_descriptor, file_descriptor);
|
|
||||||
protocol_session_set_ssl(&session, ssl);
|
|
||||||
protocol_session_bind(&session);
|
|
||||||
ModuleGateContext gate_ctx;
|
ModuleGateContext gate_ctx;
|
||||||
gate_ctx.ssl = ssl;
|
Config* config;
|
||||||
gate_ctx.fd = file_descriptor;
|
PipelineContextReceiver* context;
|
||||||
gate_ctx.super_mode_override = -1;
|
char* joined_destination;
|
||||||
gate_ctx.has_peer_ip = false;
|
bool charset_ready;
|
||||||
gate_ctx.peer_ip[0] = '\0';
|
} ServerSession;
|
||||||
gate_ctx.is_local = false;
|
|
||||||
/* All teardown state starts empty so the single `done` epilogue is safe to
|
/* Phase 1 -- config receipt + validation. Receives the client config frame
|
||||||
* reach from any error path (including before the config frame arrives). */
|
* through the module gate, applies the super-mode override the gate recorded
|
||||||
Config* config = NULL;
|
* exactly once, and installs the per-connection protocol/compression state.
|
||||||
PipelineContextReceiver* context = NULL;
|
* Returns false when the config frame was refused (the gate has already
|
||||||
char* joined_destination = NULL;
|
* answered the client); the caller jumps to the shared `done` epilogue. */
|
||||||
bool charset_ready = false;
|
static bool server_accept_config(ServerSession* state) {
|
||||||
config = config_receive_with_validate(file_descriptor, server_module_gate, &gate_ctx);
|
state->config = config_receive_with_validate(state->fd, server_module_gate, &state->gate_ctx);
|
||||||
if (config == NULL) {
|
if (state->config == NULL) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to receive config");
|
log_message(LOG_LEVEL_ERROR, "Failed to receive config");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
/* Apply the super-mode veto the gate decided on (operator --no-super, or a
|
/* Apply the super-mode veto the gate decided on (operator --no-super, or a
|
||||||
* daemon module without the `client owner = yes` opt-in) exactly once, so
|
* daemon module without the `client owner = yes` opt-in) exactly once, so
|
||||||
* every downstream gate (identity_apply_ownership via privilege_super_permitted,
|
* every downstream gate (identity_apply_ownership via privilege_super_permitted,
|
||||||
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
|
* device-node creation) sees SUPER_MODE_OFF. The gate never mutated the
|
||||||
* received config. */
|
* received config. */
|
||||||
if (gate_ctx.super_mode_override != -1)
|
if (state->gate_ctx.super_mode_override != -1)
|
||||||
config->super_mode = (SuperMode)gate_ctx.super_mode_override;
|
state->config->super_mode = (SuperMode)state->gate_ctx.super_mode_override;
|
||||||
/* Install the codec this connection negotiated before the receiver/writer
|
/* Install the codec this connection negotiated before the receiver/writer
|
||||||
* threads start (the server forks per connection, so the process-global
|
* threads start (the server forks per connection, so the process-global
|
||||||
* codec is private to this session). */
|
* codec is private to this session). */
|
||||||
compression_set_algo((CompressionAlgo)config->compression_algo);
|
compression_set_algo((CompressionAlgo)state->config->compression_algo);
|
||||||
/* If the client requested ownership but the effective super mode forbids it
|
/* If the client requested ownership but the effective super mode forbids it
|
||||||
* (operator --no-super, a privileged standalone receiver's secure default, or
|
* (operator --no-super, a privileged standalone receiver's secure default, or
|
||||||
* a daemon module without `client owner = yes`), say so ONCE per connection so
|
* a daemon module without `client owner = yes`), say so ONCE per connection so
|
||||||
* a successful -a/-o/-g transfer is not mistaken for preserved ownership. */
|
* a successful -a/-o/-g transfer is not mistaken for preserved ownership. */
|
||||||
if (config->super_mode == SUPER_MODE_OFF && identity_ownership_requested(config))
|
if (state->config->super_mode == SUPER_MODE_OFF && identity_ownership_requested(state->config))
|
||||||
log_message(LOG_LEVEL_WARNING,
|
log_message(LOG_LEVEL_WARNING,
|
||||||
"requested ownership will NOT be applied: super-user activities are disabled "
|
"requested ownership will NOT be applied: super-user activities are disabled "
|
||||||
"for this connection (operator veto, or module without `client owner = yes`)");
|
"for this connection (operator veto, or module without `client owner = yes`)");
|
||||||
protocol_set_8_bit_output(config->eight_bit_output);
|
protocol_set_8_bit_output(state->config->eight_bit_output);
|
||||||
/* Server-side per-message protocol deadline for every frame from here on.
|
/* Server-side per-message protocol deadline for every frame from here on.
|
||||||
* `timeout` is not serialized, so this is the server's own config (the server
|
* `timeout` is not serialized, so this is the server's own config (the server
|
||||||
* has no --timeout CLI and defaults it to 0). A client's --timeout tightens
|
* has no --timeout CLI and defaults it to 0). A client's --timeout tightens
|
||||||
* only that client's own protocol I/O; the server floors its own deadline at
|
* only that client's own protocol I/O; the server floors its own deadline at
|
||||||
* SERVER_IO_TIMEOUT_SEC so a silent peer can never hold a session slot
|
* SERVER_IO_TIMEOUT_SEC so a silent peer can never hold a session slot
|
||||||
* forever (the socket layer gets the same floor at startup). */
|
* forever (the socket layer gets the same floor at startup). */
|
||||||
protocol_session_set_io_timeout(&session, protocol_server_io_timeout_sec(config->timeout));
|
protocol_session_set_io_timeout(&state->session,
|
||||||
|
protocol_server_io_timeout_sec(state->config->timeout));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Phase 2 -- security gates. The ORDER here is load-bearing and must not be
|
||||||
|
* merged or reordered: transport/authentication (plaintext refusal, TLS
|
||||||
|
* client-CN verification), then daemon-root confinement (absolute-destination
|
||||||
|
* rejection, traversal + within-authorized-root), then delete/force
|
||||||
|
* authorization -- exactly the sequence the former handler() used. Returns
|
||||||
|
* false after logging the matching rejection; the caller jumps to the shared
|
||||||
|
* `done` epilogue. */
|
||||||
|
static bool server_apply_security_gates(ServerSession* state) {
|
||||||
|
Config* config = state->config;
|
||||||
const char* authorized_root = utils_get_authorized_root_path();
|
const char* authorized_root = utils_get_authorized_root_path();
|
||||||
if (!authorized_root) {
|
if (!authorized_root) {
|
||||||
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
log_message(LOG_LEVEL_ERROR, "No server-side destination root configured");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
if (!allow_unauthenticated && ssl == NULL) {
|
if (!allow_unauthenticated && state->ssl == NULL) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected unauthenticated plaintext connection");
|
log_message(LOG_LEVEL_ERROR, "Rejected unauthenticated plaintext connection");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
if (ssl && required_client_cn && !tls_client_identity_allowed(ssl)) {
|
if (state->ssl && required_client_cn && !tls_client_identity_allowed(state->ssl)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected TLS client with unauthorized identity");
|
log_message(LOG_LEVEL_ERROR, "Rejected TLS client with unauthorized identity");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
/* Daemon mode: the module's root is the authorized root (installed by
|
/* Daemon mode: the module's root is the authorized root (installed by
|
||||||
server_module_gate), and the client's destination is a MODULE-RELATIVE
|
server_module_gate), and the client's destination is a MODULE-RELATIVE
|
||||||
@@ -777,27 +793,27 @@ void handler(int file_descriptor) {
|
|||||||
if (g_daemon_conf && config->receive_root_directory && config->receive_root_directory[0] == '/') {
|
if (g_daemon_conf && config->receive_root_directory && config->receive_root_directory[0] == '/') {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected absolute daemon destination (must be relative to the "
|
log_message(LOG_LEVEL_ERROR, "Rejected absolute daemon destination (must be relative to the "
|
||||||
"selected module root)");
|
"selected module root)");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
char* destination = config->receive_root_directory;
|
char* destination = config->receive_root_directory;
|
||||||
if (destination && destination[0] != '/')
|
if (destination && destination[0] != '/')
|
||||||
joined_destination = path_cat(authorized_root, destination);
|
state->joined_destination = path_cat(authorized_root, destination);
|
||||||
if (joined_destination)
|
if (state->joined_destination)
|
||||||
destination = joined_destination;
|
destination = state->joined_destination;
|
||||||
if (!destination || has_path_traversal(destination) ||
|
if (!destination || has_path_traversal(destination) ||
|
||||||
!path_is_within_root(authorized_root, destination)) {
|
!path_is_within_root(authorized_root, destination)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root");
|
log_message(LOG_LEVEL_ERROR, "Rejected destination outside authorized root");
|
||||||
free(joined_destination);
|
free(state->joined_destination);
|
||||||
joined_destination = NULL;
|
state->joined_destination = NULL;
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
if (joined_destination) {
|
if (state->joined_destination) {
|
||||||
free(config->receive_root_directory);
|
free(config->receive_root_directory);
|
||||||
config->receive_root_directory = joined_destination;
|
config->receive_root_directory = state->joined_destination;
|
||||||
joined_destination = NULL;
|
state->joined_destination = NULL;
|
||||||
}
|
}
|
||||||
if (!config->receive_root_directory) {
|
if (!config->receive_root_directory) {
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
config->use_delete = config->use_delete && allow_delete;
|
config->use_delete = config->use_delete && allow_delete;
|
||||||
/* --force (receiver-side) is deletion authority too: it lets an incoming
|
/* --force (receiver-side) is deletion authority too: it lets an incoming
|
||||||
@@ -807,6 +823,18 @@ void handler(int file_descriptor) {
|
|||||||
* --delete-missing-args, so a client cannot use --force to bypass the delete
|
* --delete-missing-args, so a client cannot use --force to bypass the delete
|
||||||
* policy. */
|
* policy. */
|
||||||
config->force_delete = config->force_delete && allow_delete;
|
config->force_delete = config->force_delete && allow_delete;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Phase 3 -- session preparation. Installs the negotiated conversion, applies
|
||||||
|
* the remaining deletion policy, materializes the destination root (--mkpath),
|
||||||
|
* creates the --delay-updates staging tree, snapshots the identity policy, and
|
||||||
|
* publishes the --keep-dirlinks/--trust-sender globals and the daemon MOTD.
|
||||||
|
* All of it must happen before any receiver/writer thread is spawned. Returns
|
||||||
|
* false after logging the matching failure; the caller jumps to the shared
|
||||||
|
* `done` epilogue. */
|
||||||
|
static bool server_prepare_session(ServerSession* state) {
|
||||||
|
Config* config = state->config;
|
||||||
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
|
/* --iconv (protocol 2.16.0): install the receiver-side wire->local conversion
|
||||||
now that the client's full CONVERT_SPEC has been received and validated,
|
now that the client's full CONVERT_SPEC has been received and validated,
|
||||||
before any received file name is decoded. The server's own --iconv (if
|
before any received file name is decoded. The server's own --iconv (if
|
||||||
@@ -817,14 +845,14 @@ void handler(int file_descriptor) {
|
|||||||
if (!charset_wire_init_receiver(config->iconv_spec, server_iconv_spec)) {
|
if (!charset_wire_init_receiver(config->iconv_spec, server_iconv_spec)) {
|
||||||
log_message(LOG_LEVEL_ERROR,
|
log_message(LOG_LEVEL_ERROR,
|
||||||
"--iconv: unsupported charset conversion requested (LOCAL[,REMOTE])");
|
"--iconv: unsupported charset conversion requested (LOCAL[,REMOTE])");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
charset_ready = true;
|
state->charset_ready = true;
|
||||||
}
|
}
|
||||||
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
|
/* --delete-missing-args deletes destination mirrors receiver-side, so it is
|
||||||
deletion and stays gated by the same --allow-delete server policy. When
|
* 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
|
* 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). */
|
* skipped via its implied --ignore-missing-args, but nothing is deleted). */
|
||||||
config->delete_missing_args = config->delete_missing_args && allow_delete;
|
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. The precondition
|
* before anything else; without it the root must pre-exist. The precondition
|
||||||
@@ -839,7 +867,7 @@ void handler(int file_descriptor) {
|
|||||||
log_message(LOG_LEVEL_ERROR, "destination root is not available: %s",
|
log_message(LOG_LEVEL_ERROR, "destination root is not available: %s",
|
||||||
escaped_root ? escaped_root : "<allocation failed>");
|
escaped_root ? escaped_root : "<allocation failed>");
|
||||||
free(escaped_root);
|
free(escaped_root);
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
/* A --delay-updates transfer stages under a private 0700 directory inside
|
/* A --delay-updates transfer stages under a private 0700 directory inside
|
||||||
the receive root. Create it up front (wiping leftovers of any previously
|
the receive root. Create it up front (wiping leftovers of any previously
|
||||||
@@ -849,7 +877,7 @@ void handler(int file_descriptor) {
|
|||||||
config->delay_context = delay_updates_context_create(config->receive_root_directory);
|
config->delay_context = delay_updates_context_create(config->receive_root_directory);
|
||||||
if (!config->delay_context || !delay_updates_prepare(config->delay_context)) {
|
if (!config->delay_context || !delay_updates_prepare(config->delay_context)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to initialize --delay-updates staging area");
|
log_message(LOG_LEVEL_ERROR, "Failed to initialize --delay-updates staging area");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Preserve the negotiated identity policy for the fd-relative ownership
|
/* Preserve the negotiated identity policy for the fd-relative ownership
|
||||||
@@ -859,7 +887,7 @@ void handler(int file_descriptor) {
|
|||||||
rather than silently applying the wrong ownership policy. */
|
rather than silently applying the wrong ownership policy. */
|
||||||
if (!identity_set_active(config)) {
|
if (!identity_set_active(config)) {
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to activate identity policy");
|
log_message(LOG_LEVEL_ERROR, "Failed to activate identity policy");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
/* Persist the negotiated --keep-dirlinks policy once, here at config-accept,
|
/* Persist the negotiated --keep-dirlinks policy once, here at config-accept,
|
||||||
before any multithreaded receiver/writer threads are spawned, so the
|
before any multithreaded receiver/writer threads are spawned, so the
|
||||||
@@ -887,61 +915,72 @@ void handler(int file_descriptor) {
|
|||||||
Wave C note in config.h). */
|
Wave C note in config.h). */
|
||||||
if (g_daemon_conf) {
|
if (g_daemon_conf) {
|
||||||
char* motd = motd_read_file(g_daemon_conf->global.motd_file);
|
char* motd = motd_read_file(g_daemon_conf->global.motd_file);
|
||||||
if (!motd_send(file_descriptor, motd ? motd : "")) {
|
if (!motd_send(state->fd, motd ? motd : "")) {
|
||||||
free(motd);
|
free(motd);
|
||||||
log_message(LOG_LEVEL_ERROR, "Failed to send daemon MOTD");
|
log_message(LOG_LEVEL_ERROR, "Failed to send daemon MOTD");
|
||||||
goto done;
|
return false;
|
||||||
}
|
}
|
||||||
free(motd);
|
free(motd);
|
||||||
}
|
}
|
||||||
if (config->use_multithreading) {
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Phase 4a -- transfer via the multithreaded receiver. Spawns the receive/write
|
||||||
|
* thread pair, joins them, then commits the late deletion, --delay-updates
|
||||||
|
* publication and directory times before emitting the terminal stats/success
|
||||||
|
* frame. On any failure the helper just returns; the caller's `done` epilogue
|
||||||
|
* releases the pipeline context (which owns the config and queue) exactly as the
|
||||||
|
* former inline code did. */
|
||||||
|
static void server_run_mt_receiver(ServerSession* state) {
|
||||||
|
Config* config = state->config;
|
||||||
Queue* q = queue_create(100, file_destroy);
|
Queue* q = queue_create(100, file_destroy);
|
||||||
if (q == NULL)
|
if (q == NULL)
|
||||||
goto done;
|
return;
|
||||||
context = pipeline_context_receiver_create(config, q, file_descriptor, ssl);
|
state->context = pipeline_context_receiver_create(config, q, state->fd, state->ssl);
|
||||||
if (context == NULL) {
|
if (state->context == NULL) {
|
||||||
queue_destroy(q);
|
queue_destroy(q);
|
||||||
goto done;
|
return;
|
||||||
}
|
}
|
||||||
protocol_session_set_max_alloc(&context->session, config->max_alloc);
|
protocol_session_set_max_alloc(&state->context->session, config->max_alloc);
|
||||||
protocol_session_set_io_timeout(&context->session,
|
protocol_session_set_io_timeout(&state->context->session,
|
||||||
protocol_server_io_timeout_sec(config->timeout));
|
protocol_server_io_timeout_sec(config->timeout));
|
||||||
atomic_store(&context->session.total_allocated_bytes,
|
atomic_store(&state->context->session.total_allocated_bytes,
|
||||||
atomic_load(&session.total_allocated_bytes));
|
atomic_load(&state->session.total_allocated_bytes));
|
||||||
pipeline_context_receiver_set_queue_byte_limit(context, RECEIVER_QUEUE_MAX_BYTES);
|
pipeline_context_receiver_set_queue_byte_limit(state->context, RECEIVER_QUEUE_MAX_BYTES);
|
||||||
thrd_t receiver = {0};
|
thrd_t receiver = {0};
|
||||||
thrd_t writer = {0};
|
thrd_t writer = {0};
|
||||||
bool receiver_created = thrd_create(&receiver, receive_thread, context) == thrd_success;
|
bool receiver_created = thrd_create(&receiver, receive_thread, state->context) == thrd_success;
|
||||||
bool writer_created = false;
|
bool writer_created = false;
|
||||||
if (receiver_created)
|
if (receiver_created)
|
||||||
writer_created = thrd_create(&writer, write_thread, context) == thrd_success;
|
writer_created = thrd_create(&writer, write_thread, state->context) == thrd_success;
|
||||||
if (!receiver_created || !writer_created) {
|
if (!receiver_created || !writer_created) {
|
||||||
log_perror("Error creating Threads");
|
log_perror("Error creating Threads");
|
||||||
if (receiver_created) {
|
if (receiver_created) {
|
||||||
mtx_lock(&context->mutex);
|
mtx_lock(&state->context->mutex);
|
||||||
atomic_store(&context->cancelled, true);
|
atomic_store(&state->context->cancelled, true);
|
||||||
cnd_broadcast(&context->condition_not_full);
|
cnd_broadcast(&state->context->condition_not_full);
|
||||||
cnd_broadcast(&context->condition_not_empty);
|
cnd_broadcast(&state->context->condition_not_empty);
|
||||||
mtx_unlock(&context->mutex);
|
mtx_unlock(&state->context->mutex);
|
||||||
/* Unblock a worker parked in socket I/O without closing the fd (the
|
/* Unblock a worker parked in socket I/O without closing the fd (the
|
||||||
* child owns the single close). shutdown() only affects sockets; for
|
* child owns the single close). shutdown() only affects sockets; for
|
||||||
* the --stdio pipe the receiver's per-message poll timeout still
|
* the --stdio pipe the receiver's per-message poll timeout still
|
||||||
* bounds the join, so do nothing there rather than close a descriptor
|
* bounds the join, so do nothing there rather than close a descriptor
|
||||||
* another thread may still be using. */
|
* another thread may still be using. */
|
||||||
struct stat fd_stat;
|
struct stat fd_stat;
|
||||||
if (fstat(file_descriptor, &fd_stat) == 0 && S_ISSOCK(fd_stat.st_mode))
|
if (fstat(state->fd, &fd_stat) == 0 && S_ISSOCK(fd_stat.st_mode))
|
||||||
shutdown(file_descriptor, SHUT_RDWR);
|
shutdown(state->fd, SHUT_RDWR);
|
||||||
thrd_join(receiver, NULL);
|
thrd_join(receiver, NULL);
|
||||||
}
|
}
|
||||||
if (writer_created)
|
if (writer_created)
|
||||||
thrd_join(writer, NULL);
|
thrd_join(writer, NULL);
|
||||||
goto done;
|
return;
|
||||||
}
|
}
|
||||||
int receiver_result;
|
int receiver_result;
|
||||||
int writer_result;
|
int writer_result;
|
||||||
thrd_join(receiver, &receiver_result);
|
thrd_join(receiver, &receiver_result);
|
||||||
thrd_join(writer, &writer_result);
|
thrd_join(writer, &writer_result);
|
||||||
bool transfer_ok = receiver_result == thrd_success && writer_result == thrd_success;
|
bool transfer_ok = receiver_result == thrd_success && writer_result == thrd_success;
|
||||||
|
PipelineContextReceiver* context = state->context;
|
||||||
if (transfer_ok && !config->dry_run) {
|
if (transfer_ok && !config->dry_run) {
|
||||||
/* Commit-style (late) deletion: receive_thread handed the keep-set
|
/* Commit-style (late) deletion: receive_thread handed the keep-set
|
||||||
manifest here instead of deleting while write_thread might still be
|
manifest here instead of deleting while write_thread might still be
|
||||||
@@ -1008,20 +1047,64 @@ void handler(int file_descriptor) {
|
|||||||
Status final_status = context->delete_limit_reached ? STATUS_DELETE_LIMIT : STATUS_OK;
|
Status final_status = context->delete_limit_reached ? STATUS_DELETE_LIMIT : STATUS_OK;
|
||||||
/* Emit the optional wire-stats record first (protocol 2.25.0), then the
|
/* Emit the optional wire-stats record first (protocol 2.25.0), then the
|
||||||
success/outcome frame, exactly like the single-threaded receiver. */
|
success/outcome frame, exactly like the single-threaded receiver. */
|
||||||
if (!receiver_send_stats_frame(file_descriptor, config, &context->stats,
|
if (!receiver_send_stats_frame(state->fd, config, &context->stats, context->would_delete,
|
||||||
context->would_delete, context->deleted_paths) ||
|
context->deleted_paths) ||
|
||||||
!receiver_send_final_success(file_descriptor, config, &context->outcomes, final_status))
|
!receiver_send_final_success(state->fd, config, &context->outcomes, final_status))
|
||||||
transfer_ok = false;
|
transfer_ok = false;
|
||||||
} else {
|
} else {
|
||||||
send_error_detail(file_descriptor, "transfer failed on receiver");
|
send_error_detail(state->fd, "transfer failed on receiver");
|
||||||
}
|
}
|
||||||
if (!transfer_ok)
|
if (!transfer_ok)
|
||||||
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
||||||
} else {
|
}
|
||||||
if (receiver_receive_files(config, file_descriptor) != 0)
|
|
||||||
|
/* Phase 4b -- transfer via the single-threaded receiver. Failure is logged
|
||||||
|
* exactly as before; the caller's `done` epilogue then releases the config. */
|
||||||
|
static void server_run_st_receiver(ServerSession* state) {
|
||||||
|
if (receiver_receive_files(state->config, state->fd) != 0)
|
||||||
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
log_message(LOG_LEVEL_ERROR, "Transfer failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Phase 4 dispatch -- choose the receiver implementation the config asks for.
|
||||||
|
* Both helpers own their success/failure logging; the caller falls through to
|
||||||
|
* the shared `done` epilogue either way. */
|
||||||
|
static void server_run_transfer(ServerSession* state) {
|
||||||
|
if (state->config->use_multithreading)
|
||||||
|
server_run_mt_receiver(state);
|
||||||
|
else
|
||||||
|
server_run_st_receiver(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handler(int file_descriptor) {
|
||||||
|
/* Single per-connection state; every phase helper below advances it and
|
||||||
|
* returns false on a logged failure. All teardown state starts empty so the
|
||||||
|
* single `done` epilogue is safe to reach from any error path (including
|
||||||
|
* before the config frame arrives). */
|
||||||
|
ServerSession state;
|
||||||
|
state.fd = file_descriptor;
|
||||||
|
state.ssl = io_get_ssl();
|
||||||
|
protocol_session_init(&state.session, file_descriptor, file_descriptor);
|
||||||
|
protocol_session_set_ssl(&state.session, state.ssl);
|
||||||
|
protocol_session_bind(&state.session);
|
||||||
|
state.gate_ctx.ssl = state.ssl;
|
||||||
|
state.gate_ctx.fd = file_descriptor;
|
||||||
|
state.gate_ctx.super_mode_override = -1;
|
||||||
|
state.gate_ctx.has_peer_ip = false;
|
||||||
|
state.gate_ctx.peer_ip[0] = '\0';
|
||||||
|
state.gate_ctx.is_local = false;
|
||||||
|
state.config = NULL;
|
||||||
|
state.context = NULL;
|
||||||
|
state.joined_destination = NULL;
|
||||||
|
state.charset_ready = false;
|
||||||
|
|
||||||
|
if (!server_accept_config(&state))
|
||||||
|
goto done;
|
||||||
|
if (!server_apply_security_gates(&state))
|
||||||
|
goto done;
|
||||||
|
if (!server_prepare_session(&state))
|
||||||
|
goto done;
|
||||||
|
server_run_transfer(&state);
|
||||||
|
|
||||||
done:
|
done:
|
||||||
/* Single cleanup epilogue: every error path jumps here, so the iconv
|
/* Single cleanup epilogue: every error path jumps here, so the iconv
|
||||||
* receiver conversion is released, the identity snapshot cleared, the
|
* receiver conversion is released, the identity snapshot cleared, the
|
||||||
@@ -1029,22 +1112,22 @@ done:
|
|||||||
* connection fd is deliberately NOT closed here -- the child functions own
|
* connection fd is deliberately NOT closed here -- the child functions own
|
||||||
* its single close (plain_child_fn / tls_child_fn), and the --stdio call
|
* its single close (plain_child_fn / tls_child_fn), and the --stdio call
|
||||||
* site must leave stdin/stdout open. */
|
* site must leave stdin/stdout open. */
|
||||||
if (charset_ready)
|
if (state.charset_ready)
|
||||||
charset_wire_free();
|
charset_wire_free();
|
||||||
/* The delay-updates staging tree is released by config_delete (which the
|
/* The delay-updates staging tree is released by config_delete (which the
|
||||||
branch below always reaches), so it is cleaned exactly once. */
|
branch below always reaches), so it is cleaned exactly once. */
|
||||||
identity_clear_active();
|
identity_clear_active();
|
||||||
protocol_session_unbind();
|
protocol_session_unbind();
|
||||||
if (context != NULL) {
|
if (state.context != NULL) {
|
||||||
/* context owns both the config and the queue it was created with. */
|
/* context owns both the config and the queue it was created with. */
|
||||||
pipeline_context_receiver_destroy(context);
|
pipeline_context_receiver_destroy(state.context);
|
||||||
context = NULL;
|
state.context = NULL;
|
||||||
config = NULL;
|
state.config = NULL;
|
||||||
} else {
|
} else {
|
||||||
config_delete(config);
|
config_delete(state.config);
|
||||||
config = NULL;
|
state.config = NULL;
|
||||||
}
|
}
|
||||||
free(joined_destination);
|
free(state.joined_destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifndef FASTSYNC_SERVER_AS_LIB
|
#ifndef FASTSYNC_SERVER_AS_LIB
|
||||||
|
|||||||
+12
-12
@@ -20,9 +20,9 @@
|
|||||||
|
|
||||||
static void config_set_defaults(Config* config) {
|
static void config_set_defaults(Config* config) {
|
||||||
config->scanner_threads = 0;
|
config->scanner_threads = 0;
|
||||||
config->metadata_explicitly_disabled = false;
|
config->cli.preserve_perms_explicit_off = false;
|
||||||
config->preserve_perms_explicit_off = false;
|
config->cli.preserve_times_explicit_off = false;
|
||||||
config->preserve_times_explicit_off = false;
|
config->cli.metadata_explicitly_disabled = false;
|
||||||
config->show_progress = false;
|
config->show_progress = false;
|
||||||
config->compression_threads = 0;
|
config->compression_threads = 0;
|
||||||
config->ssh_port = 22;
|
config->ssh_port = 22;
|
||||||
@@ -44,8 +44,8 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->tls_ca = NULL;
|
config->tls_ca = NULL;
|
||||||
config->server_host = str_dup("127.0.0.1");
|
config->server_host = str_dup("127.0.0.1");
|
||||||
config->server_port = 8080;
|
config->server_port = 8080;
|
||||||
config->server_port_set = false;
|
config->cli.server_port_set = false;
|
||||||
config->server_host_set = false;
|
config->cli.server_host_set = false;
|
||||||
/* rsync defaults: --timeout=0 (I/O timeouts disabled) and --contimeout=60.
|
/* rsync defaults: --timeout=0 (I/O timeouts disabled) and --contimeout=60.
|
||||||
* A value of 0 disables the client's own deadline on both the socket layer
|
* A value of 0 disables the client's own deadline on both the socket layer
|
||||||
* (tcp_set_timeouts) and the protocol layer
|
* (tcp_set_timeouts) and the protocol layer
|
||||||
@@ -68,10 +68,10 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->human_readable = false;
|
config->human_readable = false;
|
||||||
config->ignore_errors = false;
|
config->ignore_errors = false;
|
||||||
config->ignore_missing_args = false;
|
config->ignore_missing_args = false;
|
||||||
config->checksum_transfer_algo = CHECKSUM_ALGO_DEFAULT;
|
config->cli.checksum_transfer_algo = CHECKSUM_ALGO_DEFAULT;
|
||||||
config->cli_exit_code = 0;
|
config->cli.cli_exit_code = 0;
|
||||||
config->compression_level_set = false;
|
config->cli.compression_level_set = false;
|
||||||
config->checksum_choice_set = false;
|
config->cli.checksum_choice_set = 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;
|
||||||
@@ -85,7 +85,6 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->rsh_command = NULL;
|
config->rsh_command = NULL;
|
||||||
config->blocking_io = false;
|
config->blocking_io = false;
|
||||||
config->outbuf = OUTBUF_BLOCK;
|
config->outbuf = OUTBUF_BLOCK;
|
||||||
config->old_args = false;
|
|
||||||
config->remote_options = NULL;
|
config->remote_options = NULL;
|
||||||
config->remote_option_count = 0;
|
config->remote_option_count = 0;
|
||||||
config->address = NULL;
|
config->address = NULL;
|
||||||
@@ -101,7 +100,7 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->trust_sender = false;
|
config->trust_sender = false;
|
||||||
config->stop_after_mins = 0;
|
config->stop_after_mins = 0;
|
||||||
config->stop_at = 0;
|
config->stop_at = 0;
|
||||||
config->stop_at_set = false;
|
config->cli.stop_at_set = false;
|
||||||
config->write_batch = NULL;
|
config->write_batch = NULL;
|
||||||
config->only_write_batch = NULL;
|
config->only_write_batch = NULL;
|
||||||
config->read_batch = NULL;
|
config->read_batch = NULL;
|
||||||
@@ -342,7 +341,8 @@ bool config_derived_use_metadata(const Config* config) {
|
|||||||
config->chown_uid_set || config->chown_gid_set || config->usermap_count > 0 ||
|
config->chown_uid_set || config->chown_gid_set || config->usermap_count > 0 ||
|
||||||
config->groupmap_count > 0 || config->update)
|
config->groupmap_count > 0 || config->update)
|
||||||
return true;
|
return true;
|
||||||
return (config->use_incremental || config->use_delta) && !config->metadata_explicitly_disabled;
|
return (config->use_incremental || config->use_delta) &&
|
||||||
|
!config->cli.metadata_explicitly_disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool config_has_basis(const Config* config) {
|
bool config_has_basis(const Config* config) {
|
||||||
|
|||||||
+48
-40
@@ -342,22 +342,60 @@ typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF
|
|||||||
CONFIG_WIRE_CODEC_FIELDS(X) \
|
CONFIG_WIRE_CODEC_FIELDS(X) \
|
||||||
CONFIG_WIRE_PROTECT_FIELDS(X)
|
CONFIG_WIRE_PROTECT_FIELDS(X)
|
||||||
|
|
||||||
|
/* Client-only, CLI-parse bookkeeping (never serialized). These members exist
|
||||||
|
* only so the client command-line parser can record HOW an option was
|
||||||
|
* specified (explicitly set, explicitly negated, or a parser-requested exit
|
||||||
|
* code); no other module and no wire peer ever needs them. Grouping them in
|
||||||
|
* one nested member keeps the public Config free of client-CLI-only state. */
|
||||||
|
typedef struct {
|
||||||
|
/* Set when the user explicitly turned an attribute off with --no-perms /
|
||||||
|
* --no-times (long or short form). --incremental/--delta historically
|
||||||
|
* auto-enabled mode and mtime preservation; these flags let
|
||||||
|
* cli_finalize_config restore that behavior while still honoring the
|
||||||
|
* explicit per-attribute negation. A later -p/-t re-enables the attribute
|
||||||
|
* directly, so the flag only prevents the incremental/delta implication,
|
||||||
|
* never a POSITIVE request. */
|
||||||
|
bool preserve_perms_explicit_off;
|
||||||
|
bool preserve_times_explicit_off;
|
||||||
|
/* Set by --no-preserve, the explicit opt-out of the whole preservation
|
||||||
|
* bundle, so the --incremental/--delta auto-preserve implication stays off. */
|
||||||
|
bool metadata_explicitly_disabled;
|
||||||
|
/* True when --server-port/--port was explicitly given. --dry-run uses it to
|
||||||
|
* decide whether a real server handshake was requested, so a plain local
|
||||||
|
* destination (no explicit port) keeps the existing client-side dry-run
|
||||||
|
* behavior instead of dialing the default 127.0.0.1:8080. */
|
||||||
|
bool server_port_set;
|
||||||
|
/* True when --server-host was explicitly given, and distinct from the
|
||||||
|
* "127.0.0.1" default: --dry-run uses it to route an explicit remote target
|
||||||
|
* to the server so it reports receiver state exactly like a real run,
|
||||||
|
* instead of silently running the client-side manifest. */
|
||||||
|
bool server_host_set;
|
||||||
|
/* Codec-negotiation CLI state. The effective pre-transfer checksum is
|
||||||
|
* Config->checksum_algo (serialized); checksum_transfer_algo is the rsync
|
||||||
|
* "transfer" half of a two-name --checksum-choice form (validated and used
|
||||||
|
* only to mirror rsync's whole-file forcing, since FastSync's per-block
|
||||||
|
* strong hash is fixed). cli_exit_code carries a parser-requested process
|
||||||
|
* exit status (rsync uses 4 for an unsupported checksum/compress algorithm)
|
||||||
|
* so main() can mirror it. */
|
||||||
|
int checksum_transfer_algo;
|
||||||
|
int cli_exit_code;
|
||||||
|
/* "The user explicitly chose" bits. They let the per-codec default level /
|
||||||
|
* checksum list be applied only when the corresponding rsync option was
|
||||||
|
* omitted (an explicit --compress-level / --checksum-choice always wins). */
|
||||||
|
bool compression_level_set;
|
||||||
|
bool checksum_choice_set;
|
||||||
|
/* True when --stop-at was given. */
|
||||||
|
bool stop_at_set;
|
||||||
|
} ConfigCliParse;
|
||||||
|
|
||||||
typedef struct Config {
|
typedef struct Config {
|
||||||
/* -j/--threads=N: number of parallel scanner worker threads for the -m
|
/* -j/--threads=N: number of parallel scanner worker threads for the -m
|
||||||
* pipeline. 0 (the default, also set by bare -j/--threads) means "use the
|
* pipeline. 0 (the default, also set by bare -j/--threads) means "use the
|
||||||
* scanner's built-in default" (4). CLIENT-ONLY: it is a local scheduling
|
* scanner's built-in default" (4). CLIENT-ONLY: it is a local scheduling
|
||||||
* concern and is NEVER serialized into the wire config frame. */
|
* concern and is NEVER serialized into the wire config frame. */
|
||||||
int scanner_threads;
|
int scanner_threads;
|
||||||
bool metadata_explicitly_disabled;
|
/* Client-only CLI-parse bookkeeping (never serialized). See ConfigCliParse. */
|
||||||
/* CLIENT-ONLY (never serialized; not in CONFIG_WIRE_FIELDS). Set when the
|
ConfigCliParse cli;
|
||||||
* user explicitly turned an attribute off with --no-perms / --no-times (long
|
|
||||||
* or short form). --incremental/--delta historically auto-enabled mode and
|
|
||||||
* mtime preservation; these flags let cli_finalize_config restore that
|
|
||||||
* behavior while still honoring the explicit per-attribute negation. A
|
|
||||||
* later -p/-t re-enables the attribute directly, so the flag only prevents
|
|
||||||
* the incremental/delta implication, never a POSITIVE request. */
|
|
||||||
bool preserve_perms_explicit_off;
|
|
||||||
bool preserve_times_explicit_off;
|
|
||||||
bool show_progress;
|
bool show_progress;
|
||||||
int compression_threads;
|
int compression_threads;
|
||||||
int ssh_port;
|
int ssh_port;
|
||||||
@@ -378,18 +416,6 @@ typedef struct Config {
|
|||||||
bool use_tls;
|
bool use_tls;
|
||||||
char* server_host;
|
char* server_host;
|
||||||
int server_port;
|
int server_port;
|
||||||
/* True when --server-port/--port was explicitly given. CLIENT-ONLY (never
|
|
||||||
* serialized): --dry-run uses it to decide whether a real server handshake
|
|
||||||
* was requested, so a plain local destination (no explicit port) keeps the
|
|
||||||
* existing client-side dry-run behavior instead of dialing the default
|
|
||||||
* 127.0.0.1:8080. */
|
|
||||||
bool server_port_set;
|
|
||||||
/* True when --server-host was explicitly given. CLIENT-ONLY (never
|
|
||||||
* serialized), and distinct from the "127.0.0.1" default: --dry-run uses it
|
|
||||||
* to route an explicit remote target to the server so it reports receiver
|
|
||||||
* state exactly like a real run, instead of silently running the client-side
|
|
||||||
* manifest. */
|
|
||||||
bool server_host_set;
|
|
||||||
char* tls_cert;
|
char* tls_cert;
|
||||||
char* tls_key;
|
char* tls_key;
|
||||||
char* tls_ca;
|
char* tls_ca;
|
||||||
@@ -435,22 +461,6 @@ typedef struct Config {
|
|||||||
* enters the keep-set. Implied by --delete-missing-args. */
|
* enters the keep-set. Implied by --delete-missing-args. */
|
||||||
bool ignore_missing_args;
|
bool ignore_missing_args;
|
||||||
|
|
||||||
/* Codec-negotiation CLI state (all client-only, never serialized). The
|
|
||||||
* effective pre-transfer checksum is Config->checksum_algo (serialized);
|
|
||||||
* checksum_transfer_algo is the rsync "transfer" half of a two-name
|
|
||||||
* --checksum-choice form (validated and used only to mirror rsync's
|
|
||||||
* whole-file forcing, since FastSync's per-block strong hash is fixed).
|
|
||||||
* cli_exit_code carries a parser-requested process exit status (rsync uses 4
|
|
||||||
* for an unsupported checksum/compress algorithm) so main() can mirror it. */
|
|
||||||
int checksum_transfer_algo;
|
|
||||||
int cli_exit_code;
|
|
||||||
/* Client-only "the user explicitly chose" bits. They let the per-codec
|
|
||||||
* default level / checksum list be applied only when the corresponding
|
|
||||||
* rsync option was omitted (an explicit --compress-level / --checksum-choice
|
|
||||||
* always wins). Never serialized. */
|
|
||||||
bool compression_level_set;
|
|
||||||
bool checksum_choice_set;
|
|
||||||
|
|
||||||
// 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).
|
||||||
ArrayList* filters; /* --filter=RULE rule strings, in order */
|
ArrayList* filters; /* --filter=RULE rule strings, in order */
|
||||||
@@ -486,7 +496,6 @@ typedef struct Config {
|
|||||||
/* --outbuf mode (OutbufMode): stdout/stderr buffering. Client-only launch
|
/* --outbuf mode (OutbufMode): stdout/stderr buffering. Client-only launch
|
||||||
* concern: NEVER crosses the wire. */
|
* concern: NEVER crosses the wire. */
|
||||||
int outbuf;
|
int outbuf;
|
||||||
bool old_args;
|
|
||||||
/* --remote-option=OPT (Phase 5, long form only): one or more extra command-line
|
/* --remote-option=OPT (Phase 5, long form only): one or more extra command-line
|
||||||
* options to append to the REMOTE server invocation over SSH. CLIENT-ONLY:
|
* options to append to the REMOTE server invocation over SSH. CLIENT-ONLY:
|
||||||
* they are composed into the remote command line by ssh_build_remote_command()
|
* they are composed into the remote command line by ssh_build_remote_command()
|
||||||
@@ -556,7 +565,6 @@ typedef struct Config {
|
|||||||
* process and are NEVER serialized into the config frame. */
|
* process and are NEVER serialized into the config frame. */
|
||||||
int stop_after_mins; /* --stop-after=MINS minutes; 0 when unset */
|
int stop_after_mins; /* --stop-after=MINS minutes; 0 when unset */
|
||||||
time_t stop_at; /* --stop-at=... absolute wall-clock deadline */
|
time_t stop_at; /* --stop-at=... absolute wall-clock deadline */
|
||||||
bool stop_at_set; /* true when --stop-at was given */
|
|
||||||
|
|
||||||
/* Client-only residual-batch paths. A residual batch is a self-contained
|
/* Client-only residual-batch paths. A residual batch is a self-contained
|
||||||
* single-file record of the whole source tree (full file images using the
|
* single-file record of the whole source tree (full file images using the
|
||||||
|
|||||||
@@ -0,0 +1,656 @@
|
|||||||
|
#include "delete.h"
|
||||||
|
|
||||||
|
#include "delay_updates.h"
|
||||||
|
#include "filter.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "utils.h"
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
/* Build the keep-set index from the exact manifest entries only. A lookup of
|
||||||
|
`rel` succeeds iff `rel` is a kept entry, a kept directory, or an ancestor
|
||||||
|
directory of kept content (the old is_dir_in_manifest predicate); the sorted
|
||||||
|
view answers "is an ancestor of kept content" without materializing any
|
||||||
|
per-component prefix copy, so the index is O(manifest size) memory. */
|
||||||
|
static bool build_keep_index(const ArrayList* manifest, PathIndex* index) {
|
||||||
|
if (!manifest || manifest->size <= 0)
|
||||||
|
return path_index_build(index, NULL, 0);
|
||||||
|
return path_index_build(index, (const char* const*)manifest->items, (size_t)manifest->size);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool keep_is_dir(const PathIndex* index, const char* rel_path) {
|
||||||
|
return path_index_contains(index, rel_path) || path_index_has_descendant(index, rel_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool keep_is_file(const PathIndex* index, const char* rel_path) {
|
||||||
|
return path_index_contains(index, rel_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* True when child_rel is, or lies below, a protected entry. A prefix "a"
|
||||||
|
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
|
||||||
|
directories that share such a name stay ordinary destination content. */
|
||||||
|
bool path_under_skip_prefix(const char* child_rel, bool at_root, const DeleteSkipEntry* skips,
|
||||||
|
int skip_count) {
|
||||||
|
for (int i = 0; i < skip_count; i++) {
|
||||||
|
if (skips[i].top_level_only && !at_root)
|
||||||
|
continue;
|
||||||
|
size_t prefix_len = strlen(skips[i].prefix);
|
||||||
|
if (strncmp(child_rel, skips[i].prefix, prefix_len) == 0 &&
|
||||||
|
(child_rel[prefix_len] == '\0' || child_rel[prefix_len] == '/'))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Per-run deletion budget and tallies. `max_delete` is the cap on the number
|
||||||
|
of entries the walker may remove (SIZE_MAX = unlimited); once it is reached
|
||||||
|
the remaining extras are counted in `skipped` and left in place, matching
|
||||||
|
rsync's partial --max-delete behavior. */
|
||||||
|
typedef struct {
|
||||||
|
size_t max_delete;
|
||||||
|
size_t deleted;
|
||||||
|
size_t skipped;
|
||||||
|
bool limit_hit;
|
||||||
|
} DeleteBudget;
|
||||||
|
|
||||||
|
/* True when direct children of the directory named by `rel` may be removed.
|
||||||
|
With no synchronization info (dirs == NULL) the whole tree is deletable; when
|
||||||
|
a dirs index is supplied only its exact entries are (the receive root is the
|
||||||
|
"." sentinel). */
|
||||||
|
static bool is_synced_dir(const PathIndex* dirs, const char* rel) {
|
||||||
|
if (!dirs)
|
||||||
|
return true;
|
||||||
|
return path_index_contains(dirs, rel[0] == '\0' ? "." : rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Unsigned byte-wise string compare, matching rsync's u_strcmp (a signed
|
||||||
|
strcmp would order bytes >= 0x80 differently). */
|
||||||
|
static int delete_name_cmp(const char* a, const char* b) {
|
||||||
|
const unsigned char* pa = (const unsigned char*)a;
|
||||||
|
const unsigned char* pb = (const unsigned char*)b;
|
||||||
|
while (*pa != '\0' && *pa == *pb) {
|
||||||
|
pa++;
|
||||||
|
pb++;
|
||||||
|
}
|
||||||
|
return (int)*pa - (int)*pb;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool delete_dir_entries_collect(int dirfd, DeleteDirEntry** out, size_t* count,
|
||||||
|
bool* operation_ok) {
|
||||||
|
*out = NULL;
|
||||||
|
*count = 0;
|
||||||
|
if (operation_ok)
|
||||||
|
*operation_ok = true;
|
||||||
|
int scanfd = openat(dirfd, ".", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
if (scanfd < 0)
|
||||||
|
return false;
|
||||||
|
DIR* dir = fdopendir(scanfd);
|
||||||
|
if (!dir) {
|
||||||
|
close(scanfd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
DeleteDirEntry* entries = NULL;
|
||||||
|
size_t used = 0;
|
||||||
|
size_t capacity = 0;
|
||||||
|
bool ok = true;
|
||||||
|
const struct dirent* entry;
|
||||||
|
while ((entry = readdir(dir)) != NULL) {
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
struct stat st;
|
||||||
|
if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
||||||
|
if (errno != ENOENT && operation_ok)
|
||||||
|
*operation_ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (used == capacity) {
|
||||||
|
size_t next = capacity == 0 ? 16 : capacity * 2;
|
||||||
|
DeleteDirEntry* grown = realloc(entries, next * sizeof(*grown));
|
||||||
|
if (!grown) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
entries = grown;
|
||||||
|
capacity = next;
|
||||||
|
}
|
||||||
|
entries[used].name = str_dup(entry->d_name);
|
||||||
|
if (!entries[used].name) {
|
||||||
|
ok = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
entries[used].is_dir = S_ISDIR(st.st_mode);
|
||||||
|
used++;
|
||||||
|
}
|
||||||
|
closedir(dir);
|
||||||
|
if (!ok) {
|
||||||
|
delete_dir_entries_free(entries, used);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*out = entries;
|
||||||
|
*count = used;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete_dir_entries_free(DeleteDirEntry* entries, size_t count) {
|
||||||
|
if (!entries)
|
||||||
|
return;
|
||||||
|
for (size_t i = 0; i < count; i++)
|
||||||
|
free(entries[i].name);
|
||||||
|
free(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rsync's extraneous-entry order: subdirectories before files, each group in
|
||||||
|
descending name order. */
|
||||||
|
int delete_dir_entry_cmp_desc(const void* a, const void* b) {
|
||||||
|
const DeleteDirEntry* ea = a;
|
||||||
|
const DeleteDirEntry* eb = b;
|
||||||
|
if (ea->is_dir != eb->is_dir)
|
||||||
|
return ea->is_dir ? -1 : 1;
|
||||||
|
return -delete_name_cmp(ea->name, eb->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rsync's kept-subdirectory order: plain ascending name. */
|
||||||
|
int delete_dir_entry_cmp_asc(const void* a, const void* b) {
|
||||||
|
const DeleteDirEntry* ea = a;
|
||||||
|
const DeleteDirEntry* eb = b;
|
||||||
|
return delete_name_cmp(ea->name, eb->name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* How the shared classification/descent walk disposes of an extra it has
|
||||||
|
identified. LIST records the destination-relative path without touching disk
|
||||||
|
(the -n/--dry-run would-delete enumeration); DELETE unlinks/rmdirs it, charges
|
||||||
|
the shared --max-delete budget and notifies the observer. Both modes classify
|
||||||
|
and traverse identically, so the dry-run enumeration and the real deletion
|
||||||
|
cannot drift. */
|
||||||
|
typedef enum { DELETE_WALK_MODE_DELETE, DELETE_WALK_MODE_LIST } DeleteWalkMode;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
DeleteWalkMode mode;
|
||||||
|
DeleteBudget* budget; /* DELETE mode */
|
||||||
|
ArrayList* out; /* LIST mode: receives strdup'd relative paths */
|
||||||
|
size_t* recorded; /* LIST mode */
|
||||||
|
DeletePathObserver observer; /* DELETE mode */
|
||||||
|
void* observer_context; /* DELETE mode */
|
||||||
|
} DeleteWalkState;
|
||||||
|
|
||||||
|
/* Remove the extras directly inside the directory open on `dirfd` (DELETE mode)
|
||||||
|
or record the paths that WOULD be removed (LIST mode), recursing into every
|
||||||
|
child directory so kept content below a synchronized prefix is reached.
|
||||||
|
`all_removed` reports whether every child entry was removed (so the caller may
|
||||||
|
rmdir this directory). A child directory is never removed when it is itself a
|
||||||
|
synchronized directory or holds kept content; with a dirs index supplied,
|
||||||
|
direct children of a non-synchronized directory are never extras at all (they
|
||||||
|
are left in place but still descended into). Symlinks are unlinked like any
|
||||||
|
other non-directory extra (never followed).
|
||||||
|
|
||||||
|
Entries are processed in rsync's order (extraneous subdirectories in
|
||||||
|
descending name order, then extraneous files, then kept subdirectories in
|
||||||
|
ascending order) rather than readdir() order, so `--max-delete` leaves the
|
||||||
|
same survivors and the `--info=del`/dry-run line order matches rsync. */
|
||||||
|
static bool delete_walk_fd(int dirfd, const char* rel_path, const PathIndex* keep,
|
||||||
|
const PathIndex* dirs, DeleteWalkState* state,
|
||||||
|
const DeleteSkipEntry* skips, int skip_count,
|
||||||
|
const FilterRuleList* protect_rules, bool parent_deletable,
|
||||||
|
bool* all_removed) {
|
||||||
|
DeleteDirEntry* entries = NULL;
|
||||||
|
size_t count = 0;
|
||||||
|
bool collect_ok = true;
|
||||||
|
if (!delete_dir_entries_collect(dirfd, &entries, &count, &collect_ok))
|
||||||
|
return false;
|
||||||
|
bool operation_ok = collect_ok;
|
||||||
|
bool local_survives = false;
|
||||||
|
bool* shielded = calloc(count ? count : 1, sizeof(bool));
|
||||||
|
bool* is_extra = calloc(count ? count : 1, sizeof(bool));
|
||||||
|
if (!shielded || !is_extra) {
|
||||||
|
free(shielded);
|
||||||
|
free(is_extra);
|
||||||
|
delete_dir_entries_free(entries, count);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
/* A directory is deletable when it or ANY ancestor is synchronized; the
|
||||||
|
`parent_deletable` flag carries that down the recursion so dest-only
|
||||||
|
directories below a synchronized root are removed wholesale. */
|
||||||
|
bool deletable = parent_deletable || is_synced_dir(dirs, rel_path);
|
||||||
|
bool at_root = rel_path[0] == '\0';
|
||||||
|
|
||||||
|
/* Reproduce rsync's traversal order: extraneous subdirectories in descending
|
||||||
|
name order, then extraneous files in descending name order, and kept
|
||||||
|
subdirectories only afterwards (ascending). Sorting up front also fixes the
|
||||||
|
identity of the survivors under a partial --max-delete. */
|
||||||
|
if (count > 1)
|
||||||
|
qsort(entries, count, sizeof(*entries), delete_dir_entry_cmp_desc);
|
||||||
|
size_t dir_count = 0;
|
||||||
|
while (dir_count < count && entries[dir_count].is_dir)
|
||||||
|
dir_count++;
|
||||||
|
|
||||||
|
/* Classify every entry up front (the verdict does not depend on processing
|
||||||
|
order) so the ordered passes below can act on it. */
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
||||||
|
if (!child_rel) {
|
||||||
|
operation_ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
/* A --delay-updates run keeps its staging directory as a direct child of
|
||||||
|
the receive root, and basis-dir snapshots live below it too. Their
|
||||||
|
contents are not manifest entries, so descending into them would delete
|
||||||
|
every staged / basis file as an "extra". Only the staging name (a
|
||||||
|
top-level-only prefix) and the basis prefixes are protected: a nested
|
||||||
|
destination directory that happens to be called .fastsync-stage is
|
||||||
|
ordinary content. */
|
||||||
|
if (path_under_skip_prefix(child_rel, at_root, skips, skip_count)) {
|
||||||
|
shielded[i] = true;
|
||||||
|
local_survives = true;
|
||||||
|
} else if (protect_rules &&
|
||||||
|
filter_rules_apply_side(protect_rules, child_rel, entries[i].name, entries[i].is_dir,
|
||||||
|
FILTER_SIDE_RECEIVER) == FILTER_ACTION_PROTECT) {
|
||||||
|
/* A first-match protect rule shields the extra; for a directory the whole
|
||||||
|
subtree is shielded (rsync prunes an excluded directory), so do not
|
||||||
|
descend. */
|
||||||
|
shielded[i] = true;
|
||||||
|
local_survives = true;
|
||||||
|
} else if (entries[i].is_dir) {
|
||||||
|
bool child_synced = dirs && path_index_contains(dirs, child_rel);
|
||||||
|
is_extra[i] = deletable && !child_synced && !keep_is_dir(keep, child_rel);
|
||||||
|
if (!is_extra[i])
|
||||||
|
local_survives = true;
|
||||||
|
} else {
|
||||||
|
is_extra[i] = deletable && !keep_is_file(keep, child_rel);
|
||||||
|
if (!is_extra[i])
|
||||||
|
local_survives = true;
|
||||||
|
}
|
||||||
|
free(child_rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pass 1: extraneous subdirectories, descending. */
|
||||||
|
for (size_t i = 0; i < dir_count; i++) {
|
||||||
|
if (!is_extra[i])
|
||||||
|
continue;
|
||||||
|
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
||||||
|
if (!child_rel) {
|
||||||
|
operation_ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
bool child_all_removed = false;
|
||||||
|
if (childfd >= 0) {
|
||||||
|
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect_rules,
|
||||||
|
deletable, &child_all_removed))
|
||||||
|
operation_ok = false;
|
||||||
|
close(childfd);
|
||||||
|
} else if (errno != ENOENT) {
|
||||||
|
operation_ok = false;
|
||||||
|
}
|
||||||
|
if (child_all_removed && deletable) {
|
||||||
|
if (state->mode == DELETE_WALK_MODE_LIST) {
|
||||||
|
/* Record the directory with rsync's trailing slash. */
|
||||||
|
size_t len = strlen(child_rel);
|
||||||
|
char* copy = malloc(len + 2);
|
||||||
|
if (!copy) {
|
||||||
|
operation_ok = false;
|
||||||
|
} else {
|
||||||
|
memcpy(copy, child_rel, len);
|
||||||
|
copy[len] = '/';
|
||||||
|
copy[len + 1] = '\0';
|
||||||
|
if (!array_list_add(state->out, copy)) {
|
||||||
|
free(copy);
|
||||||
|
operation_ok = false;
|
||||||
|
} else {
|
||||||
|
(*state->recorded)++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (state->budget->deleted >= state->budget->max_delete) {
|
||||||
|
state->budget->limit_hit = true;
|
||||||
|
state->budget->skipped++;
|
||||||
|
local_survives = true;
|
||||||
|
} else if (unlinkat(dirfd, entries[i].name, AT_REMOVEDIR) != 0) {
|
||||||
|
/* ENOENT: already gone (fine). ENOTEMPTY/EEXIST: the directory still
|
||||||
|
holds entries the walker leaves in place (a protected excluded
|
||||||
|
prefix, a kept file the manifest protects, a symlink); rsync leaves
|
||||||
|
such a directory behind, so this is not an error. Only genuine I/O
|
||||||
|
failures abort the deletion. */
|
||||||
|
if (errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST)
|
||||||
|
operation_ok = false;
|
||||||
|
local_survives = true;
|
||||||
|
} else {
|
||||||
|
state->budget->deleted++;
|
||||||
|
/* rsync reports a removed directory with a trailing slash. */
|
||||||
|
if (state->observer) {
|
||||||
|
size_t len = strlen(child_rel);
|
||||||
|
char* with_slash = malloc(len + 2);
|
||||||
|
if (with_slash) {
|
||||||
|
memcpy(with_slash, child_rel, len);
|
||||||
|
with_slash[len] = '/';
|
||||||
|
with_slash[len + 1] = '\0';
|
||||||
|
state->observer(state->observer_context, with_slash);
|
||||||
|
free(with_slash);
|
||||||
|
} else {
|
||||||
|
state->observer(state->observer_context, child_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
local_survives = true;
|
||||||
|
}
|
||||||
|
free(child_rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pass 2: extraneous files, descending. */
|
||||||
|
for (size_t i = dir_count; i < count; i++) {
|
||||||
|
if (!is_extra[i])
|
||||||
|
continue;
|
||||||
|
if (state->mode == DELETE_WALK_MODE_LIST) {
|
||||||
|
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
||||||
|
if (!child_rel) {
|
||||||
|
operation_ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
char* copy = str_dup(child_rel);
|
||||||
|
if (!copy || !array_list_add(state->out, copy)) {
|
||||||
|
free(copy);
|
||||||
|
operation_ok = false;
|
||||||
|
} else {
|
||||||
|
(*state->recorded)++;
|
||||||
|
}
|
||||||
|
free(child_rel);
|
||||||
|
} else if (state->budget->deleted >= state->budget->max_delete) {
|
||||||
|
state->budget->limit_hit = true;
|
||||||
|
state->budget->skipped++;
|
||||||
|
local_survives = true;
|
||||||
|
} else if (unlinkat(dirfd, entries[i].name, 0) != 0) {
|
||||||
|
if (errno != ENOENT)
|
||||||
|
operation_ok = false;
|
||||||
|
local_survives = true;
|
||||||
|
} else {
|
||||||
|
state->budget->deleted++;
|
||||||
|
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
||||||
|
if (child_rel) {
|
||||||
|
if (state->observer)
|
||||||
|
state->observer(state->observer_context, child_rel);
|
||||||
|
char* escaped_path = output_escape(child_rel, log_get_8_bit_output());
|
||||||
|
fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : "<allocation failed>");
|
||||||
|
free(escaped_path);
|
||||||
|
}
|
||||||
|
free(child_rel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pass 3: kept subdirectories, ascending (rsync descends into these only
|
||||||
|
after the parent's own extras have been handled). */
|
||||||
|
for (size_t i = dir_count; i-- > 0;) {
|
||||||
|
if (is_extra[i] || shielded[i])
|
||||||
|
continue;
|
||||||
|
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
||||||
|
if (!child_rel) {
|
||||||
|
operation_ok = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
bool child_all_removed = false;
|
||||||
|
if (childfd >= 0) {
|
||||||
|
if (!delete_walk_fd(childfd, child_rel, keep, dirs, state, skips, skip_count, protect_rules,
|
||||||
|
deletable, &child_all_removed))
|
||||||
|
operation_ok = false;
|
||||||
|
close(childfd);
|
||||||
|
} else if (errno != ENOENT) {
|
||||||
|
operation_ok = false;
|
||||||
|
}
|
||||||
|
/* A kept/synchronized directory is never removed. */
|
||||||
|
local_survives = true;
|
||||||
|
free(child_rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(shielded);
|
||||||
|
free(is_extra);
|
||||||
|
delete_dir_entries_free(entries, count);
|
||||||
|
*all_removed = !local_survives;
|
||||||
|
return operation_ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open the receive root following the same authorized-root confinement the
|
||||||
|
walker uses, or dest_root directly when no authorized root is installed. */
|
||||||
|
static int open_destination_root(const char* dest_root) {
|
||||||
|
int root_fd = utils_get_authorized_root_fd();
|
||||||
|
if (root_fd >= 0) {
|
||||||
|
if (utils_get_authorized_root_path())
|
||||||
|
return utils_open_authorized_destination(dest_root);
|
||||||
|
if (dest_root == NULL)
|
||||||
|
return dup(root_fd);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return open(dest_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
|
||||||
|
const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count,
|
||||||
|
const FilterRuleList* protect_rules, ArrayList* out, size_t* count_out) {
|
||||||
|
if (count_out)
|
||||||
|
*count_out = 0;
|
||||||
|
if (!manifest || !out)
|
||||||
|
return false;
|
||||||
|
PathIndex keep;
|
||||||
|
if (!build_keep_index(manifest, &keep))
|
||||||
|
return false;
|
||||||
|
PathIndex dirs;
|
||||||
|
bool have_dirs = synced_dirs != NULL;
|
||||||
|
if (have_dirs &&
|
||||||
|
!path_index_build(&dirs, (const char* const*)synced_dirs->items, (size_t)synced_dirs->size)) {
|
||||||
|
path_index_free(&keep);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int rootfd = open_destination_root(dest_root);
|
||||||
|
if (rootfd < 0) {
|
||||||
|
path_index_free(&keep);
|
||||||
|
if (have_dirs)
|
||||||
|
path_index_free(&dirs);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool all_removed = false;
|
||||||
|
size_t recorded = 0;
|
||||||
|
DeleteWalkState state = {.mode = DELETE_WALK_MODE_LIST,
|
||||||
|
.budget = NULL,
|
||||||
|
.out = out,
|
||||||
|
.recorded = &recorded,
|
||||||
|
.observer = NULL,
|
||||||
|
.observer_context = NULL};
|
||||||
|
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count,
|
||||||
|
protect_rules, false, &all_removed);
|
||||||
|
if (close(rootfd) != 0)
|
||||||
|
ok = false;
|
||||||
|
path_index_free(&keep);
|
||||||
|
if (have_dirs)
|
||||||
|
path_index_free(&dirs);
|
||||||
|
if (count_out)
|
||||||
|
*count_out = recorded;
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest,
|
||||||
|
const ArrayList* synced_dirs, size_t max_delete,
|
||||||
|
const DeleteSkipEntry* skips, int skip_count,
|
||||||
|
const FilterRuleList* protect_rules,
|
||||||
|
size_t* deleted_out, size_t* skipped_out,
|
||||||
|
DeletePathObserver observer,
|
||||||
|
void* observer_context) {
|
||||||
|
if (deleted_out)
|
||||||
|
*deleted_out = 0;
|
||||||
|
if (skipped_out)
|
||||||
|
*skipped_out = 0;
|
||||||
|
if (!manifest)
|
||||||
|
return DELETE_WALK_ERROR;
|
||||||
|
/* Index the keep-set (and the synchronized-dir set, when supplied) once so
|
||||||
|
membership is answered in O(path length) instead of scanning every entry
|
||||||
|
for every destination entry. */
|
||||||
|
PathIndex keep;
|
||||||
|
if (!build_keep_index(manifest, &keep))
|
||||||
|
return DELETE_WALK_ERROR;
|
||||||
|
PathIndex dirs;
|
||||||
|
bool have_dirs = synced_dirs != NULL;
|
||||||
|
if (have_dirs &&
|
||||||
|
!path_index_build(&dirs, (const char* const*)synced_dirs->items, (size_t)synced_dirs->size)) {
|
||||||
|
path_index_free(&keep);
|
||||||
|
return DELETE_WALK_ERROR;
|
||||||
|
}
|
||||||
|
int rootfd = open_destination_root(dest_root);
|
||||||
|
if (rootfd < 0) {
|
||||||
|
path_index_free(&keep);
|
||||||
|
if (have_dirs)
|
||||||
|
path_index_free(&dirs);
|
||||||
|
return DELETE_WALK_ERROR;
|
||||||
|
}
|
||||||
|
DeleteBudget budget = {.max_delete = max_delete, .deleted = 0, .skipped = 0, .limit_hit = false};
|
||||||
|
bool all_removed = false;
|
||||||
|
DeleteWalkState state = {.mode = DELETE_WALK_MODE_DELETE,
|
||||||
|
.budget = &budget,
|
||||||
|
.out = NULL,
|
||||||
|
.recorded = NULL,
|
||||||
|
.observer = observer,
|
||||||
|
.observer_context = observer_context};
|
||||||
|
bool ok = delete_walk_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &state, skips, skip_count,
|
||||||
|
protect_rules, false, &all_removed);
|
||||||
|
if (close(rootfd) != 0)
|
||||||
|
ok = false;
|
||||||
|
path_index_free(&keep);
|
||||||
|
if (have_dirs)
|
||||||
|
path_index_free(&dirs);
|
||||||
|
if (deleted_out)
|
||||||
|
*deleted_out = budget.deleted;
|
||||||
|
if (skipped_out)
|
||||||
|
*skipped_out = budget.skipped;
|
||||||
|
if (!ok)
|
||||||
|
return DELETE_WALK_ERROR;
|
||||||
|
return budget.limit_hit ? DELETE_WALK_LIMIT_REACHED : DELETE_WALK_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest,
|
||||||
|
const ArrayList* synced_dirs, size_t max_delete,
|
||||||
|
const DeleteSkipEntry* skips, int skip_count,
|
||||||
|
const FilterRuleList* protect_rules, size_t* deleted_out,
|
||||||
|
size_t* skipped_out) {
|
||||||
|
return delete_extras_limited_observed(dest_root, manifest, synced_dirs, max_delete, skips,
|
||||||
|
skip_count, protect_rules, deleted_out, skipped_out, NULL,
|
||||||
|
NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool delete_extras(const char* dest_root, const ArrayList* manifest) {
|
||||||
|
return delete_extras_limited(dest_root, manifest, NULL, SIZE_MAX, NULL, 0, NULL, NULL, NULL) ==
|
||||||
|
DELETE_WALK_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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* delete_basis_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool delete_skips_build(const Config* config, const ArrayList* protected_paths,
|
||||||
|
const ArrayList* size_skipped, bool basis_root_relative,
|
||||||
|
DeleteSkipSet* out) {
|
||||||
|
if (!out)
|
||||||
|
return false;
|
||||||
|
out->entries = NULL;
|
||||||
|
out->owned_prefixes = NULL;
|
||||||
|
out->count = 0;
|
||||||
|
out->owned_count = 0;
|
||||||
|
if (!config)
|
||||||
|
return false;
|
||||||
|
int protected_count = protected_paths ? protected_paths->size : 0;
|
||||||
|
int size_skipped_count = size_skipped ? size_skipped->size : 0;
|
||||||
|
int count =
|
||||||
|
(config->delay_updates ? 1 : 0) + config->basis_count + protected_count + size_skipped_count;
|
||||||
|
if (count == 0)
|
||||||
|
return true;
|
||||||
|
out->entries = calloc((size_t)count, sizeof(DeleteSkipEntry));
|
||||||
|
if (!out->entries)
|
||||||
|
return false;
|
||||||
|
if (basis_root_relative && config->basis_count > 0) {
|
||||||
|
out->owned_prefixes = calloc((size_t)config->basis_count, sizeof(char*));
|
||||||
|
if (!out->owned_prefixes) {
|
||||||
|
free(out->entries);
|
||||||
|
out->entries = NULL;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
out->owned_count = config->basis_count;
|
||||||
|
}
|
||||||
|
int idx = 0;
|
||||||
|
if (config->delay_updates) {
|
||||||
|
out->entries[idx].prefix = DELAY_UPDATES_STAGING_DIR;
|
||||||
|
out->entries[idx].top_level_only = true;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < config->basis_count; i++) {
|
||||||
|
const char* prefix = config->basis_dirs[i].path;
|
||||||
|
if (basis_root_relative) {
|
||||||
|
/* An absolute basis outside the receive root is unreachable by this walk,
|
||||||
|
so it contributes no protection prefix (and no slot). */
|
||||||
|
char* relative = delete_basis_relative(config, config->basis_dirs[i].path);
|
||||||
|
if (!relative)
|
||||||
|
continue;
|
||||||
|
out->owned_prefixes[i] = relative;
|
||||||
|
prefix = relative;
|
||||||
|
}
|
||||||
|
out->entries[idx].prefix = prefix;
|
||||||
|
out->entries[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < protected_count; i++) {
|
||||||
|
out->entries[idx].prefix = (const char*)protected_paths->items[i];
|
||||||
|
out->entries[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < size_skipped_count; i++) {
|
||||||
|
out->entries[idx].prefix = (const char*)size_skipped->items[i];
|
||||||
|
out->entries[idx].top_level_only = false;
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
out->count = idx;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void delete_skips_free(DeleteSkipSet* set) {
|
||||||
|
if (!set)
|
||||||
|
return;
|
||||||
|
if (set->owned_prefixes) {
|
||||||
|
for (int i = 0; i < set->owned_count; i++)
|
||||||
|
free(set->owned_prefixes[i]);
|
||||||
|
}
|
||||||
|
free(set->owned_prefixes);
|
||||||
|
free(set->entries);
|
||||||
|
set->entries = NULL;
|
||||||
|
set->owned_prefixes = NULL;
|
||||||
|
set->count = 0;
|
||||||
|
set->owned_count = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#ifndef DELETE_H
|
||||||
|
#define DELETE_H
|
||||||
|
|
||||||
|
#include "array_list.h"
|
||||||
|
#include "config.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Delete engine.
|
||||||
|
*
|
||||||
|
* This module owns destination-relative delete traversal: the ordered directory
|
||||||
|
* walker that reproduces rsync's extraneous-entry order, the skip-prefix
|
||||||
|
* protection set shared by every delete pass, and the read-only enumeration
|
||||||
|
* that mirrors the walker for -n/--dry-run. The budgeted manifest commit
|
||||||
|
* (delete_commit.c) and the per-directory delete plans (delete_plan.c) are
|
||||||
|
* built on the primitives exported here. */
|
||||||
|
|
||||||
|
/* Result of a bounded extra-file deletion run. */
|
||||||
|
typedef enum {
|
||||||
|
/* Every extra entry was removed (or there were none). */
|
||||||
|
DELETE_WALK_OK = 0,
|
||||||
|
/* The numeric cap for this run was reached before every extra was removed.
|
||||||
|
The walker removed exactly the entries the cap allowed and skipped (without
|
||||||
|
removing) the rest, matching rsync's partial --max-delete behavior. */
|
||||||
|
DELETE_WALK_LIMIT_REACHED,
|
||||||
|
/* A traversal or unlink failure aborted the deletion (partial removal is
|
||||||
|
possible, mirroring the delete pass). */
|
||||||
|
DELETE_WALK_ERROR
|
||||||
|
} DeleteWalkResult;
|
||||||
|
|
||||||
|
/* One protected entry for the delete walker. When top_level_only is true the
|
||||||
|
prefix is skipped only as a DIRECT child of dest_root (the --delay-updates
|
||||||
|
staging directory, which must not hide genuine extras inside a nested
|
||||||
|
destination directory that happens to share the staging name); otherwise the
|
||||||
|
prefix is skipped at any depth (the --compare-dest/--copy-dest/--link-dest
|
||||||
|
basis trees, and the sender-side protected filter-excluded prefixes, which
|
||||||
|
are never destination content). */
|
||||||
|
typedef struct {
|
||||||
|
const char* prefix;
|
||||||
|
bool top_level_only;
|
||||||
|
} DeleteSkipEntry;
|
||||||
|
|
||||||
|
/* A built skip-prefix set. `entries`/`count` are what path_under_skip_prefix()
|
||||||
|
consumes. `owned_prefixes` holds any prefix strings the builder had to
|
||||||
|
allocate (root-relative basis-dir conversions); it is NULL when every prefix
|
||||||
|
is borrowed from the config or the caller's lists. Release with
|
||||||
|
delete_skips_free(). */
|
||||||
|
typedef struct {
|
||||||
|
DeleteSkipEntry* entries;
|
||||||
|
char** owned_prefixes;
|
||||||
|
int count;
|
||||||
|
int owned_count;
|
||||||
|
} DeleteSkipSet;
|
||||||
|
|
||||||
|
/* 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);
|
||||||
|
|
||||||
|
/* One destination-directory entry collected up front so the delete walkers can
|
||||||
|
reproduce rsync's traversal order instead of readdir() order. rsync processes
|
||||||
|
a directory's extraneous subdirectories first (descending name, depth-first),
|
||||||
|
then its extraneous files (descending name), and only afterwards descends into
|
||||||
|
its kept subdirectories (ascending name). */
|
||||||
|
typedef struct {
|
||||||
|
char* name;
|
||||||
|
bool is_dir;
|
||||||
|
} DeleteDirEntry;
|
||||||
|
/* Collect the entries of the directory open on `dirfd` (excluding "." and ".."),
|
||||||
|
stat'ing each with AT_SYMLINK_NOFOLLOW. On success *out is a malloc'd array of
|
||||||
|
*count entries whose names the caller frees with delete_dir_entries_free().
|
||||||
|
Returns false on an allocation/readdir failure; a vanished entry (ENOENT) is
|
||||||
|
skipped, any other stat failure is reported through *operation_ok while the
|
||||||
|
walk continues. */
|
||||||
|
bool delete_dir_entries_collect(int dirfd, DeleteDirEntry** out, size_t* count, bool* operation_ok);
|
||||||
|
void delete_dir_entries_free(DeleteDirEntry* entries, size_t count);
|
||||||
|
/* Sort comparators: `_desc` orders subdirectories before files and each group by
|
||||||
|
descending name (rsync's extraneous-entry order); `_asc` orders plain ascending
|
||||||
|
name (rsync's kept-subdirectory order). */
|
||||||
|
int delete_dir_entry_cmp_desc(const void* a, const void* b);
|
||||||
|
int delete_dir_entry_cmp_asc(const void* a, const void* b);
|
||||||
|
|
||||||
|
/* Remove files/dirs/symlinks under dest_root that are not listed in manifest
|
||||||
|
without ever descending into a protected prefix (see DeleteSkipEntry). When
|
||||||
|
`synced_dirs` is non-NULL, extras are only removed directly inside a directory
|
||||||
|
whose destination-relative path is an exact entry in that list (the receive
|
||||||
|
root is the "." sentinel); directories outside the synchronized set are still
|
||||||
|
descended into so kept content below a listed directory is preserved, but
|
||||||
|
nothing in them is removed. A NULL `synced_dirs` keeps the legacy behavior of
|
||||||
|
treating the whole destination tree as deletable. `max_delete` caps the
|
||||||
|
number of removed entries (SIZE_MAX = unlimited): the walker removes up to the
|
||||||
|
cap and returns DELETE_WALK_LIMIT_REACHED when more extras remained.
|
||||||
|
`deleted_out`/`skipped_out` optionally receive the number of entries removed
|
||||||
|
and the number skipped because of the cap. */
|
||||||
|
DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest,
|
||||||
|
const ArrayList* synced_dirs, size_t max_delete,
|
||||||
|
const DeleteSkipEntry* skips, int skip_count,
|
||||||
|
const FilterRuleList* protect_rules, size_t* deleted_out,
|
||||||
|
size_t* skipped_out);
|
||||||
|
|
||||||
|
/* Optional per-deletion observer: called for each destination-relative path
|
||||||
|
actually removed (a file, symlink, or directory), in removal order, so the
|
||||||
|
receiver can stream rsync's `--info=del`/`--info=remove` lines. */
|
||||||
|
typedef void (*DeletePathObserver)(void* context, const char* rel_path);
|
||||||
|
|
||||||
|
/* `delete_extras_limited_observed` is delete_extras_limited with an optional
|
||||||
|
* observer; the observer is invoked only for entries truly removed. When
|
||||||
|
* `protect_rules` is non-NULL its receiver-side verdict is evaluated for every
|
||||||
|
* candidate extra: a first-match PROTECT leaves the entry (and, for a
|
||||||
|
* directory, its whole subtree) in place, while RISK/NONE fall through to the
|
||||||
|
* ordinary skip-prefix/keep-set logic. */
|
||||||
|
DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest,
|
||||||
|
const ArrayList* synced_dirs, size_t max_delete,
|
||||||
|
const DeleteSkipEntry* skips, int skip_count,
|
||||||
|
const FilterRuleList* protect_rules,
|
||||||
|
size_t* deleted_out, size_t* skipped_out,
|
||||||
|
DeletePathObserver observer,
|
||||||
|
void* observer_context);
|
||||||
|
/* Read-only companion to delete_extras_limited: walk the destination exactly as
|
||||||
|
the delete pass would and APPEND (strdup'd) destination-relative paths that
|
||||||
|
WOULD be removed, without touching disk. Used for -n/--dry-run --delete
|
||||||
|
would-delete reporting. Returns true on a clean walk; the caller owns the
|
||||||
|
strings appended to `out` and receives their count in *count_out. */
|
||||||
|
bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
|
||||||
|
const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count,
|
||||||
|
const FilterRuleList* protect_rules, ArrayList* out, size_t* count_out);
|
||||||
|
bool delete_extras(const char* dest_root, const ArrayList* manifest);
|
||||||
|
|
||||||
|
/* Build the delete walk's skip-prefix set from the config's --delay-updates
|
||||||
|
staging directory, its --compare-dest/--copy-dest/--link-dest basis dirs, and
|
||||||
|
the caller-supplied protection lists, in that order. `protected_paths` and
|
||||||
|
`size_skipped` are borrowed (may be NULL); every entry in them is protected at
|
||||||
|
any depth. The staging directory is protected only as a DIRECT child of the
|
||||||
|
receive root. `basis_root_relative` selects how a basis path becomes a
|
||||||
|
prefix: true converts an absolute path under the receive root to its
|
||||||
|
root-relative form (the whole-tree commit walk; an unreachable path
|
||||||
|
contributes no slot), false keeps the configured path verbatim (the
|
||||||
|
per-directory plan walk). On success the caller releases `*out` with
|
||||||
|
delete_skips_free(); returns false on allocation failure. */
|
||||||
|
bool delete_skips_build(const Config* config, const ArrayList* protected_paths,
|
||||||
|
const ArrayList* size_skipped, bool basis_root_relative,
|
||||||
|
DeleteSkipSet* out);
|
||||||
|
void delete_skips_free(DeleteSkipSet* set);
|
||||||
|
|
||||||
|
/* 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* delete_basis_relative(const Config* config, const char* path);
|
||||||
|
|
||||||
|
#endif
|
||||||
+37
-184
@@ -126,38 +126,6 @@ typedef struct {
|
|||||||
bool limit_hit;
|
bool limit_hit;
|
||||||
} DeleteBudgetState;
|
} 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
|
/* 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
|
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
|
replaces the server hard bound; rsync deletes up to the bound and skips the
|
||||||
@@ -168,61 +136,19 @@ char* file_receive_basis_delete_relative(const Config* config, const char* path)
|
|||||||
alternate basis directories are never destination content and are skipped at
|
alternate basis directories are never destination content and are skipped at
|
||||||
any depth. Returns true unless a traversal/unlink error aborted the walk;
|
any depth. Returns true unless a traversal/unlink error aborted the walk;
|
||||||
the budget's limit_hit/skipped fields report a cap-stopped run. */
|
the budget's limit_hit/skipped fields report a cap-stopped run. */
|
||||||
static bool delete_extras_budgeted_observed(const Config* config, DeleteManifest* manifest,
|
static bool delete_extras_budgeted_observed(const Config* config, const DeleteManifest* manifest,
|
||||||
DeleteBudgetState* budget, DeletePathObserver observer,
|
DeleteBudgetState* budget, DeletePathObserver observer,
|
||||||
void* observer_context) {
|
void* observer_context) {
|
||||||
if (!config || !manifest || !manifest->keeps)
|
if (!config || !manifest || !manifest->keeps)
|
||||||
return false;
|
return false;
|
||||||
fprintf(stderr, "Deleting files not in manifest...\n");
|
fprintf(stderr, "Deleting files not in manifest...\n");
|
||||||
/* Protected entries:
|
/* Protected entries: the --delay-updates staging name (only as a DIRECT child
|
||||||
- the --delay-updates staging name, protected only as a DIRECT child of the
|
of the receive root), the alternate basis directories and the sender-side
|
||||||
receive root (a nested destination directory that happens to be named
|
protected prefixes (filter-excluded and size-pruned source mirrors), all at
|
||||||
.fastsync-stage is ordinary content);
|
any depth. See delete_skips_build(). */
|
||||||
- alternate basis directories (--compare-dest / --copy-dest / --link-dest)
|
DeleteSkipSet skips;
|
||||||
at any depth: they are extra comparison snapshots the user pointed at,
|
if (!delete_skips_build(config, manifest->protected, NULL, true, &skips))
|
||||||
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;
|
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
|
/* Clamp rather than subtract: an accounting bug where deleted already exceeds
|
||||||
max_delete must never underflow into an effectively unlimited budget. */
|
max_delete must never underflow into an effectively unlimited budget. */
|
||||||
size_t remaining;
|
size_t remaining;
|
||||||
@@ -235,14 +161,9 @@ static bool delete_extras_budgeted_observed(const Config* config, DeleteManifest
|
|||||||
size_t deleted = 0;
|
size_t deleted = 0;
|
||||||
size_t skipped = 0;
|
size_t skipped = 0;
|
||||||
DeleteWalkResult result = delete_extras_limited_observed(
|
DeleteWalkResult result = delete_extras_limited_observed(
|
||||||
config->receive_root_directory, manifest->keeps, manifest->dirs, remaining, skips, used,
|
config->receive_root_directory, manifest->keeps, manifest->dirs, remaining, skips.entries,
|
||||||
config->protect_rules, &deleted, &skipped, observer, observer_context);
|
skips.count, config->protect_rules, &deleted, &skipped, observer, observer_context);
|
||||||
if (owned_prefixes) {
|
delete_skips_free(&skips);
|
||||||
for (int i = 0; i < config->basis_count; i++)
|
|
||||||
free(owned_prefixes[i]);
|
|
||||||
}
|
|
||||||
free(owned_prefixes);
|
|
||||||
free(skips);
|
|
||||||
budget->deleted += deleted;
|
budget->deleted += deleted;
|
||||||
budget->skipped += skipped;
|
budget->skipped += skipped;
|
||||||
if (result == DELETE_WALK_LIMIT_REACHED) {
|
if (result == DELETE_WALK_LIMIT_REACHED) {
|
||||||
@@ -256,7 +177,7 @@ static bool delete_extras_budgeted_observed(const Config* config, DeleteManifest
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool delete_extras_budgeted(const Config* config, DeleteManifest* manifest,
|
static bool delete_extras_budgeted(const Config* config, const DeleteManifest* manifest,
|
||||||
DeleteBudgetState* budget) {
|
DeleteBudgetState* budget) {
|
||||||
return delete_extras_budgeted_observed(config, manifest, budget, NULL, NULL);
|
return delete_extras_budgeted_observed(config, manifest, budget, NULL, NULL);
|
||||||
}
|
}
|
||||||
@@ -294,7 +215,8 @@ static void prefixed_delete_observer(void* context, const char* rel) {
|
|||||||
--max-delete budget: once it is exhausted the remaining requests are skipped
|
--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
|
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. */
|
a validated path or an I/O error), which fails the run. */
|
||||||
static bool delete_missing_args_budgeted_observed(const Config* config, DeleteManifest* manifest,
|
static bool delete_missing_args_budgeted_observed(const Config* config,
|
||||||
|
const DeleteManifest* manifest,
|
||||||
DeleteBudgetState* budget,
|
DeleteBudgetState* budget,
|
||||||
DeletePathObserver observer,
|
DeletePathObserver observer,
|
||||||
void* observer_context) {
|
void* observer_context) {
|
||||||
@@ -303,35 +225,12 @@ static bool delete_missing_args_budgeted_observed(const Config* config, DeleteMa
|
|||||||
if (!manifest->missing || manifest->missing->size == 0)
|
if (!manifest->missing || manifest->missing->size == 0)
|
||||||
return true;
|
return true;
|
||||||
fprintf(stderr, "Deleting destination mirrors of missing source arguments...\n");
|
fprintf(stderr, "Deleting destination mirrors of missing source arguments...\n");
|
||||||
int skip_count = (config->delay_updates ? 1 : 0) + config->basis_count;
|
/* The staging directory and basis snapshots stay protected exactly as in the
|
||||||
DeleteSkipEntry* skips = NULL;
|
extras walker (the missing-args path overrides the ordinary protected
|
||||||
char** owned_prefixes = NULL;
|
prefixes, so those are not passed here). */
|
||||||
int used = 0;
|
DeleteSkipSet skips;
|
||||||
if (skip_count > 0) {
|
if (!delete_skips_build(config, NULL, NULL, true, &skips))
|
||||||
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;
|
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;
|
bool ok = true;
|
||||||
for (int i = 0; i < manifest->missing->size; i++) {
|
for (int i = 0; i < manifest->missing->size; i++) {
|
||||||
const char* rel = (const char*)manifest->missing->items[i];
|
const char* rel = (const char*)manifest->missing->items[i];
|
||||||
@@ -343,7 +242,7 @@ static bool delete_missing_args_budgeted_observed(const Config* config, DeleteMa
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
bool at_root = strchr(rel, '/') == NULL;
|
bool at_root = strchr(rel, '/') == NULL;
|
||||||
if (path_under_skip_prefix(rel, at_root, skips, used)) {
|
if (path_under_skip_prefix(rel, at_root, skips.entries, skips.count)) {
|
||||||
char* escaped = output_escape(rel, log_get_8_bit_output());
|
char* escaped = output_escape(rel, log_get_8_bit_output());
|
||||||
log_message(LOG_LEVEL_WARNING,
|
log_message(LOG_LEVEL_WARNING,
|
||||||
"missing-args path '%s' is protected (staging directory or basis snapshot); "
|
"missing-args path '%s' is protected (staging directory or basis snapshot); "
|
||||||
@@ -472,96 +371,49 @@ static bool delete_missing_args_budgeted_observed(const Config* config, DeleteMa
|
|||||||
if (!ok)
|
if (!ok)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (owned_prefixes) {
|
delete_skips_free(&skips);
|
||||||
for (int i = 0; i < config->basis_count; i++)
|
|
||||||
free(owned_prefixes[i]);
|
|
||||||
}
|
|
||||||
free(owned_prefixes);
|
|
||||||
free(skips);
|
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Public wrappers used outside the commit path (and by unit tests): no
|
/* Public wrappers used outside the commit path (and by unit tests): no
|
||||||
--max-delete budget. */
|
--max-delete budget. */
|
||||||
bool manifest_would_delete_list(const Config* config, DeleteManifest* manifest, ArrayList* out,
|
bool manifest_would_delete_list(const Config* config, const DeleteManifest* manifest,
|
||||||
size_t* count_out) {
|
ArrayList* out, size_t* count_out) {
|
||||||
if (count_out)
|
if (count_out)
|
||||||
*count_out = 0;
|
*count_out = 0;
|
||||||
if (!config || !manifest || !manifest->keeps || !out)
|
if (!config || !manifest || !manifest->keeps || !out)
|
||||||
return false;
|
return false;
|
||||||
int skip_count = (config->delay_updates ? 1 : 0) + config->basis_count +
|
DeleteSkipSet skips;
|
||||||
(manifest->protected ? manifest->protected->size : 0);
|
if (!delete_skips_build(config, manifest->protected, NULL, true, &skips))
|
||||||
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;
|
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,
|
bool ok = delete_extras_list(config->receive_root_directory, manifest->keeps, manifest->dirs,
|
||||||
skips, used, config->protect_rules, out, count_out);
|
skips.entries, skips.count, config->protect_rules, out, count_out);
|
||||||
if (owned_prefixes) {
|
delete_skips_free(&skips);
|
||||||
for (int i = 0; i < config->basis_count; i++)
|
|
||||||
free(owned_prefixes[i]);
|
|
||||||
}
|
|
||||||
free(owned_prefixes);
|
|
||||||
free(skips);
|
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool manifest_delete_extras(const Config* config, DeleteManifest* manifest) {
|
bool manifest_delete_extras(const Config* config, const DeleteManifest* manifest) {
|
||||||
DeleteBudgetState budget = {
|
DeleteBudgetState budget = {
|
||||||
.max_delete = SIZE_MAX, .deleted = 0, .skipped = 0, .limit_hit = false};
|
.max_delete = SIZE_MAX, .deleted = 0, .skipped = 0, .limit_hit = false};
|
||||||
return delete_extras_budgeted(config, manifest, &budget);
|
return delete_extras_budgeted(config, manifest, &budget);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest) {
|
bool manifest_delete_missing_args(const Config* config, const DeleteManifest* manifest) {
|
||||||
DeleteBudgetState budget = {
|
DeleteBudgetState budget = {
|
||||||
.max_delete = SIZE_MAX, .deleted = 0, .skipped = 0, .limit_hit = false};
|
.max_delete = SIZE_MAX, .deleted = 0, .skipped = 0, .limit_hit = false};
|
||||||
return delete_missing_args_budgeted_observed(config, manifest, &budget, NULL, NULL);
|
return delete_missing_args_budgeted_observed(config, manifest, &budget, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool manifest_delete_missing_args_limited(const Config* config, DeleteManifest* manifest,
|
bool manifest_delete_missing_args_limited(const Config* config, const DeleteManifest* manifest,
|
||||||
size_t max_delete, size_t* deleted, size_t* skipped,
|
size_t max_delete, size_t* deleted, size_t* skipped,
|
||||||
bool* limit_hit) {
|
bool* limit_hit) {
|
||||||
return manifest_delete_missing_args_limited_observed(config, manifest, max_delete, deleted,
|
return manifest_delete_missing_args_limited_observed(config, manifest, max_delete, deleted,
|
||||||
skipped, limit_hit, NULL, NULL);
|
skipped, limit_hit, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool manifest_delete_missing_args_limited_observed(const Config* config, DeleteManifest* manifest,
|
bool manifest_delete_missing_args_limited_observed(
|
||||||
size_t max_delete, size_t* deleted,
|
const Config* config, const DeleteManifest* manifest, size_t max_delete, size_t* deleted,
|
||||||
size_t* skipped, bool* limit_hit,
|
size_t* skipped, bool* limit_hit, DeletePathObserver observer, void* observer_context) {
|
||||||
DeletePathObserver observer,
|
|
||||||
void* observer_context) {
|
|
||||||
DeleteBudgetState budget = {
|
DeleteBudgetState budget = {
|
||||||
.max_delete = max_delete, .deleted = 0, .skipped = 0, .limit_hit = false};
|
.max_delete = max_delete, .deleted = 0, .skipped = 0, .limit_hit = false};
|
||||||
bool ok =
|
bool ok =
|
||||||
@@ -582,17 +434,18 @@ bool manifest_delete_missing_args_limited_observed(const Config* config, DeleteM
|
|||||||
removal fail). The ordinary extras walk then runs when --delete is active.
|
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
|
Both draw from one --max-delete budget; the result reports a cap-stopped
|
||||||
(partial) commit distinctly so the client can exit 25 like rsync. */
|
(partial) commit distinctly so the client can exit 25 like rsync. */
|
||||||
DeleteCommitResult manifest_delete_all(const Config* config, DeleteManifest* manifest) {
|
DeleteCommitResult manifest_delete_all(const Config* config, const DeleteManifest* manifest) {
|
||||||
return manifest_delete_all_counted(config, manifest, NULL);
|
return manifest_delete_all_counted(config, manifest, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteCommitResult manifest_delete_all_counted(const Config* config, DeleteManifest* manifest,
|
DeleteCommitResult manifest_delete_all_counted(const Config* config, const DeleteManifest* manifest,
|
||||||
size_t* deleted) {
|
size_t* deleted) {
|
||||||
return manifest_delete_all_observed(config, manifest, deleted, NULL, NULL);
|
return manifest_delete_all_observed(config, manifest, deleted, NULL, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteCommitResult manifest_delete_all_observed(const Config* config, DeleteManifest* manifest,
|
DeleteCommitResult manifest_delete_all_observed(const Config* config,
|
||||||
size_t* deleted, DeletePathObserver observer,
|
const DeleteManifest* manifest, size_t* deleted,
|
||||||
|
DeletePathObserver observer,
|
||||||
void* observer_context) {
|
void* observer_context) {
|
||||||
if (deleted)
|
if (deleted)
|
||||||
*deleted = 0;
|
*deleted = 0;
|
||||||
|
|||||||
+14
-19
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "utils.h"
|
#include "delete.h"
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
/* Delete-commit module: delete-manifest receive plus the budgeted extras and
|
/* Delete-commit module: delete-manifest receive plus the budgeted extras and
|
||||||
@@ -44,7 +44,7 @@ DeleteManifest* receive_manifest_entries(int fd);
|
|||||||
protected-prefix skips). `--max-delete` and `--force` are honored here. The
|
protected-prefix skips). `--max-delete` and `--force` are honored here. The
|
||||||
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, const DeleteManifest* manifest);
|
||||||
/* --delete-missing-args exact-path deletions: remove each destination mirror
|
/* --delete-missing-args exact-path deletions: remove each destination mirror
|
||||||
in `manifest->missing` (never blocked by the protected prefixes, staging dir
|
in `manifest->missing` (never blocked by the protected prefixes, staging dir
|
||||||
and basis dirs excluded). A regular file/symlink is unlinked; an empty
|
and basis dirs excluded). A regular file/symlink is unlinked; an empty
|
||||||
@@ -53,22 +53,20 @@ bool manifest_delete_extras(const Config* config, DeleteManifest* manifest);
|
|||||||
parity). A missing path is a no-op. Returns false only on a genuine
|
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
|
confinement or I/O error (the run then fails); tolerated per-path cases are
|
||||||
reported and skipped. */
|
reported and skipped. */
|
||||||
bool manifest_delete_missing_args(const Config* config, DeleteManifest* manifest);
|
bool manifest_delete_missing_args(const Config* config, const DeleteManifest* manifest);
|
||||||
/* Budgeted form of manifest_delete_missing_args for the per-directory delete
|
/* Budgeted form of manifest_delete_missing_args for the per-directory delete
|
||||||
session: each removed mirror draws from `max_delete` (SIZE_MAX = unlimited)
|
session: each removed mirror draws from `max_delete` (SIZE_MAX = unlimited)
|
||||||
and the tallies are accumulated into `*deleted`/`*skipped`. `*limit_hit` is set
|
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
|
when the budget stopped the pass with entries left over. Returns false only
|
||||||
on a genuine deletion error. */
|
on a genuine deletion error. */
|
||||||
bool manifest_delete_missing_args_limited(const Config* config, DeleteManifest* manifest,
|
bool manifest_delete_missing_args_limited(const Config* config, const DeleteManifest* manifest,
|
||||||
size_t max_delete, size_t* deleted, size_t* skipped,
|
size_t max_delete, size_t* deleted, size_t* skipped,
|
||||||
bool* limit_hit);
|
bool* limit_hit);
|
||||||
/* Observer-aware form of manifest_delete_missing_args_limited: `observer` (may
|
/* Observer-aware form of manifest_delete_missing_args_limited: `observer` (may
|
||||||
be NULL) is invoked for every destination-relative path truly removed. */
|
be NULL) is invoked for every destination-relative path truly removed. */
|
||||||
bool manifest_delete_missing_args_limited_observed(const Config* config, DeleteManifest* manifest,
|
bool manifest_delete_missing_args_limited_observed(
|
||||||
size_t max_delete, size_t* deleted,
|
const Config* config, const DeleteManifest* manifest, size_t max_delete, size_t* deleted,
|
||||||
size_t* skipped, bool* limit_hit,
|
size_t* skipped, bool* limit_hit, DeletePathObserver observer, void* observer_context);
|
||||||
DeletePathObserver observer,
|
|
||||||
void* observer_context);
|
|
||||||
/* Outcome of committing a delete manifest. LIMIT_REACHED reports rsync's
|
/* Outcome of committing a delete manifest. LIMIT_REACHED reports rsync's
|
||||||
partial --max-delete result: the budget allowed some deletions and the rest
|
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). */
|
were skipped (the run still stores all file data but the client exits 25). */
|
||||||
@@ -84,15 +82,16 @@ typedef enum {
|
|||||||
share one --max-delete budget. Returns DELETE_COMMIT_OK when nothing was to
|
share one --max-delete budget. Returns DELETE_COMMIT_OK when nothing was to
|
||||||
do or everything committed, DELETE_COMMIT_LIMIT_REACHED when the budget
|
do or everything committed, DELETE_COMMIT_LIMIT_REACHED when the budget
|
||||||
stopped part of the work, or DELETE_COMMIT_ERROR on a genuine failure. */
|
stopped part of the work, or DELETE_COMMIT_ERROR on a genuine failure. */
|
||||||
DeleteCommitResult manifest_delete_all(const Config* config, DeleteManifest* manifest);
|
DeleteCommitResult manifest_delete_all(const Config* config, const DeleteManifest* manifest);
|
||||||
/* Like manifest_delete_all, but reports how many destination entries the commit
|
/* Like manifest_delete_all, but reports how many destination entries the commit
|
||||||
removed (for the end-of-transfer wire stats). `deleted` may be NULL. */
|
removed (for the end-of-transfer wire stats). `deleted` may be NULL. */
|
||||||
DeleteCommitResult manifest_delete_all_counted(const Config* config, DeleteManifest* manifest,
|
DeleteCommitResult manifest_delete_all_counted(const Config* config, const DeleteManifest* manifest,
|
||||||
size_t* deleted);
|
size_t* deleted);
|
||||||
/* Observer-aware form of manifest_delete_all_counted: `observer` (may be NULL)
|
/* Observer-aware form of manifest_delete_all_counted: `observer` (may be NULL)
|
||||||
is invoked for every destination-relative path truly removed. */
|
is invoked for every destination-relative path truly removed. */
|
||||||
DeleteCommitResult manifest_delete_all_observed(const Config* config, DeleteManifest* manifest,
|
DeleteCommitResult manifest_delete_all_observed(const Config* config,
|
||||||
size_t* deleted, DeletePathObserver observer,
|
const DeleteManifest* manifest, size_t* deleted,
|
||||||
|
DeletePathObserver observer,
|
||||||
void* observer_context);
|
void* observer_context);
|
||||||
|
|
||||||
/* -n/--dry-run --delete would-delete reporting: walk the destination exactly as
|
/* -n/--dry-run --delete would-delete reporting: walk the destination exactly as
|
||||||
@@ -100,11 +99,7 @@ DeleteCommitResult manifest_delete_all_observed(const Config* config, DeleteMani
|
|||||||
WOULD be removed to `out`, without touching disk. Uses the same staging-dir,
|
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
|
basis-dir and protected-prefix skips as the real commit. Returns true on a
|
||||||
clean walk; `*count_out` receives the number of paths appended. */
|
clean walk; `*count_out` receives the number of paths appended. */
|
||||||
bool manifest_would_delete_list(const Config* config, DeleteManifest* manifest, ArrayList* out,
|
bool manifest_would_delete_list(const Config* config, const DeleteManifest* manifest,
|
||||||
size_t* count_out);
|
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
|
#endif
|
||||||
|
|||||||
+11
-38
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include "charset.h"
|
#include "charset.h"
|
||||||
#include "delay_updates.h"
|
#include "delay_updates.h"
|
||||||
|
#include "delete.h"
|
||||||
#include "file.h"
|
#include "file.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
@@ -601,9 +602,8 @@ static int open_plan_dir(const Config* config, const char* dir) {
|
|||||||
return fd;
|
return fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef struct PlanSkips {
|
typedef struct {
|
||||||
DeleteSkipEntry* entries;
|
DeleteSkipSet set;
|
||||||
int count;
|
|
||||||
/* Receiver-side delete-protection rules received on the config frame (NULL
|
/* Receiver-side delete-protection rules received on the config frame (NULL
|
||||||
when the sender sent none). Evaluated per extra so a protect/risk rule is
|
when the sender sent none). Evaluated per extra so a protect/risk rule is
|
||||||
honored under --delete-during/--delete-delay exactly like the whole-tree
|
honored under --delete-during/--delete-delay exactly like the whole-tree
|
||||||
@@ -613,39 +613,12 @@ typedef struct PlanSkips {
|
|||||||
|
|
||||||
static bool build_plan_skips(const Config* config, const DeletePlanSession* session,
|
static bool build_plan_skips(const Config* config, const DeletePlanSession* session,
|
||||||
PlanSkips* out) {
|
PlanSkips* out) {
|
||||||
out->entries = NULL;
|
|
||||||
out->count = 0;
|
|
||||||
out->protect_rules = config->protect_rules;
|
out->protect_rules = config->protect_rules;
|
||||||
int count = (config->delay_updates ? 1 : 0) + config->basis_count +
|
/* The per-directory plan walk keeps each basis path verbatim (it does not
|
||||||
session->protected_prefixes->size + session->size_skipped->size;
|
convert an absolute under-root path to its root-relative form, unlike the
|
||||||
if (count == 0)
|
whole-tree commit walk). */
|
||||||
return true;
|
return delete_skips_build(config, session->protected_prefixes, session->size_skipped, false,
|
||||||
out->entries = calloc((size_t)count, sizeof(DeleteSkipEntry));
|
&out->set);
|
||||||
if (!out->entries)
|
|
||||||
return false;
|
|
||||||
int idx = 0;
|
|
||||||
if (config->delay_updates) {
|
|
||||||
out->entries[idx].prefix = DELAY_UPDATES_STAGING_DIR;
|
|
||||||
out->entries[idx].top_level_only = true;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < config->basis_count; i++) {
|
|
||||||
out->entries[idx].prefix = config->basis_dirs[i].path;
|
|
||||||
out->entries[idx].top_level_only = false;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < session->protected_prefixes->size; i++) {
|
|
||||||
out->entries[idx].prefix = (const char*)session->protected_prefixes->items[i];
|
|
||||||
out->entries[idx].top_level_only = false;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < session->size_skipped->size; i++) {
|
|
||||||
out->entries[idx].prefix = (const char*)session->size_skipped->items[i];
|
|
||||||
out->entries[idx].top_level_only = false;
|
|
||||||
idx++;
|
|
||||||
}
|
|
||||||
out->count = idx;
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool budget_available(const DeletePlanSession* session) {
|
static bool budget_available(const DeletePlanSession* session) {
|
||||||
@@ -796,7 +769,7 @@ static bool process_children(int dirfd, const char* dir_rel, const ArrayList* ke
|
|||||||
operation_ok = false;
|
operation_ok = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (path_under_skip_prefix(child_rel, at_root, skips->entries, skips->count)) {
|
if (path_under_skip_prefix(child_rel, at_root, skips->set.entries, skips->set.count)) {
|
||||||
shielded[i] = true;
|
shielded[i] = true;
|
||||||
local_survives = true;
|
local_survives = true;
|
||||||
free(child_rel);
|
free(child_rel);
|
||||||
@@ -883,7 +856,7 @@ static bool apply_plan_dir(DeletePlanSession* session, const Config* config, con
|
|||||||
bool survives = false;
|
bool survives = false;
|
||||||
bool ok = process_children(dirfd, dir, dirs, files, strcmp(dir, ".") == 0, false, &skips, session,
|
bool ok = process_children(dirfd, dir, dirs, files, strcmp(dir, ".") == 0, false, &skips, session,
|
||||||
&survives);
|
&survives);
|
||||||
free(skips.entries);
|
delete_skips_free(&skips.set);
|
||||||
close(dirfd);
|
close(dirfd);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
log_message(LOG_LEVEL_ERROR, "deletion failed while removing extraneous files");
|
log_message(LOG_LEVEL_ERROR, "deletion failed while removing extraneous files");
|
||||||
@@ -1024,7 +997,7 @@ static bool apply_deferred_path(DeletePlanSession* session, const Config* config
|
|||||||
}
|
}
|
||||||
bool survives = false;
|
bool survives = false;
|
||||||
bool ok = process_children(dirfd, rel, NULL, NULL, false, true, &skips, session, &survives);
|
bool ok = process_children(dirfd, rel, NULL, NULL, false, true, &skips, session, &survives);
|
||||||
free(skips.entries);
|
delete_skips_free(&skips.set);
|
||||||
close(dirfd);
|
close(dirfd);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
close(parent_fd);
|
close(parent_fd);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "array_list.h"
|
#include "array_list.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
#include "delete.h"
|
||||||
#include "file_receive.h"
|
#include "file_receive.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ bool file_send_sendfile_with_skip(File* file, int file_descriptor, bool use_meta
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
protocol_note_bytes_written((unsigned long long)sent);
|
protocol_note_bytes_written((unsigned long long)sent);
|
||||||
protocol_throttle_bytes((size_t)sent);
|
protocol_throttle_bytes(file_descriptor, (size_t)sent);
|
||||||
}
|
}
|
||||||
|
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|||||||
@@ -302,11 +302,14 @@ static ProtocolSession* legacy_session(int read_fd, int write_fd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Pace an out-of-band write that bypassed protocol_send_n_data (the plaintext
|
/* Pace an out-of-band write that bypassed protocol_send_n_data (the plaintext
|
||||||
* sendfile fast path). The bound/legacy session is resolved exactly as
|
* sendfile fast path). The bound/legacy session is resolved exactly as the
|
||||||
* send_n_data resolves it, so the same token-bucket state is throttled and the
|
* preceding send_n_data(fd, ...) resolved it, so the same token-bucket state is
|
||||||
* TLS and plaintext transports share identical --bwlimit semantics. */
|
* throttled and the TLS and plaintext transports share identical --bwlimit
|
||||||
void protocol_throttle_bytes(size_t bytes) {
|
* semantics. Passing the wire fd (rather than -1) is essential: the sendfile
|
||||||
bw_throttle_session(legacy_session(-1, -1), bytes);
|
* send left legacy_io_session.write_fd bound to it, so resolving with -1 would
|
||||||
|
* mismatch, re-initialize the session and hand out a second first-call burst. */
|
||||||
|
void protocol_throttle_bytes(int file_descriptor, size_t bytes) {
|
||||||
|
bw_throttle_session(legacy_session(-1, file_descriptor), bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
|
bool send_n_data(int file_descriptor, const void* data, size_t data_size) {
|
||||||
|
|||||||
@@ -225,11 +225,14 @@ unsigned long long protocol_bytes_written(void);
|
|||||||
unsigned long long protocol_bytes_read(void);
|
unsigned long long protocol_bytes_read(void);
|
||||||
void protocol_note_bytes_written(unsigned long long bytes);
|
void protocol_note_bytes_written(unsigned long long bytes);
|
||||||
/* Apply --bwlimit pacing to bytes written outside protocol_send_n_data (the
|
/* Apply --bwlimit pacing to bytes written outside protocol_send_n_data (the
|
||||||
* plaintext zero-copy sendfile fast path). Resolves the bound/legacy session
|
* plaintext zero-copy sendfile fast path). `file_descriptor` is the wire fd
|
||||||
* exactly as send_n_data does and runs the same token-bucket throttle, so the
|
* the bytes were written to, so the legacy session is resolved exactly as the
|
||||||
* sendfile transport is paced identically to the buffered/TLS paths. A no-op
|
* preceding send_n_data call resolved it (the bound TLS session still wins when
|
||||||
* when the effective session has no bandwidth limit. */
|
* set); resolving with the same fd avoids re-initializing the legacy session
|
||||||
void protocol_throttle_bytes(size_t bytes);
|
* and granting a second first-call burst. Runs the same token-bucket throttle,
|
||||||
|
* so the sendfile transport is paced identically to the buffered/TLS paths. A
|
||||||
|
* no-op when the effective session has no bandwidth limit. */
|
||||||
|
void protocol_throttle_bytes(int file_descriptor, size_t bytes);
|
||||||
|
|
||||||
void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd);
|
void protocol_session_init(ProtocolSession* session, int read_fd, int write_fd);
|
||||||
/* Transitional bridge for helpers whose signatures still carry only an fd. */
|
/* Transitional bridge for helpers whose signatures still carry only an fd. */
|
||||||
|
|||||||
@@ -625,641 +625,6 @@ bool format_human_bytes(unsigned long long bytes, char* buffer, size_t buffer_si
|
|||||||
return written >= 0 && (size_t)written < buffer_size;
|
return written >= 0 && (size_t)written < buffer_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Build the keep-set index from the exact manifest entries only. A lookup of
|
|
||||||
`rel` succeeds iff `rel` is a kept entry, a kept directory, or an ancestor
|
|
||||||
directory of kept content (the old is_dir_in_manifest predicate); the sorted
|
|
||||||
view answers "is an ancestor of kept content" without materializing any
|
|
||||||
per-component prefix copy, so the index is O(manifest size) memory. */
|
|
||||||
static bool build_keep_index(const ArrayList* manifest, PathIndex* index) {
|
|
||||||
if (!manifest || manifest->size <= 0)
|
|
||||||
return path_index_build(index, NULL, 0);
|
|
||||||
return path_index_build(index, (const char* const*)manifest->items, (size_t)manifest->size);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool keep_is_dir(const PathIndex* index, const char* rel_path) {
|
|
||||||
return path_index_contains(index, rel_path) || path_index_has_descendant(index, rel_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool keep_is_file(const PathIndex* index, const char* rel_path) {
|
|
||||||
return path_index_contains(index, rel_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* True when child_rel is, or lies below, a protected entry. A prefix "a"
|
|
||||||
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
|
|
||||||
directories that share such a name stay ordinary destination content. */
|
|
||||||
bool path_under_skip_prefix(const char* child_rel, bool at_root, const DeleteSkipEntry* skips,
|
|
||||||
int skip_count) {
|
|
||||||
for (int i = 0; i < skip_count; i++) {
|
|
||||||
if (skips[i].top_level_only && !at_root)
|
|
||||||
continue;
|
|
||||||
size_t prefix_len = strlen(skips[i].prefix);
|
|
||||||
if (strncmp(child_rel, skips[i].prefix, prefix_len) == 0 &&
|
|
||||||
(child_rel[prefix_len] == '\0' || child_rel[prefix_len] == '/'))
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Per-run deletion budget and tallies. `max_delete` is the cap on the number
|
|
||||||
of entries the walker may remove (SIZE_MAX = unlimited); once it is reached
|
|
||||||
the remaining extras are counted in `skipped` and left in place, matching
|
|
||||||
rsync's partial --max-delete behavior. */
|
|
||||||
typedef struct {
|
|
||||||
size_t max_delete;
|
|
||||||
size_t deleted;
|
|
||||||
size_t skipped;
|
|
||||||
bool limit_hit;
|
|
||||||
} DeleteBudget;
|
|
||||||
|
|
||||||
/* True when direct children of the directory named by `rel` may be removed.
|
|
||||||
With no synchronization info (dirs == NULL) the whole tree is deletable; when
|
|
||||||
a dirs index is supplied only its exact entries are (the receive root is the
|
|
||||||
"." sentinel). */
|
|
||||||
static bool is_synced_dir(const PathIndex* dirs, const char* rel) {
|
|
||||||
if (!dirs)
|
|
||||||
return true;
|
|
||||||
return path_index_contains(dirs, rel[0] == '\0' ? "." : rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Unsigned byte-wise string compare, matching rsync's u_strcmp (a signed
|
|
||||||
strcmp would order bytes >= 0x80 differently). */
|
|
||||||
static int delete_name_cmp(const char* a, const char* b) {
|
|
||||||
const unsigned char* pa = (const unsigned char*)a;
|
|
||||||
const unsigned char* pb = (const unsigned char*)b;
|
|
||||||
while (*pa != '\0' && *pa == *pb) {
|
|
||||||
pa++;
|
|
||||||
pb++;
|
|
||||||
}
|
|
||||||
return (int)*pa - (int)*pb;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool delete_dir_entries_collect(int dirfd, DeleteDirEntry** out, size_t* count,
|
|
||||||
bool* operation_ok) {
|
|
||||||
*out = NULL;
|
|
||||||
*count = 0;
|
|
||||||
if (operation_ok)
|
|
||||||
*operation_ok = true;
|
|
||||||
int scanfd = openat(dirfd, ".", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
||||||
if (scanfd < 0)
|
|
||||||
return false;
|
|
||||||
DIR* dir = fdopendir(scanfd);
|
|
||||||
if (!dir) {
|
|
||||||
close(scanfd);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
DeleteDirEntry* entries = NULL;
|
|
||||||
size_t used = 0;
|
|
||||||
size_t capacity = 0;
|
|
||||||
bool ok = true;
|
|
||||||
const struct dirent* entry;
|
|
||||||
while ((entry = readdir(dir)) != NULL) {
|
|
||||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
|
||||||
continue;
|
|
||||||
struct stat st;
|
|
||||||
if (fstatat(dirfd, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
|
|
||||||
if (errno != ENOENT && operation_ok)
|
|
||||||
*operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (used == capacity) {
|
|
||||||
size_t next = capacity == 0 ? 16 : capacity * 2;
|
|
||||||
DeleteDirEntry* grown = realloc(entries, next * sizeof(*grown));
|
|
||||||
if (!grown) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
entries = grown;
|
|
||||||
capacity = next;
|
|
||||||
}
|
|
||||||
entries[used].name = str_dup(entry->d_name);
|
|
||||||
if (!entries[used].name) {
|
|
||||||
ok = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
entries[used].is_dir = S_ISDIR(st.st_mode);
|
|
||||||
used++;
|
|
||||||
}
|
|
||||||
closedir(dir);
|
|
||||||
if (!ok) {
|
|
||||||
delete_dir_entries_free(entries, used);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
*out = entries;
|
|
||||||
*count = used;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void delete_dir_entries_free(DeleteDirEntry* entries, size_t count) {
|
|
||||||
if (!entries)
|
|
||||||
return;
|
|
||||||
for (size_t i = 0; i < count; i++)
|
|
||||||
free(entries[i].name);
|
|
||||||
free(entries);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* rsync's extraneous-entry order: subdirectories before files, each group in
|
|
||||||
descending name order. */
|
|
||||||
int delete_dir_entry_cmp_desc(const void* a, const void* b) {
|
|
||||||
const DeleteDirEntry* ea = a;
|
|
||||||
const DeleteDirEntry* eb = b;
|
|
||||||
if (ea->is_dir != eb->is_dir)
|
|
||||||
return ea->is_dir ? -1 : 1;
|
|
||||||
return -delete_name_cmp(ea->name, eb->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* rsync's kept-subdirectory order: plain ascending name. */
|
|
||||||
int delete_dir_entry_cmp_asc(const void* a, const void* b) {
|
|
||||||
const DeleteDirEntry* ea = a;
|
|
||||||
const DeleteDirEntry* eb = b;
|
|
||||||
return delete_name_cmp(ea->name, eb->name);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove the extras directly inside the directory open on `dirfd`, recursing
|
|
||||||
into every child directory so kept content below a synchronized prefix is
|
|
||||||
reached. `all_removed` reports whether every child entry was removed (so the
|
|
||||||
caller may rmdir this directory). A child directory is never removed when it
|
|
||||||
is itself a synchronized directory or holds kept content; with a dirs index
|
|
||||||
supplied, direct children of a non-synchronized directory are never extras at
|
|
||||||
all (they are left in place but still descended into). Symlinks are unlinked
|
|
||||||
like any other non-directory extra (never followed).
|
|
||||||
|
|
||||||
Entries are processed in rsync's order (extraneous subdirectories in
|
|
||||||
descending name order, then extraneous files, then kept subdirectories in
|
|
||||||
ascending order) rather than readdir() order, so `--max-delete` leaves the
|
|
||||||
same survivors and the `--info=del`/dry-run line order matches rsync. */
|
|
||||||
static bool delete_extras_fd(int dirfd, const char* rel_path, const PathIndex* keep,
|
|
||||||
const PathIndex* dirs, DeleteBudget* budget,
|
|
||||||
const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules, bool parent_deletable,
|
|
||||||
bool* all_removed, DeletePathObserver observer,
|
|
||||||
void* observer_context) {
|
|
||||||
DeleteDirEntry* entries = NULL;
|
|
||||||
size_t count = 0;
|
|
||||||
bool collect_ok = true;
|
|
||||||
if (!delete_dir_entries_collect(dirfd, &entries, &count, &collect_ok))
|
|
||||||
return false;
|
|
||||||
bool operation_ok = collect_ok;
|
|
||||||
bool local_survives = false;
|
|
||||||
bool* shielded = calloc(count ? count : 1, sizeof(bool));
|
|
||||||
bool* is_extra = calloc(count ? count : 1, sizeof(bool));
|
|
||||||
if (!shielded || !is_extra) {
|
|
||||||
free(shielded);
|
|
||||||
free(is_extra);
|
|
||||||
delete_dir_entries_free(entries, count);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
/* A directory is deletable when it or ANY ancestor is synchronized; the
|
|
||||||
`parent_deletable` flag carries that down the recursion so dest-only
|
|
||||||
directories below a synchronized root are removed wholesale. */
|
|
||||||
bool deletable = parent_deletable || is_synced_dir(dirs, rel_path);
|
|
||||||
bool at_root = rel_path[0] == '\0';
|
|
||||||
|
|
||||||
/* Reproduce rsync's traversal order: extraneous subdirectories in descending
|
|
||||||
name order, then extraneous files in descending name order, and kept
|
|
||||||
subdirectories only afterwards (ascending). Sorting up front also fixes the
|
|
||||||
identity of the survivors under a partial --max-delete. */
|
|
||||||
if (count > 1)
|
|
||||||
qsort(entries, count, sizeof(*entries), delete_dir_entry_cmp_desc);
|
|
||||||
size_t dir_count = 0;
|
|
||||||
while (dir_count < count && entries[dir_count].is_dir)
|
|
||||||
dir_count++;
|
|
||||||
|
|
||||||
/* Classify every entry up front (the verdict does not depend on processing
|
|
||||||
order) so the ordered passes below can act on it. */
|
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (!child_rel) {
|
|
||||||
operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
/* A --delay-updates run keeps its staging directory as a direct child of
|
|
||||||
the receive root, and basis-dir snapshots live below it too. Their
|
|
||||||
contents are not manifest entries, so descending into them would delete
|
|
||||||
every staged / basis file as an "extra". Only the staging name (a
|
|
||||||
top-level-only prefix) and the basis prefixes are protected: a nested
|
|
||||||
destination directory that happens to be called .fastsync-stage is
|
|
||||||
ordinary content. */
|
|
||||||
if (path_under_skip_prefix(child_rel, at_root, skips, skip_count)) {
|
|
||||||
shielded[i] = true;
|
|
||||||
local_survives = true;
|
|
||||||
} else if (protect_rules &&
|
|
||||||
filter_rules_apply_side(protect_rules, child_rel, entries[i].name, entries[i].is_dir,
|
|
||||||
FILTER_SIDE_RECEIVER) == FILTER_ACTION_PROTECT) {
|
|
||||||
/* A first-match protect rule shields the extra; for a directory the whole
|
|
||||||
subtree is shielded (rsync prunes an excluded directory), so do not
|
|
||||||
descend. */
|
|
||||||
shielded[i] = true;
|
|
||||||
local_survives = true;
|
|
||||||
} else if (entries[i].is_dir) {
|
|
||||||
bool child_synced = dirs && path_index_contains(dirs, child_rel);
|
|
||||||
is_extra[i] = deletable && !child_synced && !keep_is_dir(keep, child_rel);
|
|
||||||
if (!is_extra[i])
|
|
||||||
local_survives = true;
|
|
||||||
} else {
|
|
||||||
is_extra[i] = deletable && !keep_is_file(keep, child_rel);
|
|
||||||
if (!is_extra[i])
|
|
||||||
local_survives = true;
|
|
||||||
}
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pass 1: extraneous subdirectories, descending. */
|
|
||||||
for (size_t i = 0; i < dir_count; i++) {
|
|
||||||
if (!is_extra[i])
|
|
||||||
continue;
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (!child_rel) {
|
|
||||||
operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
||||||
bool child_all_removed = false;
|
|
||||||
if (childfd >= 0) {
|
|
||||||
if (!delete_extras_fd(childfd, child_rel, keep, dirs, budget, skips, skip_count,
|
|
||||||
protect_rules, deletable, &child_all_removed, observer,
|
|
||||||
observer_context))
|
|
||||||
operation_ok = false;
|
|
||||||
close(childfd);
|
|
||||||
} else if (errno != ENOENT) {
|
|
||||||
operation_ok = false;
|
|
||||||
}
|
|
||||||
if (child_all_removed && deletable) {
|
|
||||||
if (budget->deleted >= budget->max_delete) {
|
|
||||||
budget->limit_hit = true;
|
|
||||||
budget->skipped++;
|
|
||||||
local_survives = true;
|
|
||||||
} else if (unlinkat(dirfd, entries[i].name, AT_REMOVEDIR) != 0) {
|
|
||||||
/* ENOENT: already gone (fine). ENOTEMPTY/EEXIST: the directory still
|
|
||||||
holds entries the walker leaves in place (a protected excluded
|
|
||||||
prefix, a kept file the manifest protects, a symlink); rsync leaves
|
|
||||||
such a directory behind, so this is not an error. Only genuine I/O
|
|
||||||
failures abort the deletion. */
|
|
||||||
if (errno != ENOENT && errno != ENOTEMPTY && errno != EEXIST)
|
|
||||||
operation_ok = false;
|
|
||||||
local_survives = true;
|
|
||||||
} else {
|
|
||||||
budget->deleted++;
|
|
||||||
/* rsync reports a removed directory with a trailing slash. */
|
|
||||||
if (observer) {
|
|
||||||
size_t len = strlen(child_rel);
|
|
||||||
char* with_slash = malloc(len + 2);
|
|
||||||
if (with_slash) {
|
|
||||||
memcpy(with_slash, child_rel, len);
|
|
||||||
with_slash[len] = '/';
|
|
||||||
with_slash[len + 1] = '\0';
|
|
||||||
observer(observer_context, with_slash);
|
|
||||||
free(with_slash);
|
|
||||||
} else {
|
|
||||||
observer(observer_context, child_rel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
local_survives = true;
|
|
||||||
}
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pass 2: extraneous files, descending. */
|
|
||||||
for (size_t i = dir_count; i < count; i++) {
|
|
||||||
if (!is_extra[i])
|
|
||||||
continue;
|
|
||||||
if (budget->deleted >= budget->max_delete) {
|
|
||||||
budget->limit_hit = true;
|
|
||||||
budget->skipped++;
|
|
||||||
local_survives = true;
|
|
||||||
} else if (unlinkat(dirfd, entries[i].name, 0) != 0) {
|
|
||||||
if (errno != ENOENT)
|
|
||||||
operation_ok = false;
|
|
||||||
local_survives = true;
|
|
||||||
} else {
|
|
||||||
budget->deleted++;
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (child_rel) {
|
|
||||||
if (observer)
|
|
||||||
observer(observer_context, child_rel);
|
|
||||||
char* escaped_path = output_escape(child_rel, log_get_8_bit_output());
|
|
||||||
fprintf(stderr, " Deleted: %s\n", escaped_path ? escaped_path : "<allocation failed>");
|
|
||||||
free(escaped_path);
|
|
||||||
}
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pass 3: kept subdirectories, ascending (rsync descends into these only
|
|
||||||
after the parent's own extras have been handled). */
|
|
||||||
for (size_t i = dir_count; i-- > 0;) {
|
|
||||||
if (is_extra[i] || shielded[i])
|
|
||||||
continue;
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (!child_rel) {
|
|
||||||
operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
||||||
bool child_all_removed = false;
|
|
||||||
if (childfd >= 0) {
|
|
||||||
if (!delete_extras_fd(childfd, child_rel, keep, dirs, budget, skips, skip_count,
|
|
||||||
protect_rules, deletable, &child_all_removed, observer,
|
|
||||||
observer_context))
|
|
||||||
operation_ok = false;
|
|
||||||
close(childfd);
|
|
||||||
} else if (errno != ENOENT) {
|
|
||||||
operation_ok = false;
|
|
||||||
}
|
|
||||||
/* A kept/synchronized directory is never removed. */
|
|
||||||
local_survives = true;
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(shielded);
|
|
||||||
free(is_extra);
|
|
||||||
delete_dir_entries_free(entries, count);
|
|
||||||
*all_removed = !local_survives;
|
|
||||||
return operation_ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Read-only mirror of delete_extras_fd: records the paths that WOULD be removed
|
|
||||||
without unlinking anything. A child directory is reported after its own
|
|
||||||
reportable children (depth-first), matching the delete pass's ordering. */
|
|
||||||
static bool list_extras_fd(int dirfd, const char* rel_path, const PathIndex* keep,
|
|
||||||
const PathIndex* dirs, ArrayList* out, size_t* recorded,
|
|
||||||
const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules, bool parent_deletable,
|
|
||||||
bool* all_removed) {
|
|
||||||
DeleteDirEntry* entries = NULL;
|
|
||||||
size_t count = 0;
|
|
||||||
bool collect_ok = true;
|
|
||||||
if (!delete_dir_entries_collect(dirfd, &entries, &count, &collect_ok))
|
|
||||||
return false;
|
|
||||||
bool operation_ok = collect_ok;
|
|
||||||
bool local_survives = false;
|
|
||||||
bool* shielded = calloc(count ? count : 1, sizeof(bool));
|
|
||||||
bool* is_extra = calloc(count ? count : 1, sizeof(bool));
|
|
||||||
if (!shielded || !is_extra) {
|
|
||||||
free(shielded);
|
|
||||||
free(is_extra);
|
|
||||||
delete_dir_entries_free(entries, count);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bool deletable = parent_deletable || is_synced_dir(dirs, rel_path);
|
|
||||||
bool at_root = rel_path[0] == '\0';
|
|
||||||
|
|
||||||
/* Mirror the delete walk's rsync order (extraneous subdirectories descending,
|
|
||||||
then extraneous files descending, then kept subdirectories ascending). */
|
|
||||||
if (count > 1)
|
|
||||||
qsort(entries, count, sizeof(*entries), delete_dir_entry_cmp_desc);
|
|
||||||
size_t dir_count = 0;
|
|
||||||
while (dir_count < count && entries[dir_count].is_dir)
|
|
||||||
dir_count++;
|
|
||||||
|
|
||||||
for (size_t i = 0; i < count; i++) {
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (!child_rel) {
|
|
||||||
operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (path_under_skip_prefix(child_rel, at_root, skips, skip_count)) {
|
|
||||||
shielded[i] = true;
|
|
||||||
local_survives = true;
|
|
||||||
} else if (protect_rules &&
|
|
||||||
filter_rules_apply_side(protect_rules, child_rel, entries[i].name, entries[i].is_dir,
|
|
||||||
FILTER_SIDE_RECEIVER) == FILTER_ACTION_PROTECT) {
|
|
||||||
/* Mirror the delete walk: a protected entry is never reported as a
|
|
||||||
would-delete and a protected directory's subtree is not enumerated. */
|
|
||||||
shielded[i] = true;
|
|
||||||
local_survives = true;
|
|
||||||
} else if (entries[i].is_dir) {
|
|
||||||
bool child_synced = dirs && path_index_contains(dirs, child_rel);
|
|
||||||
is_extra[i] = deletable && !child_synced && !keep_is_dir(keep, child_rel);
|
|
||||||
if (!is_extra[i])
|
|
||||||
local_survives = true;
|
|
||||||
} else {
|
|
||||||
is_extra[i] = deletable && !keep_is_file(keep, child_rel);
|
|
||||||
if (!is_extra[i])
|
|
||||||
local_survives = true;
|
|
||||||
}
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pass 1: extraneous subdirectories, descending (recorded after contents). */
|
|
||||||
for (size_t i = 0; i < dir_count; i++) {
|
|
||||||
if (!is_extra[i])
|
|
||||||
continue;
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (!child_rel) {
|
|
||||||
operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
||||||
bool child_all_removed = false;
|
|
||||||
if (childfd >= 0) {
|
|
||||||
if (!list_extras_fd(childfd, child_rel, keep, dirs, out, recorded, skips, skip_count,
|
|
||||||
protect_rules, deletable, &child_all_removed))
|
|
||||||
operation_ok = false;
|
|
||||||
close(childfd);
|
|
||||||
} else if (errno != ENOENT) {
|
|
||||||
operation_ok = false;
|
|
||||||
}
|
|
||||||
if (child_all_removed && deletable) {
|
|
||||||
size_t len = strlen(child_rel);
|
|
||||||
char* copy = malloc(len + 2);
|
|
||||||
if (!copy) {
|
|
||||||
operation_ok = false;
|
|
||||||
} else {
|
|
||||||
memcpy(copy, child_rel, len);
|
|
||||||
copy[len] = '/';
|
|
||||||
copy[len + 1] = '\0';
|
|
||||||
if (!array_list_add(out, copy)) {
|
|
||||||
free(copy);
|
|
||||||
operation_ok = false;
|
|
||||||
} else {
|
|
||||||
(*recorded)++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
local_survives = true;
|
|
||||||
}
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pass 2: extraneous files, descending. */
|
|
||||||
for (size_t i = dir_count; i < count; i++) {
|
|
||||||
if (!is_extra[i])
|
|
||||||
continue;
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (!child_rel) {
|
|
||||||
operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
char* copy = str_dup(child_rel);
|
|
||||||
if (!copy || !array_list_add(out, copy)) {
|
|
||||||
free(copy);
|
|
||||||
operation_ok = false;
|
|
||||||
} else {
|
|
||||||
(*recorded)++;
|
|
||||||
}
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Pass 3: kept subdirectories, ascending. */
|
|
||||||
for (size_t i = dir_count; i-- > 0;) {
|
|
||||||
if (is_extra[i] || shielded[i])
|
|
||||||
continue;
|
|
||||||
char* child_rel = path_cat((char*)rel_path, entries[i].name);
|
|
||||||
if (!child_rel) {
|
|
||||||
operation_ok = false;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
int childfd = openat(dirfd, entries[i].name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
||||||
bool child_all_removed = false;
|
|
||||||
if (childfd >= 0) {
|
|
||||||
if (!list_extras_fd(childfd, child_rel, keep, dirs, out, recorded, skips, skip_count,
|
|
||||||
protect_rules, deletable, &child_all_removed))
|
|
||||||
operation_ok = false;
|
|
||||||
close(childfd);
|
|
||||||
} else if (errno != ENOENT) {
|
|
||||||
operation_ok = false;
|
|
||||||
}
|
|
||||||
local_survives = true;
|
|
||||||
free(child_rel);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(shielded);
|
|
||||||
free(is_extra);
|
|
||||||
delete_dir_entries_free(entries, count);
|
|
||||||
*all_removed = !local_survives;
|
|
||||||
return operation_ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
|
|
||||||
const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules, ArrayList* out, size_t* count_out) {
|
|
||||||
if (count_out)
|
|
||||||
*count_out = 0;
|
|
||||||
if (!manifest || !out)
|
|
||||||
return false;
|
|
||||||
PathIndex keep;
|
|
||||||
if (!build_keep_index(manifest, &keep))
|
|
||||||
return false;
|
|
||||||
PathIndex dirs;
|
|
||||||
bool have_dirs = synced_dirs != NULL;
|
|
||||||
if (have_dirs &&
|
|
||||||
!path_index_build(&dirs, (const char* const*)synced_dirs->items, (size_t)synced_dirs->size)) {
|
|
||||||
path_index_free(&keep);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
int rootfd;
|
|
||||||
int root_fd = utils_get_authorized_root_fd();
|
|
||||||
if (root_fd >= 0) {
|
|
||||||
if (utils_get_authorized_root_path())
|
|
||||||
rootfd = utils_open_authorized_destination(dest_root);
|
|
||||||
else if (dest_root == NULL)
|
|
||||||
rootfd = dup(root_fd);
|
|
||||||
else
|
|
||||||
rootfd = -1;
|
|
||||||
} else {
|
|
||||||
rootfd = open(dest_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
||||||
}
|
|
||||||
if (rootfd < 0) {
|
|
||||||
path_index_free(&keep);
|
|
||||||
if (have_dirs)
|
|
||||||
path_index_free(&dirs);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bool all_removed = false;
|
|
||||||
size_t recorded = 0;
|
|
||||||
bool ok = list_extras_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, out, &recorded, skips,
|
|
||||||
skip_count, protect_rules, false, &all_removed);
|
|
||||||
if (close(rootfd) != 0)
|
|
||||||
ok = false;
|
|
||||||
path_index_free(&keep);
|
|
||||||
if (have_dirs)
|
|
||||||
path_index_free(&dirs);
|
|
||||||
if (count_out)
|
|
||||||
*count_out = recorded;
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest,
|
|
||||||
const ArrayList* synced_dirs, size_t max_delete,
|
|
||||||
const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules,
|
|
||||||
size_t* deleted_out, size_t* skipped_out,
|
|
||||||
DeletePathObserver observer,
|
|
||||||
void* observer_context) {
|
|
||||||
if (deleted_out)
|
|
||||||
*deleted_out = 0;
|
|
||||||
if (skipped_out)
|
|
||||||
*skipped_out = 0;
|
|
||||||
if (!manifest)
|
|
||||||
return DELETE_WALK_ERROR;
|
|
||||||
/* Index the keep-set (and the synchronized-dir set, when supplied) once so
|
|
||||||
membership is answered in O(path length) instead of scanning every entry
|
|
||||||
for every destination entry. */
|
|
||||||
PathIndex keep;
|
|
||||||
if (!build_keep_index(manifest, &keep))
|
|
||||||
return DELETE_WALK_ERROR;
|
|
||||||
PathIndex dirs;
|
|
||||||
bool have_dirs = synced_dirs != NULL;
|
|
||||||
if (have_dirs &&
|
|
||||||
!path_index_build(&dirs, (const char* const*)synced_dirs->items, (size_t)synced_dirs->size)) {
|
|
||||||
path_index_free(&keep);
|
|
||||||
return DELETE_WALK_ERROR;
|
|
||||||
}
|
|
||||||
int rootfd;
|
|
||||||
int root_fd = utils_get_authorized_root_fd();
|
|
||||||
if (root_fd >= 0) {
|
|
||||||
if (utils_get_authorized_root_path())
|
|
||||||
rootfd = utils_open_authorized_destination(dest_root);
|
|
||||||
else if (dest_root == NULL)
|
|
||||||
rootfd = dup(root_fd);
|
|
||||||
else
|
|
||||||
rootfd = -1;
|
|
||||||
} else {
|
|
||||||
rootfd = open(dest_root, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
|
||||||
}
|
|
||||||
if (rootfd < 0) {
|
|
||||||
path_index_free(&keep);
|
|
||||||
if (have_dirs)
|
|
||||||
path_index_free(&dirs);
|
|
||||||
return DELETE_WALK_ERROR;
|
|
||||||
}
|
|
||||||
DeleteBudget budget = {.max_delete = max_delete, .deleted = 0, .skipped = 0, .limit_hit = false};
|
|
||||||
bool all_removed = false;
|
|
||||||
bool ok =
|
|
||||||
delete_extras_fd(rootfd, "", &keep, have_dirs ? &dirs : NULL, &budget, skips, skip_count,
|
|
||||||
protect_rules, false, &all_removed, observer, observer_context);
|
|
||||||
if (close(rootfd) != 0)
|
|
||||||
ok = false;
|
|
||||||
path_index_free(&keep);
|
|
||||||
if (have_dirs)
|
|
||||||
path_index_free(&dirs);
|
|
||||||
if (deleted_out)
|
|
||||||
*deleted_out = budget.deleted;
|
|
||||||
if (skipped_out)
|
|
||||||
*skipped_out = budget.skipped;
|
|
||||||
if (!ok)
|
|
||||||
return DELETE_WALK_ERROR;
|
|
||||||
return budget.limit_hit ? DELETE_WALK_LIMIT_REACHED : DELETE_WALK_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest,
|
|
||||||
const ArrayList* synced_dirs, size_t max_delete,
|
|
||||||
const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules, size_t* deleted_out,
|
|
||||||
size_t* skipped_out) {
|
|
||||||
return delete_extras_limited_observed(dest_root, manifest, synced_dirs, max_delete, skips,
|
|
||||||
skip_count, protect_rules, deleted_out, skipped_out, NULL,
|
|
||||||
NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool delete_extras(const char* dest_root, const ArrayList* manifest) {
|
|
||||||
return delete_extras_limited(dest_root, manifest, NULL, SIZE_MAX, NULL, 0, NULL, NULL, NULL) ==
|
|
||||||
DELETE_WALK_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool has_path_traversal(const char* path) {
|
bool has_path_traversal(const char* path) {
|
||||||
if (!path)
|
if (!path)
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -106,101 +106,7 @@ int env_choice_first(const char* env_name, int (*resolve)(const char*), bool* sp
|
|||||||
ssize_t utils_getdelim_bounded(FILE* stream, char** line, size_t* cap, int delim, size_t max_len);
|
ssize_t utils_getdelim_bounded(FILE* stream, char** line, size_t* cap, int delim, size_t max_len);
|
||||||
char* path_cat(const char* path1, const char* path2);
|
char* path_cat(const char* path1, const char* path2);
|
||||||
bool glob_match(const char* pattern, const char* str);
|
bool glob_match(const char* pattern, const char* str);
|
||||||
/* Result of a bounded extra-file deletion run. */
|
|
||||||
typedef enum {
|
|
||||||
/* Every extra entry was removed (or there were none). */
|
|
||||||
DELETE_WALK_OK = 0,
|
|
||||||
/* The numeric cap for this run was reached before every extra was removed.
|
|
||||||
The walker removed exactly the entries the cap allowed and skipped (without
|
|
||||||
removing) the rest, matching rsync's partial --max-delete behavior. */
|
|
||||||
DELETE_WALK_LIMIT_REACHED,
|
|
||||||
/* A traversal or unlink failure aborted the deletion (partial removal is
|
|
||||||
possible, mirroring the delete pass). */
|
|
||||||
DELETE_WALK_ERROR
|
|
||||||
} DeleteWalkResult;
|
|
||||||
/* One protected entry for the delete walker. When top_level_only is true the
|
|
||||||
prefix is skipped only as a DIRECT child of dest_root (the --delay-updates
|
|
||||||
staging directory, which must not hide genuine extras inside a nested
|
|
||||||
destination directory that happens to share the staging name); otherwise the
|
|
||||||
prefix is skipped at any depth (the --compare-dest/--copy-dest/--link-dest
|
|
||||||
basis trees, and the sender-side protected filter-excluded prefixes, which
|
|
||||||
are never destination content). */
|
|
||||||
typedef struct {
|
|
||||||
const char* prefix;
|
|
||||||
bool top_level_only;
|
|
||||||
} 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);
|
|
||||||
/* One destination-directory entry collected up front so the delete walkers can
|
|
||||||
reproduce rsync's traversal order instead of readdir() order. rsync processes
|
|
||||||
a directory's extraneous subdirectories first (descending name, depth-first),
|
|
||||||
then its extraneous files (descending name), and only afterwards descends into
|
|
||||||
its kept subdirectories (ascending name). */
|
|
||||||
typedef struct {
|
|
||||||
char* name;
|
|
||||||
bool is_dir;
|
|
||||||
} DeleteDirEntry;
|
|
||||||
/* Collect the entries of the directory open on `dirfd` (excluding "." and ".."),
|
|
||||||
stat'ing each with AT_SYMLINK_NOFOLLOW. On success *out is a malloc'd array of
|
|
||||||
*count entries whose names the caller frees with delete_dir_entries_free().
|
|
||||||
Returns false on an allocation/readdir failure; a vanished entry (ENOENT) is
|
|
||||||
skipped, any other stat failure is reported through *operation_ok while the
|
|
||||||
walk continues. */
|
|
||||||
bool delete_dir_entries_collect(int dirfd, DeleteDirEntry** out, size_t* count, bool* operation_ok);
|
|
||||||
void delete_dir_entries_free(DeleteDirEntry* entries, size_t count);
|
|
||||||
/* Sort comparators: `_desc` orders subdirectories before files and each group by
|
|
||||||
descending name (rsync's extraneous-entry order); `_asc` orders plain ascending
|
|
||||||
name (rsync's kept-subdirectory order). */
|
|
||||||
int delete_dir_entry_cmp_desc(const void* a, const void* b);
|
|
||||||
int delete_dir_entry_cmp_asc(const void* a, const void* b);
|
|
||||||
/* Remove files/dirs/symlinks under dest_root that are not listed in manifest
|
|
||||||
without ever descending into a protected prefix (see DeleteSkipEntry). When
|
|
||||||
`synced_dirs` is non-NULL, extras are only removed directly inside a directory
|
|
||||||
whose destination-relative path is an exact entry in that list (the receive
|
|
||||||
root is the "." sentinel); directories outside the synchronized set are still
|
|
||||||
descended into so kept content below a listed directory is preserved, but
|
|
||||||
nothing in them is removed. A NULL `synced_dirs` keeps the legacy behavior of
|
|
||||||
treating the whole destination tree as deletable. `max_delete` caps the
|
|
||||||
number of removed entries (SIZE_MAX = unlimited): the walker removes up to the
|
|
||||||
cap and returns DELETE_WALK_LIMIT_REACHED when more extras remained.
|
|
||||||
`deleted_out`/`skipped_out` optionally receive the number of entries removed
|
|
||||||
and the number skipped because of the cap. */
|
|
||||||
DeleteWalkResult delete_extras_limited(const char* dest_root, const ArrayList* manifest,
|
|
||||||
const ArrayList* synced_dirs, size_t max_delete,
|
|
||||||
const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules, size_t* deleted_out,
|
|
||||||
size_t* skipped_out);
|
|
||||||
|
|
||||||
/* Optional per-deletion observer: called for each destination-relative path
|
|
||||||
actually removed (a file, symlink, or directory), in removal order, so the
|
|
||||||
receiver can stream rsync's `--info=del`/`--info=remove` lines. */
|
|
||||||
typedef void (*DeletePathObserver)(void* context, const char* rel_path);
|
|
||||||
|
|
||||||
/* `delete_extras_limited_observed` is delete_extras_limited with an optional
|
|
||||||
* observer; the observer is invoked only for entries truly removed. When
|
|
||||||
* `protect_rules` is non-NULL its receiver-side verdict is evaluated for every
|
|
||||||
* candidate extra: a first-match PROTECT leaves the entry (and, for a
|
|
||||||
* directory, its whole subtree) in place, while RISK/NONE fall through to the
|
|
||||||
* ordinary skip-prefix/keep-set logic. */
|
|
||||||
DeleteWalkResult delete_extras_limited_observed(const char* dest_root, const ArrayList* manifest,
|
|
||||||
const ArrayList* synced_dirs, size_t max_delete,
|
|
||||||
const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules,
|
|
||||||
size_t* deleted_out, size_t* skipped_out,
|
|
||||||
DeletePathObserver observer,
|
|
||||||
void* observer_context);
|
|
||||||
/* Read-only companion to delete_extras_limited: walk the destination exactly as
|
|
||||||
the delete pass would and APPEND (strdup'd) destination-relative paths that
|
|
||||||
WOULD be removed, without touching disk. Used for -n/--dry-run --delete
|
|
||||||
would-delete reporting. Returns true on a clean walk; the caller owns the
|
|
||||||
strings appended to `out` and receives their count in *count_out. */
|
|
||||||
bool delete_extras_list(const char* dest_root, const ArrayList* manifest,
|
|
||||||
const ArrayList* synced_dirs, const DeleteSkipEntry* skips, int skip_count,
|
|
||||||
const FilterRuleList* protect_rules, ArrayList* out, size_t* count_out);
|
|
||||||
bool delete_extras(const char* dest_root, const ArrayList* manifest);
|
|
||||||
/* Open the existing destination directory at `dest_root`, confined to the
|
/* Open the existing destination directory at `dest_root`, confined to the
|
||||||
authorized root with an O_NOFOLLOW component walk (the same confinement the
|
authorized root with an O_NOFOLLOW component walk (the same confinement the
|
||||||
deletion walker uses for its root). Returns a new fd the caller owns, or -1
|
deletion walker uses for its root). Returns a new fd the caller owns, or -1
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
"""End-to-end coverage for the `--temp-dir` EXDEV (cross-filesystem) fallback.
|
||||||
|
|
||||||
|
`file_to_disk_secure_impl` installs a completed temp file with `renameat(2)`;
|
||||||
|
when the scratch dir lives on a different filesystem the rename fails with
|
||||||
|
`EXDEV` and the engine retries with no scratch dir, writing the file directly in
|
||||||
|
the destination directory (a non-atomic copy), matching rsync.
|
||||||
|
|
||||||
|
The daemon receiver confines `--temp-dir` to the authorized receive root, so a
|
||||||
|
genuine cross-fs scratch there would require an in-root mount point. Bind/tmpfs
|
||||||
|
mounting is not permitted in the CI container (no `CAP_SYS_ADMIN`, and
|
||||||
|
unprivileged user namespaces are disabled), so this test reaches the exact same
|
||||||
|
code path through the local `--read-batch` apply instead: it has no
|
||||||
|
authorized-root confinement, so a relative `--temp-dir` that is a symlink to a
|
||||||
|
tmpfs (`/dev/shm`) is accepted and the final install then crosses filesystems.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
|
from common import CLIENT_CMD, get_dest_received_dir
|
||||||
|
|
||||||
|
TMPFS = "/dev/shm"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(args):
|
||||||
|
return subprocess.run(CLIENT_CMD + args, capture_output=True, text=True, timeout=180)
|
||||||
|
|
||||||
|
|
||||||
|
def _read(path):
|
||||||
|
with open(path, "rb") as fh:
|
||||||
|
return fh.read()
|
||||||
|
|
||||||
|
|
||||||
|
def test_read_batch_temp_dir_cross_filesystem_fallback(tmp_path):
|
||||||
|
if not os.path.isdir(TMPFS):
|
||||||
|
pytest.skip("no /dev/shm tmpfs available to force a cross-filesystem install")
|
||||||
|
|
||||||
|
source = tmp_path / "src"
|
||||||
|
dest = tmp_path / "dst"
|
||||||
|
source.mkdir()
|
||||||
|
dest.mkdir()
|
||||||
|
files = {
|
||||||
|
"payload.bin": bytes(range(256)) * 64,
|
||||||
|
"sub/nested.txt": b"nested exdev fallback\n" * 8,
|
||||||
|
}
|
||||||
|
for rel, data in files.items():
|
||||||
|
full = source / rel
|
||||||
|
full.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
full.write_bytes(data)
|
||||||
|
|
||||||
|
batch = tmp_path / "tree.batch"
|
||||||
|
r = _run(["--only-write-batch", str(batch), str(source)])
|
||||||
|
assert r.returncode == 0, (r.stdout, r.stderr)
|
||||||
|
|
||||||
|
# A cross-filesystem scratch dir, reached through a relative --temp-dir
|
||||||
|
# symlink (the local batch apply performs no authorized-root confinement).
|
||||||
|
scratch = os.path.join(TMPFS, "fastsync_exdev_%d" % os.getpid())
|
||||||
|
shutil.rmtree(scratch, ignore_errors=True)
|
||||||
|
os.makedirs(scratch)
|
||||||
|
os.symlink(scratch, dest / "scratch")
|
||||||
|
try:
|
||||||
|
assert os.stat(scratch).st_dev != os.stat(dest).st_dev, (
|
||||||
|
"scratch and destination share a filesystem; EXDEV cannot be exercised"
|
||||||
|
)
|
||||||
|
r = _run(["--read-batch", str(batch), str(dest), "--temp-dir=scratch"])
|
||||||
|
assert r.returncode == 0, (r.stdout, r.stderr)
|
||||||
|
# The engine must report the non-atomic cross-fs fallback rather than
|
||||||
|
# silently claiming an atomic install.
|
||||||
|
assert "different filesystem" in (r.stdout + r.stderr), (r.stdout, r.stderr)
|
||||||
|
# The tree is still byte-exact and the scratch dir is left clean.
|
||||||
|
received = get_dest_received_dir(str(dest), str(source))
|
||||||
|
for rel, data in files.items():
|
||||||
|
assert _read(os.path.join(received, rel)) == data, f"content mismatch for {rel}"
|
||||||
|
assert os.listdir(scratch) == [], "cross-fs temp file was not cleaned up"
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(scratch, ignore_errors=True)
|
||||||
@@ -195,7 +195,7 @@ static void test_format_C_padding_uses_transfer_algo() {
|
|||||||
{CHECKSUM_ALGO_NONE, 2},
|
{CHECKSUM_ALGO_NONE, 2},
|
||||||
};
|
};
|
||||||
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
|
||||||
config->checksum_transfer_algo = cases[i].algo;
|
config->cli.checksum_transfer_algo = cases[i].algo;
|
||||||
char expected[64];
|
char expected[64];
|
||||||
size_t n = 0;
|
size_t n = 0;
|
||||||
expected[n++] = '[';
|
expected[n++] = '[';
|
||||||
|
|||||||
+24
-21
@@ -730,7 +730,7 @@ static void test_parse_args_port_alias() {
|
|||||||
EXPECT_EQ_INT(cfg->server_port, 9000);
|
EXPECT_EQ_INT(cfg->server_port, 9000);
|
||||||
/* The default port is 8080; the explicit bit is what lets --dry-run tell an
|
/* The default port is 8080; the explicit bit is what lets --dry-run tell an
|
||||||
explicit remote target from the default and route to the server. */
|
explicit remote target from the default and route to the server. */
|
||||||
EXPECT_TRUE(cfg->server_port_set);
|
EXPECT_TRUE(cfg->cli.server_port_set);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
|
|
||||||
cfg = config_create();
|
cfg = config_create();
|
||||||
@@ -738,7 +738,7 @@ static void test_parse_args_port_alias() {
|
|||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_inline, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_inline, positional_args, &positional_count), 0);
|
||||||
EXPECT_EQ_INT(cfg->server_port, 9001);
|
EXPECT_EQ_INT(cfg->server_port, 9001);
|
||||||
EXPECT_TRUE(cfg->server_port_set);
|
EXPECT_TRUE(cfg->cli.server_port_set);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
|
|
||||||
cfg = config_create();
|
cfg = config_create();
|
||||||
@@ -746,7 +746,7 @@ static void test_parse_args_port_alias() {
|
|||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_long, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_long, positional_args, &positional_count), 0);
|
||||||
EXPECT_EQ_INT(cfg->server_port, 9002);
|
EXPECT_EQ_INT(cfg->server_port, 9002);
|
||||||
EXPECT_TRUE(cfg->server_port_set);
|
EXPECT_TRUE(cfg->cli.server_port_set);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,18 +757,18 @@ static void test_parse_args_server_host_sets_routing_bit() {
|
|||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
int positional_args[2];
|
int positional_args[2];
|
||||||
int positional_count = 0;
|
int positional_count = 0;
|
||||||
EXPECT_FALSE(cfg->server_host_set);
|
EXPECT_FALSE(cfg->cli.server_host_set);
|
||||||
char* argv_space[] = {"fastsync", "--server-host", "example.test", "/src", "/dst"};
|
char* argv_space[] = {"fastsync", "--server-host", "example.test", "/src", "/dst"};
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv_space, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv_space, positional_args, &positional_count), 0);
|
||||||
EXPECT_EQ_STR(cfg->server_host, "example.test");
|
EXPECT_EQ_STR(cfg->server_host, "example.test");
|
||||||
EXPECT_TRUE(cfg->server_host_set);
|
EXPECT_TRUE(cfg->cli.server_host_set);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
|
|
||||||
cfg = config_create();
|
cfg = config_create();
|
||||||
char* argv_inline[] = {"fastsync", "--server-host=example.test", "/src", "/dst"};
|
char* argv_inline[] = {"fastsync", "--server-host=example.test", "/src", "/dst"};
|
||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv_inline, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv_inline, positional_args, &positional_count), 0);
|
||||||
EXPECT_TRUE(cfg->server_host_set);
|
EXPECT_TRUE(cfg->cli.server_host_set);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1726,7 +1726,7 @@ static void test_parse_args_no_preserve_blocks_implicit_metadata() {
|
|||||||
|
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
|
||||||
EXPECT_FALSE(cfg->use_metadata);
|
EXPECT_FALSE(cfg->use_metadata);
|
||||||
EXPECT_TRUE(cfg->metadata_explicitly_disabled);
|
EXPECT_TRUE(cfg->cli.metadata_explicitly_disabled);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1777,7 +1777,7 @@ static void test_parse_args_checksum_choice_rejects_unsupported() {
|
|||||||
int positional_args[2];
|
int positional_args[2];
|
||||||
int positional_count = 0;
|
int positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||||
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
|
EXPECT_EQ_INT(cfg->cli.cli_exit_code, 4);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1817,7 +1817,7 @@ static void test_parse_args_checksum_choice_new_algos() {
|
|||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv4, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv4, positional_args, &positional_count), 0);
|
||||||
EXPECT_EQ_INT(cfg->checksum_algo, single[i]);
|
EXPECT_EQ_INT(cfg->checksum_algo, single[i]);
|
||||||
EXPECT_EQ_INT(cfg->checksum_transfer_algo, single[i]);
|
EXPECT_EQ_INT(cfg->cli.checksum_transfer_algo, single[i]);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1827,7 +1827,7 @@ static void test_parse_args_checksum_choice_new_algos() {
|
|||||||
char* argv5[] = {"fastsync", "--cc=sha1,md4", "/checksum/src", "/dst"};
|
char* argv5[] = {"fastsync", "--cc=sha1,md4", "/checksum/src", "/dst"};
|
||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv5, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv5, positional_args, &positional_count), 0);
|
||||||
EXPECT_EQ_INT(cfg->checksum_transfer_algo, (int)CHECKSUM_ALGO_SHA1);
|
EXPECT_EQ_INT(cfg->cli.checksum_transfer_algo, (int)CHECKSUM_ALGO_SHA1);
|
||||||
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_MD4);
|
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_MD4);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
|
|
||||||
@@ -1849,14 +1849,14 @@ static void test_parse_args_checksum_none_with_checksum_rejected() {
|
|||||||
int positional_args[2];
|
int positional_args[2];
|
||||||
int positional_count = 0;
|
int positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||||
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
|
EXPECT_EQ_INT(cfg->cli.cli_exit_code, 4);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
|
|
||||||
cfg = config_create();
|
cfg = config_create();
|
||||||
char* argv2[] = {"fastsync", "--checksum", "--cc=md5,none", "/checksum/src", "/dst"};
|
char* argv2[] = {"fastsync", "--checksum", "--cc=md5,none", "/checksum/src", "/dst"};
|
||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), -1);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv2, positional_args, &positional_count), -1);
|
||||||
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
|
EXPECT_EQ_INT(cfg->cli.cli_exit_code, 4);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
|
|
||||||
/* "none" as the TRANSFER checksum with a real pre-transfer checksum is
|
/* "none" as the TRANSFER checksum with a real pre-transfer checksum is
|
||||||
@@ -1958,7 +1958,7 @@ static void test_parse_args_compress_choice_parity() {
|
|||||||
int positional_args[2];
|
int positional_args[2];
|
||||||
int positional_count = 0;
|
int positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), -1);
|
||||||
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
|
EXPECT_EQ_INT(cfg->cli.cli_exit_code, 4);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2078,6 +2078,9 @@ static void test_parse_args_temp_dir() {
|
|||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --old-args is accepted for rsync CLI compatibility as a documented no-op (the
|
||||||
|
* remote server path is always safely quoted); it stores no Config field, so
|
||||||
|
* parsing it must simply succeed and leave the positional arguments intact. */
|
||||||
static void test_parse_args_old_args() {
|
static void test_parse_args_old_args() {
|
||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
char* argv[] = {"fastsync", "--old-args", "/src", "/dst"};
|
char* argv[] = {"fastsync", "--old-args", "/src", "/dst"};
|
||||||
@@ -2085,7 +2088,7 @@ static void test_parse_args_old_args() {
|
|||||||
int positional_count = 0;
|
int positional_count = 0;
|
||||||
|
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
EXPECT_TRUE(cfg->old_args);
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2719,7 +2722,7 @@ static void test_parse_args_compression_env_list() {
|
|||||||
cfg = config_create();
|
cfg = config_create();
|
||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
||||||
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
|
EXPECT_EQ_INT(cfg->cli.cli_exit_code, 4);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
unsetenv("RSYNC_COMPRESS_LIST");
|
unsetenv("RSYNC_COMPRESS_LIST");
|
||||||
}
|
}
|
||||||
@@ -2734,7 +2737,7 @@ static void test_parse_args_checksum_env_list() {
|
|||||||
setenv("RSYNC_CHECKSUM_LIST", "md5", 1);
|
setenv("RSYNC_CHECKSUM_LIST", "md5", 1);
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
|
||||||
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_MD5);
|
EXPECT_EQ_INT(cfg->checksum_algo, (int)CHECKSUM_ALGO_MD5);
|
||||||
EXPECT_EQ_INT(cfg->checksum_transfer_algo, (int)CHECKSUM_ALGO_MD5);
|
EXPECT_EQ_INT(cfg->cli.checksum_transfer_algo, (int)CHECKSUM_ALGO_MD5);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
|
|
||||||
/* An explicit --cc wins. */
|
/* An explicit --cc wins. */
|
||||||
@@ -2750,7 +2753,7 @@ static void test_parse_args_checksum_env_list() {
|
|||||||
cfg = config_create();
|
cfg = config_create();
|
||||||
positional_count = 0;
|
positional_count = 0;
|
||||||
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1);
|
||||||
EXPECT_EQ_INT(cfg->cli_exit_code, 4);
|
EXPECT_EQ_INT(cfg->cli.cli_exit_code, 4);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
unsetenv("RSYNC_CHECKSUM_LIST");
|
unsetenv("RSYNC_CHECKSUM_LIST");
|
||||||
}
|
}
|
||||||
@@ -4385,7 +4388,7 @@ static void test_parse_args_preserve_long_form() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --no-perms/--no-times/--no-owner/--no-group (long and short) clear only
|
/* --no-perms/--no-times/--no-owner/--no-group (long and short) clear only
|
||||||
* their own attribute bit; they never set metadata_explicitly_disabled. */
|
* their own attribute bit; they never set cli.metadata_explicitly_disabled. */
|
||||||
static void test_parse_args_preserve_negations() {
|
static void test_parse_args_preserve_negations() {
|
||||||
struct {
|
struct {
|
||||||
const char* arg;
|
const char* arg;
|
||||||
@@ -4413,7 +4416,7 @@ static void test_parse_args_preserve_negations() {
|
|||||||
bool expected = all[j] != cases[i].offset;
|
bool expected = all[j] != cases[i].offset;
|
||||||
EXPECT_TRUE(*(bool*)((char*)cfg + all[j]) == expected);
|
EXPECT_TRUE(*(bool*)((char*)cfg + all[j]) == expected);
|
||||||
}
|
}
|
||||||
EXPECT_FALSE(cfg->metadata_explicitly_disabled);
|
EXPECT_FALSE(cfg->cli.metadata_explicitly_disabled);
|
||||||
/* -a's devices/specials keep the metadata frame on. */
|
/* -a's devices/specials keep the metadata frame on. */
|
||||||
EXPECT_TRUE(cfg->use_metadata);
|
EXPECT_TRUE(cfg->use_metadata);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
@@ -4456,7 +4459,7 @@ static void test_parse_args_no_preserve_disables_bundle() {
|
|||||||
EXPECT_FALSE(cfg->preserve_times);
|
EXPECT_FALSE(cfg->preserve_times);
|
||||||
EXPECT_FALSE(cfg->preserve_owner);
|
EXPECT_FALSE(cfg->preserve_owner);
|
||||||
EXPECT_FALSE(cfg->preserve_group);
|
EXPECT_FALSE(cfg->preserve_group);
|
||||||
EXPECT_TRUE(cfg->metadata_explicitly_disabled);
|
EXPECT_TRUE(cfg->cli.metadata_explicitly_disabled);
|
||||||
EXPECT_TRUE(cfg->use_incremental);
|
EXPECT_TRUE(cfg->use_incremental);
|
||||||
EXPECT_FALSE(cfg->use_metadata);
|
EXPECT_FALSE(cfg->use_metadata);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
@@ -4509,7 +4512,7 @@ static void test_parse_args_incremental_implies_preserve() {
|
|||||||
EXPECT_EQ_INT(parse_args(cfg, 5, argv4, positional_args, &positional_count), 0);
|
EXPECT_EQ_INT(parse_args(cfg, 5, argv4, positional_args, &positional_count), 0);
|
||||||
EXPECT_FALSE(cfg->preserve_perms);
|
EXPECT_FALSE(cfg->preserve_perms);
|
||||||
EXPECT_FALSE(cfg->preserve_times);
|
EXPECT_FALSE(cfg->preserve_times);
|
||||||
EXPECT_TRUE(cfg->metadata_explicitly_disabled);
|
EXPECT_TRUE(cfg->cli.metadata_explicitly_disabled);
|
||||||
EXPECT_FALSE(cfg->use_metadata);
|
EXPECT_FALSE(cfg->use_metadata);
|
||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-4
@@ -2533,15 +2533,15 @@ static void test_config_derived_use_metadata() {
|
|||||||
/* Incremental/delta imply metadata unless --no-preserve disabled it. */
|
/* Incremental/delta imply metadata unless --no-preserve disabled it. */
|
||||||
c->use_incremental = true;
|
c->use_incremental = true;
|
||||||
EXPECT_TRUE(config_derived_use_metadata(c));
|
EXPECT_TRUE(config_derived_use_metadata(c));
|
||||||
c->metadata_explicitly_disabled = true;
|
c->cli.metadata_explicitly_disabled = true;
|
||||||
EXPECT_FALSE(config_derived_use_metadata(c));
|
EXPECT_FALSE(config_derived_use_metadata(c));
|
||||||
c->metadata_explicitly_disabled = false;
|
c->cli.metadata_explicitly_disabled = false;
|
||||||
c->use_incremental = false;
|
c->use_incremental = false;
|
||||||
c->use_delta = true;
|
c->use_delta = true;
|
||||||
EXPECT_TRUE(config_derived_use_metadata(c));
|
EXPECT_TRUE(config_derived_use_metadata(c));
|
||||||
c->metadata_explicitly_disabled = true;
|
c->cli.metadata_explicitly_disabled = true;
|
||||||
EXPECT_FALSE(config_derived_use_metadata(c));
|
EXPECT_FALSE(config_derived_use_metadata(c));
|
||||||
c->metadata_explicitly_disabled = false;
|
c->cli.metadata_explicitly_disabled = false;
|
||||||
c->use_delta = false;
|
c->use_delta = false;
|
||||||
|
|
||||||
/* Flags that must NOT imply metadata on their own. */
|
/* Flags that must NOT imply metadata on their own. */
|
||||||
|
|||||||
+6
-6
@@ -2255,26 +2255,26 @@ static void test_basis_delete_relative_root_slash() {
|
|||||||
EXPECT_NOT_NULL(cfg);
|
EXPECT_NOT_NULL(cfg);
|
||||||
cfg->receive_root_directory = str_dup("/");
|
cfg->receive_root_directory = str_dup("/");
|
||||||
|
|
||||||
char* rel = file_receive_basis_delete_relative(cfg, "/a");
|
char* rel = delete_basis_relative(cfg, "/a");
|
||||||
EXPECT_NOT_NULL(rel);
|
EXPECT_NOT_NULL(rel);
|
||||||
EXPECT_EQ_STR(rel, "a");
|
EXPECT_EQ_STR(rel, "a");
|
||||||
free(rel);
|
free(rel);
|
||||||
rel = file_receive_basis_delete_relative(cfg, "/a/b");
|
rel = delete_basis_relative(cfg, "/a/b");
|
||||||
EXPECT_NOT_NULL(rel);
|
EXPECT_NOT_NULL(rel);
|
||||||
EXPECT_EQ_STR(rel, "a/b");
|
EXPECT_EQ_STR(rel, "a/b");
|
||||||
free(rel);
|
free(rel);
|
||||||
/* The root itself is not a child. */
|
/* The root itself is not a child. */
|
||||||
EXPECT_NULL(file_receive_basis_delete_relative(cfg, "/"));
|
EXPECT_NULL(delete_basis_relative(cfg, "/"));
|
||||||
/* A relative entry is already root-relative. */
|
/* A relative entry is already root-relative. */
|
||||||
rel = file_receive_basis_delete_relative(cfg, "x/y");
|
rel = delete_basis_relative(cfg, "x/y");
|
||||||
EXPECT_NOT_NULL(rel);
|
EXPECT_NOT_NULL(rel);
|
||||||
EXPECT_EQ_STR(rel, "x/y");
|
EXPECT_EQ_STR(rel, "x/y");
|
||||||
free(rel);
|
free(rel);
|
||||||
/* An absolute path outside a non-"/" root is unreachable. */
|
/* An absolute path outside a non-"/" root is unreachable. */
|
||||||
free(cfg->receive_root_directory);
|
free(cfg->receive_root_directory);
|
||||||
cfg->receive_root_directory = str_dup("/root");
|
cfg->receive_root_directory = str_dup("/root");
|
||||||
EXPECT_NULL(file_receive_basis_delete_relative(cfg, "/other/a"));
|
EXPECT_NULL(delete_basis_relative(cfg, "/other/a"));
|
||||||
rel = file_receive_basis_delete_relative(cfg, "/root/a");
|
rel = delete_basis_relative(cfg, "/root/a");
|
||||||
EXPECT_NOT_NULL(rel);
|
EXPECT_NOT_NULL(rel);
|
||||||
EXPECT_EQ_STR(rel, "a");
|
EXPECT_EQ_STR(rel, "a");
|
||||||
free(rel);
|
free(rel);
|
||||||
|
|||||||
+43
-2
@@ -1,6 +1,8 @@
|
|||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
|
#include <fcntl.h>
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -715,7 +717,7 @@ static void test_protocol_throttle_bytes_paces() {
|
|||||||
|
|
||||||
struct timespec start;
|
struct timespec start;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &start);
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||||
protocol_throttle_bytes(150000);
|
protocol_throttle_bytes(-1, 150000);
|
||||||
struct timespec now;
|
struct timespec now;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
long long elapsed_ms =
|
long long elapsed_ms =
|
||||||
@@ -736,7 +738,7 @@ static void test_protocol_throttle_bytes_unlimited() {
|
|||||||
|
|
||||||
struct timespec start;
|
struct timespec start;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &start);
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||||
protocol_throttle_bytes(100000000ULL);
|
protocol_throttle_bytes(-1, 100000000ULL);
|
||||||
struct timespec now;
|
struct timespec now;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &now);
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
long long elapsed_ms =
|
long long elapsed_ms =
|
||||||
@@ -746,6 +748,44 @@ static void test_protocol_throttle_bytes_unlimited() {
|
|||||||
protocol_session_unbind();
|
protocol_session_unbind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Regression for the plaintext sendfile path: it calls protocol_throttle_bytes()
|
||||||
|
* immediately after send_n_data(), which already bound legacy_io_session.write_fd
|
||||||
|
* to the wire fd. Resolving the throttle session with (read=-1, write=-1)
|
||||||
|
* mismatched that fd and re-initialized the legacy session, granting a *second*
|
||||||
|
* first-call burst and discarding the accumulated debt. This drives the same
|
||||||
|
* sequence and asserts the debt from send_n_data carries into the throttle. */
|
||||||
|
static void test_protocol_throttle_bytes_legacy_same_session() {
|
||||||
|
const size_t payload = 150000; /* 1.5x the 100 KB burst at --bwlimit=1 MB/s */
|
||||||
|
unsigned char* buffer = malloc(payload);
|
||||||
|
EXPECT_TRUE(buffer != NULL);
|
||||||
|
memset(buffer, 0, payload);
|
||||||
|
|
||||||
|
io_set_fds(-1, -1);
|
||||||
|
io_set_bwlimit(1000000ULL);
|
||||||
|
|
||||||
|
int fd = open("/dev/null", O_WRONLY);
|
||||||
|
EXPECT_TRUE(fd >= 0);
|
||||||
|
|
||||||
|
struct timespec start;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||||
|
/* send_n_data() consumes the whole 100 KB burst and sleeps ~50 ms. */
|
||||||
|
EXPECT_TRUE(send_n_data(fd, buffer, payload));
|
||||||
|
/* The throttle must share that session, so the 150 KB is all debt and sleeps
|
||||||
|
~150 ms (total ~200 ms). A re-initialized session would hand out a fresh
|
||||||
|
100 KB burst and sleep only ~50 ms (total ~100 ms). */
|
||||||
|
protocol_throttle_bytes(fd, payload);
|
||||||
|
struct timespec now;
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &now);
|
||||||
|
long long elapsed_ms =
|
||||||
|
(now.tv_sec - start.tv_sec) * 1000LL + (now.tv_nsec - start.tv_nsec) / 1000000LL;
|
||||||
|
EXPECT_TRUE(elapsed_ms >= 150);
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
free(buffer);
|
||||||
|
io_set_bwlimit(0);
|
||||||
|
io_set_fds(-1, -1);
|
||||||
|
}
|
||||||
|
|
||||||
void test_protocol() {
|
void test_protocol() {
|
||||||
test_send_receive_n_data();
|
test_send_receive_n_data();
|
||||||
test_send_receive_n_data_zero();
|
test_send_receive_n_data_zero();
|
||||||
@@ -778,4 +818,5 @@ void test_protocol() {
|
|||||||
test_data_create_starts_uncharged_and_unowned();
|
test_data_create_starts_uncharged_and_unowned();
|
||||||
test_protocol_throttle_bytes_paces();
|
test_protocol_throttle_bytes_paces();
|
||||||
test_protocol_throttle_bytes_unlimited();
|
test_protocol_throttle_bytes_unlimited();
|
||||||
|
test_protocol_throttle_bytes_legacy_same_session();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "test_shared_utils.h"
|
#include "test_shared_utils.h"
|
||||||
|
#include "delete.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
#include "protocol.h"
|
#include "protocol.h"
|
||||||
#include "test_utils.h"
|
#include "test_utils.h"
|
||||||
|
|||||||
Reference in New Issue
Block a user