diff --git a/src/client/client_cli.c b/src/client/client_cli.c index d38078d..6cde795 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -2349,6 +2349,15 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool config->preserve_times = true; } + /* --ignore-existing is a receiver-side existence policy: the receiver must + * answer "skip" BEFORE the sender transmits any payload, which only the + * per-file STATUS_CHECK handshake provides. Imply --incremental here (after + * the auto-preserve capture above, so a bare --ignore-existing does not gain + * -p/-t, which rsync likewise does not imply) so an existing destination is + * skipped on the wire instead of being streamed and discarded. */ + if (config->ignore_existing) + config->use_incremental = true; + /* Derive the transport bit from the FINAL parsed flags. Every * preservation/ownership option that needs the metadata frame (per-attribute * perms/times/owner/group, atimes/crtimes, executability, xattrs/acls, diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index 6da4325..f9bac25 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -1764,6 +1764,7 @@ typedef struct { long long check_mtime_nsec; uint8_t check_digest[CHECKSUM_MAX_DIGEST_LEN]; size_t check_digest_len; + bool dest_exists; /* any destination entry exists (lstat succeeded) */ bool has_old_file; int old_fd; struct stat old_st; @@ -1860,6 +1861,9 @@ static IncrementalCheckOutcome incremental_check_open_destination(IncrementalChe char* leaf = NULL; int parent_fd = file_open_secure_parent(full_path, &leaf, false); if (parent_fd >= 0) { + struct stat dest_st; + if (fstatat(parent_fd, leaf, &dest_st, AT_SYMLINK_NOFOLLOW) == 0) + state->dest_exists = true; /* O_NONBLOCK: an existing FIFO at the destination must not block this openat(); the S_ISREG gate below rejects the non-regular entry. */ state->old_fd = openat(parent_fd, leaf, O_RDONLY | O_CLOEXEC | O_NOFOLLOW | O_NONBLOCK); @@ -1903,6 +1907,21 @@ static IncrementalCheckOutcome incremental_check_report_dest_info(IncrementalChe return INCREMENTAL_CONTINUE; } +/* --ignore-existing short-circuit. The receiver must answer "skip" (STATUS_OK) + BEFORE the sender transmits any payload, otherwise the whole file crosses the + wire only to be discarded at write time. rsync skips an existing destination + entry regardless of its content or type, so the reply depends only on the + lstat existence probe; the ordinary --ignore-existing checks inside + file_receive remain as defense-in-depth for the frame types that have no + per-file check (directories/symlinks/specials/hard-links). */ +static IncrementalCheckOutcome incremental_check_ignore_existing(IncrementalCheckState* state) { + if (!state->config->ignore_existing || !state->dest_exists) + return INCREMENTAL_CONTINUE; + if (!send_status(state->fd, STATUS_OK)) + return INCREMENTAL_ERROR; + return INCREMENTAL_SKIP; +} + /* Metadata-only (and, when --checksum forces it, content) up-to-date decision. Loads the old contents only when a checksum comparison or delta needs them. */ static IncrementalCheckOutcome incremental_check_quick_skip(IncrementalCheckState* state, @@ -2351,6 +2370,16 @@ File* receive_incremental_check_ex(int fd, const Config* config, bool* skipped, if (outcome == INCREMENTAL_ERROR) goto done; + /* --ignore-existing must answer before any data is requested; it takes + precedence over the metadata up-to-date check below. */ + outcome = incremental_check_ignore_existing(&state); + if (outcome == INCREMENTAL_ERROR) + goto done; + if (outcome == INCREMENTAL_SKIP) { + *skipped = true; + goto done; + } + outcome = incremental_check_quick_skip(&state, &try_delta); if (outcome == INCREMENTAL_ERROR) goto done;