fix: --delay-updates delete/backup collisions, publish-failure test, staging lock
Review fixes for --delay-updates: - --delete no longer deletes the staged files: the delete walker gains a skip_root_child parameter and receive_manifest passes DELAY_UPDATES_STAGING_DIR when delay_updates is active, so deletion removes genuine extras while the staging dir (a direct child of the receive root) is left for publication in both single and -m modes. - --backup-dir is rejected when it collides with the reserved internal staging name .fastsync-stage (trailing slash normalized), in client validation and in the received-config wire validation, preventing old backups from being silently installed as new files. - Staging dir is now held under an exclusive advisory flock for the whole transfer (context lifetime): two simultaneous delayed transfers to one destination root no longer share/destroy each other's staged data - the second fails cleanly. Cleanup only touches the staging dir when this context owns the lock, so a lock-contention failure cannot wipe a live session. - Post-publish staging cleanup now returns/logs instead of discarding failures (warning when the staging dir cannot be fully removed). - Reworked the publish-failure integration test to exercise real mid-publish semantics (top-level file published, nested rename fails, no rollback, sources retained under --remove-source-files) and added integration tests for --delete + --delay-updates ordering and reserved --backup-dir rejection. - RSYNC_COMPAT note documents delete ordering, the reserved-name hazard, and the concurrency guard.
This commit is contained in:
+18
-12
@@ -38,20 +38,21 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
|
||||
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)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool sparse = config->preserve_sparse;
|
||||
bool preserve_executability = config->use_executability;
|
||||
|
||||
if (config && config->existing && !file_path_exists_secure(destination_path))
|
||||
if (config->existing && !file_path_exists_secure(destination_path))
|
||||
return FILE_SAVE_SKIPPED;
|
||||
if (config && config->ignore_existing && file_path_exists_secure(destination_path))
|
||||
if (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))
|
||||
if (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) {
|
||||
if (metadata && 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;
|
||||
@@ -75,9 +76,9 @@ static FileSaveResult file_stage_delayed_update(const char* root_directory,
|
||||
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);
|
||||
bool ok =
|
||||
file_to_disk_secure_with_fsync(staged_path, file->data->data, file->data->size, false, sparse,
|
||||
metadata, preserve_executability, config->use_fsync, NULL);
|
||||
if (!ok) {
|
||||
free(staged_path);
|
||||
return FILE_SAVE_ERROR;
|
||||
@@ -796,8 +797,13 @@ int receive_manifest(int fd, const Config* config, int* next_status) {
|
||||
return *status_out == STATUS_FINISHED ? 0 : -1;
|
||||
}
|
||||
fprintf(stderr, "Deleting files not in manifest...\n");
|
||||
bool deletion_ok =
|
||||
delete_extras_limited(config->receive_root_directory, manifest, MAX_SERVER_DELETE_COUNT);
|
||||
/* With --delay-updates the staged (not yet published) files live directly
|
||||
under the receive root in the staging directory; the delete walker must
|
||||
not treat them as extras or it would remove every staged file before it
|
||||
can be published. */
|
||||
const char* skip_staging = config->delay_updates ? DELAY_UPDATES_STAGING_DIR : NULL;
|
||||
bool deletion_ok = delete_extras_limited(config->receive_root_directory, manifest,
|
||||
MAX_SERVER_DELETE_COUNT, skip_staging);
|
||||
array_list_delete(manifest);
|
||||
if (!deletion_ok)
|
||||
send_status(fd, STATUS_ERROR);
|
||||
|
||||
Reference in New Issue
Block a user