# Conflicts: # src/server/receiver_pipeline.c # src/shared/config.h # src/shared/protocol.h # tests/integration/test_fault_injection.py # tests/integration/test_preflight.py # tests/test_client_cli.c # tests/test_config.c
651 lines
27 KiB
C
651 lines
27 KiB
C
#include "receiver.h"
|
|
|
|
#include "charset.h"
|
|
#include "chunk.h"
|
|
#include "config.h"
|
|
#include "delete_plan.h"
|
|
#include "delay_updates.h"
|
|
#include "file.h"
|
|
#include "file_receive.h"
|
|
#include "log.h"
|
|
#include "metadata.h"
|
|
#include "protocol.h"
|
|
#include "utils.h"
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <sys/stat.h>
|
|
#include <time.h>
|
|
|
|
bool receiver_outcomes_append(ReceiverOutcomes* outcomes, unsigned char code) {
|
|
if (!outcomes)
|
|
return false;
|
|
if (outcomes->count == outcomes->capacity) {
|
|
size_t new_capacity = outcomes->capacity == 0 ? 64 : outcomes->capacity * 2;
|
|
if (new_capacity < outcomes->capacity)
|
|
return false;
|
|
unsigned char* grown = realloc(outcomes->entries, new_capacity);
|
|
if (!grown)
|
|
return false;
|
|
outcomes->entries = grown;
|
|
outcomes->capacity = new_capacity;
|
|
}
|
|
outcomes->entries[outcomes->count++] = code;
|
|
return true;
|
|
}
|
|
|
|
void receiver_outcomes_destroy(ReceiverOutcomes* outcomes) {
|
|
if (!outcomes)
|
|
return;
|
|
free(outcomes->entries);
|
|
outcomes->entries = NULL;
|
|
outcomes->count = 0;
|
|
outcomes->capacity = 0;
|
|
}
|
|
|
|
/* End-of-transfer success frame. When --remove-source-files was negotiated
|
|
each processed data file is acknowledged first (STATUS_NEXT = written,
|
|
STATUS_OK = skipped) so the sender never removes a source the receiver did
|
|
not actually store. The frame ends with `final_status` (STATUS_OK, or
|
|
STATUS_DELETE_LIMIT when a --max-delete commit was capped). */
|
|
bool receiver_send_final_success(int fd, const Config* config, const ReceiverOutcomes* outcomes,
|
|
Status final_status) {
|
|
if (!config->remove_source_files)
|
|
return send_status(fd, final_status);
|
|
size_t count = outcomes ? outcomes->count : 0;
|
|
for (size_t i = 0; i < count; i++) {
|
|
Status per_file = outcomes->entries[i] == FILE_SAVE_WRITTEN ? STATUS_NEXT : STATUS_OK;
|
|
if (!send_status(fd, per_file))
|
|
return false;
|
|
}
|
|
return send_status(fd, final_status);
|
|
}
|
|
|
|
bool receiver_send_stats_frame(int fd, const Config* config, const ReceiverStats* stats,
|
|
const struct ArrayList* would_delete) {
|
|
if (!config->report_stats)
|
|
return true;
|
|
ReceiverStats local;
|
|
memset(&local, 0, sizeof(local));
|
|
const ReceiverStats* out = stats ? stats : &local;
|
|
size_t count = would_delete ? (size_t)would_delete->size : 0;
|
|
if (count > (size_t)MAX_MANIFEST_ENTRIES)
|
|
count = MAX_MANIFEST_ENTRIES;
|
|
ReceiverStats record = *out;
|
|
record.would_delete_count = count;
|
|
if (!send_status(fd, STATUS_STATS) || !format_stats_send(fd, &record) ||
|
|
!send_int(fd, (int)count))
|
|
return false;
|
|
for (size_t i = 0; i < count; i++) {
|
|
const char* path = (const char*)would_delete->items[i];
|
|
if (!send_wire_str(fd, path ? path : ""))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool receiver_process_chunk(Chunk* chunk, const ReceiverSink* sink) {
|
|
if (!chunk || !sink || !sink->store_file)
|
|
return false;
|
|
for (int i = 0; i < chunk->element_count; i++) {
|
|
File* file = chunk->items[i];
|
|
if (!file) {
|
|
chunk_destroy(chunk);
|
|
return false;
|
|
}
|
|
chunk->items[i] = NULL;
|
|
if (!sink->store_file(file, sink->context)) {
|
|
chunk_destroy(chunk);
|
|
return false;
|
|
}
|
|
}
|
|
chunk_destroy(chunk);
|
|
return true;
|
|
}
|
|
|
|
/* P7 Wave D: read one STATUS_DIR_TIMES frame (a count followed by that many
|
|
* (path, metadata) directory entries) and route every entry through the regular
|
|
* store_file sink. A dir-time entry is RECORD-ONLY (file->dir_time_only): the
|
|
* sink accumulates its metadata for end-of-transfer application but creates
|
|
* nothing, so an empty/pruned source directory is never resurrected. A large
|
|
* tree arrives as repeated frames, each bounded by MAX_MANIFEST_ENTRIES; a
|
|
* malformed count or entry is a hard error. */
|
|
static bool receiver_process_dir_times(int fd, const Config* config, const ReceiverSink* sink) {
|
|
int count;
|
|
if (!receive_int(fd, &count) || count < 0 || count > MAX_MANIFEST_ENTRIES)
|
|
return false;
|
|
for (int i = 0; i < count; i++) {
|
|
File* dir = file_receive_dir_time(fd, config);
|
|
if (!dir || !sink->store_file(dir, sink->context))
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool receiver_process_batch(Config* config, int file_descriptor) {
|
|
int count;
|
|
if (config->checksum || !receive_int(file_descriptor, &count) || count < 0 ||
|
|
count > MAX_MANIFEST_ENTRIES)
|
|
return false;
|
|
for (int i = 0; i < count; i++) {
|
|
char* check_path = receive_wire_str(file_descriptor);
|
|
if (!check_path)
|
|
return false;
|
|
unsigned long long check_size;
|
|
long long check_mtime;
|
|
long long check_mtime_nsec;
|
|
if (!receive_n_data(file_descriptor, &check_size, sizeof(check_size)) ||
|
|
!receive_n_data(file_descriptor, &check_mtime, sizeof(check_mtime)) ||
|
|
!receive_n_data(file_descriptor, &check_mtime_nsec, sizeof(check_mtime_nsec)) ||
|
|
check_mtime_nsec < 0 || check_mtime_nsec >= 1000000000LL) {
|
|
free(check_path);
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
return false;
|
|
}
|
|
/* --trust-sender: accept a ``..``/absolute check path (a trusted sender's
|
|
odd-but-legit entry) and defer containment to the secure stat below;
|
|
an empty path is still always rejected. */
|
|
if (check_path[0] == '\0' ||
|
|
(!file_get_trust_sender() && !utils_valid_batch_path(check_path))) {
|
|
free(check_path);
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
return false;
|
|
}
|
|
if (check_size > MAX_RECEIVE_WHOLE_FILE_SIZE) {
|
|
free(check_path);
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
return false;
|
|
}
|
|
char* full_path = path_cat(config->receive_root_directory, check_path);
|
|
if (!full_path) {
|
|
free(check_path);
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
return false;
|
|
}
|
|
struct stat st;
|
|
bool has_old = file_stat_secure(full_path, &st);
|
|
long long old_mtime_nsec = 0;
|
|
if (has_old) {
|
|
#ifdef __linux__
|
|
old_mtime_nsec = st.st_mtim.tv_nsec;
|
|
#endif
|
|
}
|
|
bool match = !config->ignore_times && has_old && (unsigned long long)st.st_size == check_size &&
|
|
metadata_mtime_matches(st.st_mtime, old_mtime_nsec, (time_t)check_mtime,
|
|
(long)check_mtime_nsec, config->modify_window);
|
|
bool sent = send_status(file_descriptor, match ? STATUS_OK : STATUS_NEXT);
|
|
free(full_path);
|
|
free(check_path);
|
|
if (!sent)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ---- Anti-slowloris connection bounds ----
|
|
* A legitimate transfer either streams data frames continuously or, when it
|
|
* must pause, sends STATUS_KEEPALIVE so the peer sees the connection is alive.
|
|
* An attacker can therefore squat on a connection slot indefinitely by sending
|
|
* only keepalives under the per-message timeout. Two CLOCK_MONOTONIC bounds
|
|
* defeat that without ever punishing a real transfer:
|
|
*
|
|
* MAX_SESSION_IDLE_SEC (1 h): the longest a stream may make no forward
|
|
* progress. Data/status frames count as progress and refresh the timer;
|
|
* keepalives do not. One hour is far longer than any real pause between
|
|
* data frames, yet small enough to reap a slowloris well before the 24 h
|
|
* session cap.
|
|
*
|
|
* MAX_SESSION_WALL_SEC (24 h): an absolute ceiling on one connection's
|
|
* lifetime as defense-in-depth against a trickle of progress frames that
|
|
* resets the idle timer just below its limit. Larger than any plausible
|
|
* single transfer while still bounding resource occupancy.
|
|
*
|
|
* Both are wall-clock deltas, so the per-message poll timeout (60 s by default,
|
|
* or --timeout) can never fool them, and both the single-threaded and the -m
|
|
* receiver paths (receiver_process_pending) share the same logic. */
|
|
#define MAX_SESSION_IDLE_SEC 3600u
|
|
#define MAX_SESSION_WALL_SEC 86400u
|
|
|
|
static unsigned int g_max_session_idle_sec = MAX_SESSION_IDLE_SEC;
|
|
static unsigned int g_max_session_wall_sec = MAX_SESSION_WALL_SEC;
|
|
|
|
void receiver_set_time_limits(unsigned int idle_sec, unsigned int wall_sec) {
|
|
g_max_session_idle_sec = idle_sec;
|
|
g_max_session_wall_sec = wall_sec;
|
|
}
|
|
|
|
void receiver_reset_time_limits(void) {
|
|
g_max_session_idle_sec = MAX_SESSION_IDLE_SEC;
|
|
g_max_session_wall_sec = MAX_SESSION_WALL_SEC;
|
|
}
|
|
|
|
bool receiver_time_limit_exceeded(const struct timespec* session_start,
|
|
const struct timespec* last_progress,
|
|
const struct timespec* now) {
|
|
if (!session_start || !last_progress || !now)
|
|
return false;
|
|
if (now->tv_sec - session_start->tv_sec >= (time_t)g_max_session_wall_sec)
|
|
return true;
|
|
if (now->tv_sec - last_progress->tv_sec >= (time_t)g_max_session_idle_sec)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
/* A frame proves forward progress only when it cannot be fabricated for free.
|
|
* KEEPALIVE/ABORT are pure liveness, and CHECK_BATCH/DIR_TIMES may carry zero
|
|
* entries, so a peer must not be able to hold a connection slot forever by
|
|
* merely emitting empty frames. */
|
|
static bool status_counts_as_progress(Status status) {
|
|
switch (status) {
|
|
case STATUS_KEEPALIVE:
|
|
case STATUS_ABORT:
|
|
case STATUS_CHECK_BATCH:
|
|
case STATUS_DIR_TIMES:
|
|
return false;
|
|
default:
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/* Refresh the progress timestamp for a forward-moving frame and enforce the
|
|
* bounds above. Returns false when the connection must be dropped; the
|
|
* terminal STATUS_ERROR is sent only when the sink owns error reporting (the
|
|
* -m sink sets send_error=false so the main thread emits exactly one). */
|
|
static bool receiver_note_status(const struct timespec* session_start,
|
|
struct timespec* last_progress, Status status, int file_descriptor,
|
|
const ReceiverSink* sink) {
|
|
struct timespec now;
|
|
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
|
|
now = *last_progress;
|
|
if (status_counts_as_progress(status))
|
|
*last_progress = now;
|
|
if (!receiver_time_limit_exceeded(session_start, last_progress, &now))
|
|
return true;
|
|
log_message(LOG_LEVEL_ERROR,
|
|
"Receive session exceeded its time bound (idle %us / total %us); aborting connection",
|
|
g_max_session_idle_sec, g_max_session_wall_sec);
|
|
if (!sink || sink->send_error)
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
return false;
|
|
}
|
|
|
|
int receiver_process(Config* config, int file_descriptor, const ReceiverSink* sink) {
|
|
return receiver_process_pending(config, file_descriptor, sink, NULL, 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 (plain --delete /
|
|
--delete-after / --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. See 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
|
|
transfers ownership to *pending_manifest (used by the -m receiver). */
|
|
DeleteManifest* deferred_manifest = NULL;
|
|
/* Per-directory delete session for --delete-during/--delete-delay. During the
|
|
loop it applies plans inline (during) or snapshots their extras (delay); on
|
|
a successful FINISHED it is either committed here or handed to
|
|
*pending_plans so the -m caller commits after its disk writer drained. */
|
|
DeletePlanSession* plan_session = NULL;
|
|
bool delete_limit_noted = 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) {
|
|
if (status == STATUS_KEEPALIVE) {
|
|
if (!send_status(file_descriptor, STATUS_KEEPALIVE))
|
|
goto fail;
|
|
goto next_status;
|
|
}
|
|
if (status == STATUS_ABORT) {
|
|
log_message(LOG_LEVEL_INFO, "Received abort from client, cleaning up");
|
|
goto fail;
|
|
}
|
|
if (status == STATUS_CHECK) {
|
|
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))
|
|
goto receive_error;
|
|
} else if (status == STATUS_CHECK_BATCH) {
|
|
if (!receiver_process_batch(config, file_descriptor))
|
|
goto fail;
|
|
goto next_status;
|
|
} else if (status == STATUS_MKDIR) {
|
|
File* dir = file_receive_directory(file_descriptor, config);
|
|
if (!dir || !sink->store_file(dir, sink->context))
|
|
goto receive_error;
|
|
} else if (status == STATUS_DIR_TIMES) {
|
|
if (!receiver_process_dir_times(file_descriptor, config, sink))
|
|
goto receive_error;
|
|
} else if (status == STATUS_HARDLINK) {
|
|
File* file = file_receive_hardlink(file_descriptor);
|
|
if (!file || !sink->store_file(file, sink->context))
|
|
goto receive_error;
|
|
} else if (status == STATUS_SYMLINK) {
|
|
File* sym = file_receive_symlink(file_descriptor, config);
|
|
if (!sym || !sink->store_file(sym, sink->context))
|
|
goto receive_error;
|
|
} else if (status == STATUS_SPECIAL) {
|
|
File* file = file_receive_special(file_descriptor);
|
|
if (!file || !sink->store_file(file, sink->context))
|
|
goto receive_error;
|
|
} else if (status == STATUS_MANIFEST) {
|
|
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.
|
|
When would-delete reporting is armed, enumerate (read-only) the
|
|
destination extras so the terminal STATUS_STATS frame can list them. */
|
|
if (config->use_delete && sink->would_delete) {
|
|
size_t count = 0;
|
|
if (!manifest_would_delete_list(config, manifest, sink->would_delete, &count))
|
|
log_message(LOG_LEVEL_WARNING, "dry-run: could not enumerate would-delete paths");
|
|
}
|
|
delete_manifest_free(manifest);
|
|
if (early_delete && !send_status(file_descriptor, STATUS_OK))
|
|
goto fail;
|
|
goto next_status;
|
|
}
|
|
if (early_delete) {
|
|
/* --delete-before: the whole-tree manifest is authoritative the moment
|
|
it arrives, before any file data. Delete now and acknowledge so the
|
|
sender only starts streaming once the deletion committed (or failed).
|
|
A later transfer failure does not restore these deletions. A
|
|
--max-delete-capped commit still succeeds and the transfer proceeds;
|
|
the terminal success frame reports the cap. */
|
|
DeleteCommitResult deletion = (config->use_delete || config->delete_missing_args)
|
|
? manifest_delete_all(config, manifest)
|
|
: DELETE_COMMIT_OK;
|
|
delete_manifest_free(manifest);
|
|
if (deletion == DELETE_COMMIT_ERROR) {
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
goto fail;
|
|
}
|
|
if (deletion == DELETE_COMMIT_LIMIT_REACHED && sink->note_delete_limit)
|
|
sink->note_delete_limit(sink->context);
|
|
if (!send_status(file_descriptor, STATUS_OK))
|
|
goto fail;
|
|
} else if (config->use_delete || config->delete_missing_args) {
|
|
/* Plain --delete / --delete-after and the --delete-missing-args
|
|
exact-path deletions: hold the manifest and commit it only after
|
|
STATUS_FINISHED. The per-directory modes never send this frame. */
|
|
if (deferred_manifest) {
|
|
log_message(LOG_LEVEL_ERROR, "Received a second delete manifest");
|
|
delete_manifest_free(deferred_manifest);
|
|
deferred_manifest = NULL;
|
|
delete_manifest_free(manifest);
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
goto fail;
|
|
}
|
|
deferred_manifest = manifest;
|
|
} else {
|
|
delete_manifest_free(manifest);
|
|
}
|
|
goto next_status;
|
|
} else if (status == STATUS_DELETE_PLAN) {
|
|
if (!per_dir_delete) {
|
|
log_message(LOG_LEVEL_ERROR, "Received a per-directory delete plan without a per-dir "
|
|
"delete timing");
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
goto fail;
|
|
}
|
|
if (!plan_session)
|
|
plan_session = delete_plan_session_create(config);
|
|
if (!plan_session || delete_plan_session_receive(plan_session, config, file_descriptor) != 0)
|
|
goto fail;
|
|
if (delete_plan_session_limit_reached(plan_session) && !delete_limit_noted &&
|
|
sink->note_delete_limit) {
|
|
sink->note_delete_limit(sink->context);
|
|
delete_limit_noted = true;
|
|
}
|
|
goto next_status;
|
|
} else {
|
|
File* file = file_receive(config, file_descriptor);
|
|
if (!file) {
|
|
log_message(LOG_LEVEL_ERROR, "Failed to receive file");
|
|
goto receive_error;
|
|
}
|
|
if (!sink->store_file(file, sink->context))
|
|
goto receive_error;
|
|
}
|
|
next_status:
|
|
if (!receive_status(file_descriptor, &status))
|
|
goto receive_error;
|
|
if (!receiver_note_status(&session_start, &last_progress, status, file_descriptor, sink))
|
|
goto fail;
|
|
}
|
|
if (status != STATUS_FINISHED) {
|
|
log_message(LOG_LEVEL_ERROR, "Did not receive FINISHED Status");
|
|
goto receive_error;
|
|
}
|
|
/* Commit-style (late) deletion: every data frame has been received and the
|
|
sender proved the whole tree with STATUS_FINISHED. The single-threaded
|
|
receiver stores files synchronously, so everything is on disk here and the
|
|
deletion can be committed before the --delay-updates publication in
|
|
send_success (the walker skips the staging dir, so staged files are never
|
|
treated as extras). The -m receiver passes `pending_manifest` because its
|
|
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
|
|
succeeded. */
|
|
if (deferred_manifest) {
|
|
if (pending_manifest) {
|
|
*pending_manifest = deferred_manifest;
|
|
deferred_manifest = NULL;
|
|
} else {
|
|
DeleteCommitResult deletion = manifest_delete_all(config, deferred_manifest);
|
|
delete_manifest_free(deferred_manifest);
|
|
deferred_manifest = NULL;
|
|
if (deletion == DELETE_COMMIT_ERROR) {
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
goto fail;
|
|
}
|
|
if (deletion == DELETE_COMMIT_LIMIT_REACHED && sink->note_delete_limit)
|
|
sink->note_delete_limit(sink->context);
|
|
}
|
|
}
|
|
/* Per-directory deletion: --delete-during already applied each plan inline, so
|
|
this only finishes the missing-args deletions; --delete-delay committed
|
|
nothing yet and applies its decompressed snapshot here. The -m receiver
|
|
hands the session to its caller instead, which commits after the disk
|
|
writer drained. */
|
|
if (plan_session) {
|
|
if (pending_plans) {
|
|
*pending_plans = plan_session;
|
|
plan_session = NULL;
|
|
} else {
|
|
DeleteCommitResult deletion = delete_plan_session_commit(plan_session, config);
|
|
bool limit = delete_plan_session_limit_reached(plan_session);
|
|
delete_plan_session_destroy(plan_session);
|
|
plan_session = NULL;
|
|
if (deletion == DELETE_COMMIT_ERROR) {
|
|
send_status(file_descriptor, STATUS_ERROR);
|
|
goto fail;
|
|
}
|
|
if (limit && !delete_limit_noted && sink->note_delete_limit)
|
|
sink->note_delete_limit(sink->context);
|
|
}
|
|
}
|
|
if (sink->send_success) {
|
|
if (sink->send_success_frame) {
|
|
if (!sink->send_success_frame(file_descriptor, sink->context))
|
|
goto fail;
|
|
} else if (!send_status(file_descriptor, STATUS_OK)) {
|
|
goto fail;
|
|
}
|
|
}
|
|
return 0;
|
|
|
|
fail:
|
|
/* 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
|
|
stream. */
|
|
if (deferred_manifest) {
|
|
delete_manifest_free(deferred_manifest);
|
|
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);
|
|
return -1;
|
|
}
|
|
|
|
/* ---- Single-threaded sink (used by receiver_receive_files) ---- */
|
|
|
|
typedef struct {
|
|
Config* config;
|
|
ReceiverOutcomes outcomes;
|
|
/* P7 Wave D: directory metadata accumulated during the stream, applied only
|
|
after the whole transfer (and its delete/publication phases) has run so a
|
|
child write never clobbers a directory mtime. */
|
|
DirTimeList dir_times;
|
|
/* Set when a --max-delete commit was capped; the terminal frame then carries
|
|
STATUS_DELETE_LIMIT so the sender exits 25 like rsync. */
|
|
bool delete_limit_reached;
|
|
/* End-of-transfer wire counters (protocol 2.25.0) and the -n/--dry-run
|
|
--delete would-delete path list collected while processing the manifest. */
|
|
ReceiverStats stats;
|
|
ArrayList* would_delete;
|
|
} ReceiverSaveContext;
|
|
|
|
static bool receiver_save_file(File* file, void* context_pointer) {
|
|
ReceiverSaveContext* context = context_pointer;
|
|
FileSaveResult result = FILE_SAVE_ERROR;
|
|
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;
|
|
} else {
|
|
result = file_save_to_disk_full(context->config->receive_root_directory, file, context->config);
|
|
}
|
|
/* A directory's metadata is deferred, never applied inline: collect it now
|
|
and apply it at the end. -O/--omit-dir-times and --preserve_perms/-times
|
|
are honored by dir_metadata_list_apply's caller (see
|
|
receiver_send_success_frame). */
|
|
if (result != FILE_SAVE_ERROR && file->is_dir && file->metadata &&
|
|
dir_metadata_should_capture(context->config) &&
|
|
!dir_time_list_add(&context->dir_times, file->path, file->metadata, file->xattrs)) {
|
|
file_destroy(file);
|
|
return false;
|
|
}
|
|
/* 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;
|
|
}
|
|
file_destroy(file);
|
|
return result != FILE_SAVE_ERROR;
|
|
}
|
|
|
|
static void receiver_note_delete_limit(void* context_pointer) {
|
|
ReceiverSaveContext* context = context_pointer;
|
|
context->delete_limit_reached = true;
|
|
}
|
|
|
|
static bool receiver_send_success_frame(int fd, void* context_pointer) {
|
|
ReceiverSaveContext* context = context_pointer;
|
|
Status final_status = context->delete_limit_reached ? STATUS_DELETE_LIMIT : STATUS_OK;
|
|
if (!receiver_send_stats_frame(fd, context->config, &context->stats, context->would_delete))
|
|
return false;
|
|
/* 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, final_status);
|
|
/* --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
|
|
success/outcome frame tells a --remove-source-files sender it may delete
|
|
its sources. */
|
|
if (context->config->delay_updates && context->config->delay_context) {
|
|
if (!delay_updates_publish(context->config->delay_context, context->config)) {
|
|
send_status(fd, STATUS_ERROR);
|
|
return false;
|
|
}
|
|
}
|
|
/* P7 Wave D: every child is now written and the delete / --delay-updates
|
|
phases have committed, so it is finally safe to stamp directory times.
|
|
This runs after the deferred deletion because receiver_process commits it
|
|
before calling this success frame. */
|
|
dir_metadata_list_apply(&context->dir_times, context->config->receive_root_directory,
|
|
context->config);
|
|
return receiver_send_final_success(fd, context->config, &context->outcomes, final_status);
|
|
}
|
|
|
|
int receiver_receive_files(Config* config, int file_descriptor) {
|
|
ReceiverSaveContext context = {.config = config, .outcomes = {0}};
|
|
dir_time_list_init(&context.dir_times);
|
|
context.would_delete = array_list_create(free);
|
|
if (!context.would_delete)
|
|
return -1;
|
|
ReceiverSink sink = {receiver_save_file,
|
|
&context,
|
|
true,
|
|
true,
|
|
receiver_send_success_frame,
|
|
receiver_note_delete_limit,
|
|
&context.stats,
|
|
context.would_delete};
|
|
int ret = receiver_process(config, file_descriptor, &sink);
|
|
if (ret != 0 && config->delay_updates && config->delay_context)
|
|
delay_updates_cleanup(config->delay_context);
|
|
receiver_outcomes_destroy(&context.outcomes);
|
|
dir_time_list_free(&context.dir_times);
|
|
array_list_delete(context.would_delete);
|
|
return ret;
|
|
}
|