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:
+15
-5
@@ -205,7 +205,8 @@ static bool is_dir_in_manifest(const char* rel_path, ArrayList* manifest) {
|
||||
}
|
||||
|
||||
static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifest,
|
||||
size_t max_delete, size_t* deleted_count) {
|
||||
size_t max_delete, size_t* deleted_count,
|
||||
const char* skip_root_child) {
|
||||
int scanfd = dup(dirfd);
|
||||
if (scanfd < 0)
|
||||
return false;
|
||||
@@ -219,6 +220,13 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
/* A --delay-updates run keeps its staging directory as a direct child of
|
||||
the receive root. Its contents are not manifest entries yet (they are
|
||||
published after deletion), so descending into it would delete every
|
||||
staged file as an "extra". Skip only the top-level staging name; nested
|
||||
directories with the same name are ordinary destination content. */
|
||||
if (rel_path[0] == '\0' && skip_root_child && strcmp(entry->d_name, skip_root_child) == 0)
|
||||
continue;
|
||||
char* child_rel = path_cat((char*)rel_path, entry->d_name);
|
||||
if (!child_rel) {
|
||||
operation_ok = false;
|
||||
@@ -240,7 +248,8 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
|
||||
int childfd = openat(dirfd, entry->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||
bool child_removed = false;
|
||||
if (childfd >= 0) {
|
||||
child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count);
|
||||
child_removed = delete_extras_fd(childfd, child_rel, manifest, max_delete, deleted_count,
|
||||
skip_root_child);
|
||||
if (!child_removed)
|
||||
operation_ok = false;
|
||||
close(childfd);
|
||||
@@ -291,7 +300,8 @@ static bool delete_extras_fd(int dirfd, const char* rel_path, ArrayList* manifes
|
||||
return operation_ok;
|
||||
}
|
||||
|
||||
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete) {
|
||||
bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t max_delete,
|
||||
const char* skip_root_child) {
|
||||
if (!manifest)
|
||||
return false;
|
||||
int rootfd;
|
||||
@@ -308,14 +318,14 @@ bool delete_extras_limited(const char* dest_root, ArrayList* manifest, size_t ma
|
||||
if (rootfd < 0)
|
||||
return false;
|
||||
size_t deleted_count = 0;
|
||||
bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count);
|
||||
bool ok = delete_extras_fd(rootfd, "", manifest, max_delete, &deleted_count, skip_root_child);
|
||||
if (close(rootfd) != 0)
|
||||
ok = false;
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool delete_extras(const char* dest_root, ArrayList* manifest) {
|
||||
return delete_extras_limited(dest_root, manifest, SIZE_MAX);
|
||||
return delete_extras_limited(dest_root, manifest, SIZE_MAX, NULL);
|
||||
}
|
||||
|
||||
bool has_path_traversal(const char* path) {
|
||||
|
||||
Reference in New Issue
Block a user