feat: implement --delay-updates receiver staging and publication
CI / lint (pull_request) Failing after 22s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
CI / lint (pull_request) Failing after 22s
CI / build-and-test (pull_request) Skipped
CI / sanitizers (address) (pull_request) Skipped
CI / sanitizers (undefined) (pull_request) Skipped
CI / fuzz-build (pull_request) Skipped
CI / coverage (pull_request) Skipped
CI / valgrind (pull_request) Skipped
Stage every successfully written file under a private 0700 .fastsync-stage directory inside the receive root and atomically publish all staged files only after the whole protocol stream (manifest/delete handling included) has completed, immediately before the success/outcome frame. On any abort/error before publication nothing is installed and staging is removed; a publish failure aborts the transfer with best-effort cleanup of the remainder (already-published files are not rolled back). Crash leftovers are wiped when the next delayed transfer starts. Wire: new delay_updates config flag (selection-options block), protocol version bumped to 2.6.0, client/server validation rejects --inplace. CLI/usage/validation updated. Works in single-threaded and -m modes (exactly one write_thread stages files; the staged-file registry is mutex-protected; publication runs once after both threads join). --existing/--ignore-existing/--update decide against the final destination at stage time; --backup is deferred to publication. remove_source_files outcomes are only sent after publication so skipped/unpublished sources are never deleted. Default (no flag) behavior is unchanged. Tests: config wire round-trip, CLI parse, --inplace rejection, new test_delay_updates unit suite (27 suites total), and integration TestDelayUpdates covering single/-m parity, incremental reruns, remove source files, receiver-skip ordering, and a deterministic publish-failure abort path.
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#include "compression.h"
|
||||
#include "config.h"
|
||||
#include "data.h"
|
||||
#include "delay_updates.h"
|
||||
#include "delta.h"
|
||||
#include "file.h"
|
||||
#include "log.h"
|
||||
@@ -26,6 +27,71 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
|
||||
return file_save_to_disk_full(root_directory, file, config) != FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* --delay-updates receiver path: write the file into a private staging tree
|
||||
below the receive root instead of its final destination, and remember it so
|
||||
it can be atomically renamed into place only once the whole transfer has
|
||||
succeeded. Existence/update policies (--existing/--ignore-existing/--update)
|
||||
are decided against the FINAL destination path at stage time so the run
|
||||
decides exactly what an immediate (non-delayed) run would decide; the staged
|
||||
file is then never re-checked at publication. Backups are deferred to
|
||||
publication so the final destination is untouched until the transfer ends. */
|
||||
static FileSaveResult file_stage_delayed_update(const char* root_directory,
|
||||
const char* destination_path, const File* file,
|
||||
Config* config) {
|
||||
bool sparse = config && config->preserve_sparse;
|
||||
bool preserve_executability = config && config->use_executability;
|
||||
|
||||
if (config && config->existing && !file_path_exists_secure(destination_path))
|
||||
return FILE_SAVE_SKIPPED;
|
||||
if (config && config->ignore_existing && file_path_exists_secure(destination_path))
|
||||
return FILE_SAVE_SKIPPED;
|
||||
if (config && config->update &&
|
||||
file_destination_is_newer_secure(destination_path, file->metadata))
|
||||
return FILE_SAVE_SKIPPED;
|
||||
|
||||
FileMetadata adjusted_metadata;
|
||||
const FileMetadata* metadata = file->metadata;
|
||||
if (metadata && config && config->chmod_spec && *config->chmod_spec) {
|
||||
adjusted_metadata = *metadata;
|
||||
if (!chmod_apply(adjusted_metadata.mode, config->chmod_spec, &adjusted_metadata.mode))
|
||||
return FILE_SAVE_ERROR;
|
||||
metadata = &adjusted_metadata;
|
||||
}
|
||||
|
||||
if (!config->delay_context) {
|
||||
config->delay_context = delay_updates_context_create(root_directory);
|
||||
if (!config->delay_context)
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
DelayUpdatesContext* context = config->delay_context;
|
||||
if (!delay_updates_prepare(context))
|
||||
return FILE_SAVE_ERROR;
|
||||
|
||||
char* staged_path = path_cat(context->staging_root, file->path);
|
||||
if (!staged_path)
|
||||
return FILE_SAVE_ERROR;
|
||||
|
||||
/* The staged location is brand new (stale leftovers from a prior crash were
|
||||
wiped by prepare), so the plain atomic temp+rename engine installs the
|
||||
complete file there. --temp-dir scratch is deliberately not layered on
|
||||
top of the delay-updates staging tree. */
|
||||
bool ok = file_to_disk_secure_with_fsync(staged_path, file->data->data, file->data->size, false,
|
||||
sparse, metadata, preserve_executability,
|
||||
config && config->use_fsync, NULL);
|
||||
if (!ok) {
|
||||
free(staged_path);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
if (!delay_updates_record(context, staged_path, destination_path, file->path)) {
|
||||
unlink(staged_path);
|
||||
free(staged_path);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
free(staged_path);
|
||||
return FILE_SAVE_WRITTEN;
|
||||
}
|
||||
|
||||
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
||||
const Config* config) {
|
||||
/* Backups are incompatible with ignore-existing: moving the entry first
|
||||
@@ -79,6 +145,18 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
/* --delay-updates diverts the whole write into the staging tree; the rest of
|
||||
this function is the immediate-install path. */
|
||||
if (config && config->delay_updates) {
|
||||
FileSaveResult result =
|
||||
file_stage_delayed_update(root_directory, destination_path, file, (Config*)config);
|
||||
free(confined_backup);
|
||||
free(confined_partial);
|
||||
free(destination_path);
|
||||
free(disk_path);
|
||||
return result;
|
||||
}
|
||||
|
||||
/* --existing checks the final destination, not a temporary partial path. */
|
||||
if (config && config->existing && !file_path_exists_secure(destination_path)) {
|
||||
free(confined_backup);
|
||||
|
||||
Reference in New Issue
Block a user