Merge branch 'feat/w9-dryrun' into fix/w9-integration
This commit is contained in:
+36
-6
@@ -288,10 +288,19 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
||||
goto fail;
|
||||
}
|
||||
if (status == STATUS_CHECK) {
|
||||
bool skipped;
|
||||
File* file = receive_incremental_check(file_descriptor, config, &skipped);
|
||||
if (!skipped && (!file || !sink->store_file(file, sink->context)))
|
||||
bool skipped = false;
|
||||
bool would_transfer = false;
|
||||
File* file = receive_incremental_check_ex(file_descriptor, config, &skipped, &would_transfer);
|
||||
if (config->dry_run) {
|
||||
/* Server-contacting --dry-run: the reply has already been sent
|
||||
(STATUS_OK = up to date, STATUS_DRY_RUN_TRANSFER = would transfer) and
|
||||
nothing may be stored. Both flags false means a genuine protocol
|
||||
error (STATUS_ERROR already sent or sent by receive_error below). */
|
||||
if (!skipped && !would_transfer)
|
||||
goto receive_error;
|
||||
} else if (!skipped && (!file || !sink->store_file(file, sink->context))) {
|
||||
goto receive_error;
|
||||
}
|
||||
} else if (status == STATUS_CHUNK) {
|
||||
Chunk* chunk = receive_chunk_data(file_descriptor, config);
|
||||
if (!chunk || !receiver_process_chunk(chunk, sink))
|
||||
@@ -323,6 +332,15 @@ int receiver_process_pending(Config* config, int file_descriptor, const Receiver
|
||||
DeleteManifest* manifest = receive_manifest_entries(file_descriptor);
|
||||
if (!manifest)
|
||||
goto fail; /* receive_manifest_entries already sent STATUS_ERROR */
|
||||
if (config->dry_run) {
|
||||
/* Server-contacting --dry-run mutates nothing, so a keep-set manifest
|
||||
is consumed and discarded. The early-delete mode still needs its ACK
|
||||
so a sender blocked on the delete handshake is not left hanging. */
|
||||
delete_manifest_free(manifest);
|
||||
if (early_delete && !send_status(file_descriptor, STATUS_OK))
|
||||
goto fail;
|
||||
goto next_status;
|
||||
}
|
||||
if (early_delete) {
|
||||
/* --delete-before / --delete-during: the manifest is authoritative the
|
||||
moment it arrives, before any file data. Delete now and acknowledge
|
||||
@@ -441,7 +459,11 @@ typedef struct {
|
||||
static bool receiver_save_file(File* file, void* context_pointer) {
|
||||
ReceiverSaveContext* context = context_pointer;
|
||||
FileSaveResult result = FILE_SAVE_ERROR;
|
||||
if (!context->config->save_to_disk) {
|
||||
if (context->config->dry_run) {
|
||||
/* Defense in depth: a dry-run receiver mutates nothing even if a data
|
||||
frame reaches the sink (the sender is not supposed to send one). */
|
||||
result = FILE_SAVE_SKIPPED;
|
||||
} else if (!context->config->save_to_disk) {
|
||||
/* Nothing is stored; report the file as not-written so a
|
||||
--remove-source-files sender keeps its source. */
|
||||
result = FILE_SAVE_SKIPPED;
|
||||
@@ -457,8 +479,12 @@ static bool receiver_save_file(File* file, void* context_pointer) {
|
||||
file_destroy(file);
|
||||
return false;
|
||||
}
|
||||
if (result != FILE_SAVE_ERROR && context->config->remove_source_files && !file->is_dir &&
|
||||
!file->is_special && !file->skip &&
|
||||
/* A dry-run receiver mutates nothing AND records no per-file outcomes: a
|
||||
hostile dry-run client that streamed data frames anyway must not be able to
|
||||
grow `outcomes` without bound (receiver_outcomes_append reallocs uncharged)
|
||||
or force a per-frame ack. */
|
||||
if (!context->config->dry_run && result != FILE_SAVE_ERROR &&
|
||||
context->config->remove_source_files && !file->is_dir && !file->is_special && !file->skip &&
|
||||
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||
file_destroy(file);
|
||||
return false;
|
||||
@@ -469,6 +495,10 @@ static bool receiver_save_file(File* file, void* context_pointer) {
|
||||
|
||||
static bool receiver_send_success_frame(int fd, void* context_pointer) {
|
||||
ReceiverSaveContext* context = context_pointer;
|
||||
/* Server-contacting --dry-run: nothing was staged or written, so there is
|
||||
nothing to publish and no directory times to stamp. */
|
||||
if (context->config->dry_run)
|
||||
return receiver_send_final_success(fd, context->config, &context->outcomes);
|
||||
/* --delay-updates: the whole protocol stream (including manifest/delete
|
||||
handling, which ran inside receiver_process) has succeeded and every
|
||||
staged file was fully written. Publish them atomically now, before the
|
||||
|
||||
@@ -196,7 +196,11 @@ int write_thread(void* pipeline_context) {
|
||||
}
|
||||
size_t file_bytes = file->data ? file->data->size : 0;
|
||||
FileSaveResult result = FILE_SAVE_SKIPPED;
|
||||
if (save_to_disk) {
|
||||
/* Server-contacting --dry-run: never write. The receiver thread does not
|
||||
enqueue anything on the dry-run path, but this keeps the writer thread
|
||||
provably mutation-free if a data frame ever reached it. */
|
||||
bool dry_run = context->config->dry_run;
|
||||
if (save_to_disk && !dry_run) {
|
||||
result = file_save_to_disk_full(root_directory, file, context->config);
|
||||
if (result == FILE_SAVE_ERROR) {
|
||||
file_destroy(file);
|
||||
@@ -215,7 +219,7 @@ int write_thread(void* pipeline_context) {
|
||||
/* P7 Wave D: a directory's times are never applied inline (a later child
|
||||
write would clobber them); accumulate the metadata here and let the
|
||||
caller apply it once every writer has drained. */
|
||||
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||
if (!dry_run && result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
||||
dir_times_should_capture(context->config) &&
|
||||
!dir_time_list_add(&context->dir_times, file->path, file->metadata)) {
|
||||
file_destroy(file);
|
||||
@@ -234,8 +238,8 @@ int write_thread(void* pipeline_context) {
|
||||
which sources were actually written versus skipped on the receiver.
|
||||
Explicit directory entries and recreated device/special nodes have no
|
||||
source and are never acknowledged (mirrors receiver.c). */
|
||||
if (context->config->remove_source_files && !file->is_dir && !file->is_special && !file->skip &&
|
||||
!receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||
if (!dry_run && context->config->remove_source_files && !file->is_dir && !file->is_special &&
|
||||
!file->skip && !receiver_outcomes_append(&context->outcomes, (unsigned char)result)) {
|
||||
file_destroy(file);
|
||||
pipeline_context_receiver_note_bytes_released(context, file_bytes);
|
||||
mtx_lock(&context->mutex);
|
||||
|
||||
+38
-16
@@ -217,12 +217,24 @@ static bool path_is_within(const char* root, const char* path) {
|
||||
root is rejected up front instead of being silently invented by a later
|
||||
write. Both paths are confined to the authorized root by the secure file
|
||||
helpers. */
|
||||
/* Existence-only half of the precondition: the destination root must already
|
||||
resolve to a directory below the authorized root. Never creates anything, so
|
||||
a server-contacting --dry-run can apply the exact same fail-closed check a
|
||||
real run would without mutating the tree. */
|
||||
static bool receive_root_exists(const Config* config) {
|
||||
if (!config || !config->receive_root_directory)
|
||||
return false;
|
||||
return file_directory_exists_secure(config->receive_root_directory);
|
||||
}
|
||||
|
||||
/* Full precondition for a real run: --mkpath creates the root (and missing
|
||||
leading components), otherwise it must already exist as a directory. */
|
||||
static bool ensure_receive_root(const Config* config) {
|
||||
if (!config || !config->receive_root_directory)
|
||||
return false;
|
||||
if (config->mkpath)
|
||||
return file_ensure_directory_secure(config->receive_root_directory);
|
||||
return file_directory_exists_secure(config->receive_root_directory);
|
||||
return receive_root_exists(config);
|
||||
}
|
||||
|
||||
static bool configure_authorization(const char* root) {
|
||||
@@ -263,9 +275,11 @@ typedef enum {
|
||||
} ModuleAuthResult;
|
||||
|
||||
/* Looks up the daemon module selected by the client's config frame and rejects
|
||||
* a `read only` one (every FastSync network transfer writes; there is no
|
||||
* read-only wire operation yet). Returns the module, or NULL with *error set
|
||||
* to the caller-facing rejection message. */
|
||||
* a `read only` one for a real write transfer. A server-contacting --dry-run
|
||||
* IS a read-only wire operation (it reports what would transfer/skip and
|
||||
* mutates nothing), so a `read only` module is the safest possible dry-run
|
||||
* target and is accepted. Returns the module, or NULL with *error set to the
|
||||
* caller-facing rejection message. */
|
||||
static const DaemonModule* module_gate_lookup_module(const Config* config, const char** error) {
|
||||
const DaemonModule* module = daemon_conf_find_module(g_daemon_conf, config->module);
|
||||
if (module == NULL) {
|
||||
@@ -276,7 +290,7 @@ static const DaemonModule* module_gate_lookup_module(const Config* config, const
|
||||
*error = "requested daemon module does not exist";
|
||||
return NULL;
|
||||
}
|
||||
if (module->read_only) {
|
||||
if (module->read_only && !config->dry_run) {
|
||||
log_message(LOG_LEVEL_ERROR, "daemon module '%s' is read only; refusing write transfer",
|
||||
config->module);
|
||||
*error = "requested daemon module is read only";
|
||||
@@ -556,9 +570,10 @@ static const char* module_gate_install_root(const Config* config, const DaemonMo
|
||||
* becomes the authorized root via configure_authorization -- exactly the same
|
||||
* root confinement the standalone server applies to its single
|
||||
* --destination-root, but per-module and NEVER client-chosen. The module is
|
||||
* refused (with a clear log) when it is unknown, when it is `read only` (every
|
||||
* FastSync network transfer writes; there is no read-only wire operation yet),
|
||||
* when it requests client-chosen ownership without the module's
|
||||
* refused (with a clear log) when it is unknown, when it is `read only` for a
|
||||
* real write transfer (a server-contacting --dry-run is a read-only wire
|
||||
* operation and may target a `read only` module), when it requests
|
||||
* client-chosen ownership without the module's
|
||||
* `client owner = yes` opt-in (P7 Wave E hardening), or when the presented
|
||||
* daemon credentials fail for a module that declares `auth users`. Wave A
|
||||
* refused every auth-required module (auth was not yet implemented); Wave B
|
||||
@@ -756,9 +771,14 @@ void handler(int file_descriptor) {
|
||||
skipped via its implied --ignore-missing-args, but nothing is deleted). */
|
||||
config->delete_missing_args = config->delete_missing_args && allow_delete;
|
||||
/* --mkpath: create the destination root (and its missing leading components)
|
||||
before anything else; without it the root must pre-exist. A failure here
|
||||
aborts the connection cleanly before any file data is exchanged. */
|
||||
if (!ensure_receive_root(config)) {
|
||||
* before anything else; without it the root must pre-exist. The precondition
|
||||
* is UNCONDITIONAL: a server-contacting --dry-run must reject exactly the
|
||||
* root a real session would reject, so a client cannot set the wire dry_run
|
||||
* bit to relax it. Dry-run only runs the existence/directory check (never
|
||||
* --mkpath) so it creates nothing while still failing closed. A failure here
|
||||
* aborts the connection cleanly before any file data is exchanged. */
|
||||
bool root_ok = config->dry_run ? receive_root_exists(config) : ensure_receive_root(config);
|
||||
if (!root_ok) {
|
||||
char* escaped_root = output_escape(config->receive_root_directory, log_get_8_bit_output());
|
||||
log_message(LOG_LEVEL_ERROR, "destination root is not available: %s",
|
||||
escaped_root ? escaped_root : "<allocation failed>");
|
||||
@@ -767,8 +787,9 @@ void handler(int file_descriptor) {
|
||||
}
|
||||
/* A --delay-updates transfer stages under a private 0700 directory inside
|
||||
the receive root. Create it up front (wiping leftovers of any previously
|
||||
interrupted delayed transfer) so a fully-skipped run also starts clean. */
|
||||
if (config->delay_updates) {
|
||||
interrupted delayed transfer) so a fully-skipped run also starts clean.
|
||||
A dry-run stages nothing, so the staging tree is never created. */
|
||||
if (config->delay_updates && !config->dry_run) {
|
||||
config->delay_context = delay_updates_context_create(config->receive_root_directory);
|
||||
if (!config->delay_context || !delay_updates_prepare(config->delay_context)) {
|
||||
log_message(LOG_LEVEL_ERROR, "Failed to initialize --delay-updates staging area");
|
||||
@@ -864,12 +885,13 @@ void handler(int file_descriptor) {
|
||||
thrd_join(receiver, &receiver_result);
|
||||
thrd_join(writer, &writer_result);
|
||||
bool transfer_ok = receiver_result == thrd_success && writer_result == thrd_success;
|
||||
if (transfer_ok) {
|
||||
if (transfer_ok && !config->dry_run) {
|
||||
/* Commit-style (late) deletion: receive_thread handed the keep-set
|
||||
manifest here instead of deleting while write_thread might still be
|
||||
draining, so by now every file is on disk and the whole transfer is
|
||||
known to have succeeded. Remove the extras before publishing a
|
||||
--delay-updates run; the walker skips the staging directory. */
|
||||
--delay-updates run; the walker skips the staging directory. A
|
||||
server-contacting --dry-run deletes nothing (no manifest is sent). */
|
||||
if (context->deferred_manifest) {
|
||||
if (!manifest_delete_all(config, context->deferred_manifest)) {
|
||||
transfer_ok = false;
|
||||
@@ -878,7 +900,7 @@ void handler(int file_descriptor) {
|
||||
context->deferred_manifest = NULL;
|
||||
}
|
||||
}
|
||||
if (transfer_ok) {
|
||||
if (transfer_ok && !config->dry_run) {
|
||||
/* --delay-updates: receive_thread has finished the whole protocol stream
|
||||
(including manifest/delete handling) and write_thread has drained its
|
||||
queue, so every staged file is complete. Publish atomically before the
|
||||
|
||||
Reference in New Issue
Block a user