From 616c84c2e473ec94867e37ee93205d5be291e0ec Mon Sep 17 00:00:00 2001 From: Mario Stief Date: Sun, 26 Oct 2025 19:48:26 +0100 Subject: [PATCH 1/3] Add --no-i-r-skip-unchanged for accurate progress on resumed transfers --- NEWS.md | 10 ++++++ generator.c | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++ io.c | 14 +++++++++ options.c | 11 ++++++- progress.c | 2 +- rsync.1.md | 37 +++++++++++++++++++++++ rsync.h | 2 ++ 7 files changed, 161 insertions(+), 2 deletions(-) diff --git a/NEWS.md b/NEWS.md index 96608cf94..815a5f906 100644 --- a/NEWS.md +++ b/NEWS.md @@ -2,6 +2,16 @@ ## Changes in this version: +### ENHANCEMENTS: + +- Added `--no-i-r-skip-unchanged` option to provide accurate progress reporting + for resumed transfers. This option pre-scans the destination to identify + unchanged files, skips them from processing, and adjusts `stats.total_size` + accordingly. This fixes the issue where interrupted transfers show incorrect + progress percentages (e.g., 1% to 80% instead of 0% to 100%) when resumed. + The option works for all transfer types (local, local→remote, remote→local, + daemon) and implies `--no-i-r`. + ### BUG FIXES: - ... diff --git a/generator.c b/generator.c index b56fa569a..626910e49 100644 --- a/generator.c +++ b/generator.c @@ -30,9 +30,12 @@ extern int stdout_format_has_i; extern int logfile_format_has_i; extern int am_root; extern int am_server; +extern int am_sender; extern int am_daemon; extern int inc_recurse; +extern int no_i_r_skip_unchanged; extern int relative_paths; +extern struct stats stats; extern int implied_dirs; extern int keep_dirlinks; extern int write_devices; @@ -1242,6 +1245,13 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx, return; } + if (!F_IS_ACTIVE(file)) { + /* File was marked as inactive (unchanged) during pre-scan */ + if (DEBUG_GTE(GENR, 2)) + rprintf(FINFO, "skipping inactive file: %s\n", fname); + return; + } + maybe_ATTRS_ACCURATE_TIME = always_checksum ? ATTRS_ACCURATE_TIME : 0; if (skip_dir) { @@ -2223,6 +2233,80 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo) } } +/* Pre-scan the file list to mark unchanged files and adjust stats.total_size. + * This allows accurate progress reporting on resumed transfers. */ +static void prescan_for_unchanged(const char *local_name, int f_out) +{ + int i, active_count = 0, skipped_count = 0; + char fbuf[MAXPATHLEN]; + STRUCT_STAT st; + + if (!no_i_r_skip_unchanged || !cur_flist) + return; + + if (DEBUG_GTE(GENR, 1)) + rprintf(FINFO, "pre-scanning for unchanged files\n"); + + for (i = 0; i < cur_flist->used; i++) { + struct file_struct *file = cur_flist->files[i]; + enum filetype ftype; + + if (!file || !F_IS_ACTIVE(file)) + continue; + + ftype = get_file_type(file->mode); + + /* Only check regular files */ + if (ftype != FT_REG) { + active_count++; + continue; + } + + /* Construct destination path */ + if (local_name) + strlcpy(fbuf, local_name, sizeof fbuf); + else + f_name(file, fbuf); + + /* Stat destination file */ + if (do_stat(fbuf, &st) < 0) { + active_count++; + continue; + } + + /* Check if file is unchanged */ + if (quick_check_ok(ftype, fbuf, file, &st)) { + if (DEBUG_GTE(GENR, 2)) + rprintf(FINFO, "skipping unchanged: %s\n", fbuf); + + /* Subtract from total size for accurate progress */ + stats.total_size -= F_LENGTH(file); + + /* Mark as inactive to remove from file list */ + clear_file(file); + skipped_count++; + } else { + active_count++; + } + } + + /* Update stats to reflect skipped files */ + stats.num_files = active_count; + stats.num_skipped_files = skipped_count; + + if (DEBUG_GTE(GENR, 1)) + rprintf(FINFO, "skipped %d unchanged files, %d active, adjusted size: %.2f GB\n", + skipped_count, active_count, (double)stats.total_size / 1024 / 1024 / 1024); + + /* Send skipped count and adjusted total_size to sender for accurate progress display */ + if (f_out >= 0) { + char buf[12]; + SIVAL(buf, 0, skipped_count); + SIVAL64(buf, 4, stats.total_size); + send_msg(MSG_FLIST_COUNT, buf, 12, -1); + } +} + void generate_files(int f_out, const char *local_name) { int i, ndx, next_loopchk = 0; @@ -2279,6 +2363,9 @@ void generate_files(int f_out, const char *local_name) dflt_perms = (ACCESSPERMS & ~orig_umask); + /* Pre-scan to mark unchanged files for accurate progress reporting */ + prescan_for_unchanged(local_name, f_out); + do { #ifdef SUPPORT_HARD_LINKS if (preserve_hard_links && inc_recurse) { diff --git a/io.c b/io.c index 436bed45e..52c63c2ee 100644 --- a/io.c +++ b/io.c @@ -1511,6 +1511,20 @@ static void read_a_msg(void) raw_read_buf((char*)&stats.total_read, sizeof stats.total_read); iobuf.in_multiplexed = 1; break; + case MSG_FLIST_COUNT: + if (msg_bytes != 12 || !am_sender) + goto invalid_msg; + val = raw_read_int(); + stats.num_skipped_files = val; + /* Adjust total file count to reflect only active files */ + stats.num_files -= val; + /* Read and update adjusted total_size for accurate progress percentage */ + raw_read_buf((char*)&stats.total_size, 8); + iobuf.in_multiplexed = 1; + if (DEBUG_GTE(IO, 1)) + rprintf(FINFO, "[%s] received MSG_FLIST_COUNT: %d skipped, %d active, size: %.2f GB\n", + who_am_i(), val, stats.num_files, (double)stats.total_size / 1024 / 1024 / 1024); + break; case MSG_REDO: if (msg_bytes != 4 || !am_generator) goto invalid_msg; diff --git a/options.c b/options.c index 74b39bf6a..23363b625 100644 --- a/options.c +++ b/options.c @@ -112,6 +112,7 @@ int human_readable = 1; int recurse = 0; int mkpath_dest_arg = 0; int allow_inc_recurse = 1; +int no_i_r_skip_unchanged = 0; int xfer_dirs = -1; int am_daemon = 0; int connect_timeout = 0; @@ -585,7 +586,7 @@ enum {OPT_SERVER = 1000, OPT_DAEMON, OPT_SENDER, OPT_EXCLUDE, OPT_EXCLUDE_FROM, OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG, OPT_BLOCK_SIZE, OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN, OPT_BWLIMIT, OPT_STDERR, OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS, OPT_OLD_ARGS, - OPT_STOP_AFTER, OPT_STOP_AT, + OPT_STOP_AFTER, OPT_STOP_AT, OPT_NO_I_R_SKIP_UNCHANGED, OPT_REFUSED_BASE = 9000}; static struct poptOption long_options[] = { @@ -616,6 +617,9 @@ static struct poptOption long_options[] = { {"no-inc-recursive", 0, POPT_ARG_VAL, &allow_inc_recurse, 0, 0, 0 }, {"i-r", 0, POPT_ARG_VAL, &allow_inc_recurse, 1, 0, 0 }, {"no-i-r", 0, POPT_ARG_VAL, &allow_inc_recurse, 0, 0, 0 }, + {"no-inc-recursive-skip-unchanged", 0, POPT_ARG_NONE, 0, OPT_NO_I_R_SKIP_UNCHANGED, 0, 0 }, + {"no-i-r-skip-unchanged", 0, POPT_ARG_NONE, 0, OPT_NO_I_R_SKIP_UNCHANGED, 0, 0 }, + {"no-i-r-s-u", 0, POPT_ARG_NONE, 0, OPT_NO_I_R_SKIP_UNCHANGED, 0, 0 }, {"dirs", 'd', POPT_ARG_VAL, &xfer_dirs, 2, 0, 0 }, {"no-dirs", 0, POPT_ARG_VAL, &xfer_dirs, 0, 0, 0 }, {"no-d", 0, POPT_ARG_VAL, &xfer_dirs, 0, 0, 0 }, @@ -1900,6 +1904,11 @@ int parse_arguments(int *argc_p, const char ***argv_p) break; #endif + case OPT_NO_I_R_SKIP_UNCHANGED: + no_i_r_skip_unchanged = 1; + allow_inc_recurse = 0; + break; + case OPT_STDERR: { int len; arg = poptGetOptArg(pc); diff --git a/progress.c b/progress.c index 87207fbfa..d9eb12ae8 100644 --- a/progress.c +++ b/progress.c @@ -78,7 +78,7 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l int len = snprintf(eol, sizeof eol, " (xfr#%d, %s-chk=%d/%d)\n", stats.xferred_files, flist_eof ? "to" : "ir", - stats.num_files - current_file_index - 1, + stats.num_files - (current_file_index - stats.num_skipped_files) - 1, stats.num_files); if (INFO_GTE(PROGRESS, 2)) { static int last_len = 0; diff --git a/rsync.1.md b/rsync.1.md index 2b4b75087..e00e13516 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -909,6 +909,43 @@ expand it. before it begins to transfer files. See [`--inc-recursive`](#opt) for more info. +0. `--no-inc-recursive-skip-unchanged`, `--no-i-r-skip-unchanged`, `--no-i-r-s-u` + + This option combines [`--no-i-r`](#opt) with pre-scanning to skip unchanged + files, providing accurate progress reporting for resumed transfers. When + using [`--info=progress2`](#opt), interrupted transfers that are resumed + normally show incorrect progress percentages because `stats.total_size` + includes already-transferred files. This option pre-scans the destination + during generator initialization, marks unchanged files for skipping, and + adjusts `stats.total_size` accordingly, resulting in accurate 0% to 100% + progress reporting. + + The pre-scan uses the same comparison logic as normal rsync operations + (checking size, mtime, checksums if [`--checksum`](#opt) is used, etc.). + Files determined to be unchanged are completely skipped from processing, + reducing both CPU and I/O overhead while fixing progress reporting. + + This option works for all transfer types: local-to-local, local-to-remote, + remote-to-local, and daemon transfers. Because the generator runs on the + receiver side and has access to destination files in all scenarios, the + feature functions correctly regardless of transfer direction. + + This option implies [`--no-i-r`](#opt), so it requires the full file list + to be available before processing begins. The performance overhead is + minimal since the pre-scan performs the same stat operations that would + occur anyway during normal generator operation, just earlier in the pipeline. + + Example use cases: + + - Resuming interrupted transfers with accurate progress: + `rsync -av --no-i-r-s-u --info=progress2 src/ host:dest/` + + - Large synchronization with mostly unchanged files: + `rsync -av --no-inc-recursive-skip-unchanged /data/ /backup/` + + - Remote-to-local transfer with progress tracking: + `rsync -av --no-i-r-skip-unchanged --info=progress2 host:src/ dest/` + 0. `--relative`, `-R` Use relative paths. This means that the full path names specified on the diff --git a/rsync.h b/rsync.h index 479ac4848..d9f6b8540 100644 --- a/rsync.h +++ b/rsync.h @@ -269,6 +269,7 @@ enum msgcode { MSG_LOG=FLOG, MSG_CLIENT=FCLIENT, /* sibling logging */ MSG_REDO=9, /* reprocess indicated flist index */ MSG_STATS=10, /* message has stats data for generator */ + MSG_FLIST_COUNT=11, /* generator sends updated file count to sender */ MSG_IO_ERROR=22,/* the sending side had an I/O error */ MSG_IO_TIMEOUT=33,/* tell client about a daemon's timeout value */ MSG_NOOP=42, /* a do-nothing message (legacy protocol-30 only) */ @@ -1044,6 +1045,7 @@ struct stats { int created_files, created_dirs, created_symlinks, created_devices, created_specials; int deleted_files, deleted_dirs, deleted_symlinks, deleted_devices, deleted_specials; int xferred_files; + int num_skipped_files; /* files marked as unchanged by --no-i-r-skip-unchanged */ }; struct chmod_mode_struct; From da99012afc3d5990e32b52fbbd106dd949a87fe3 Mon Sep 17 00:00:00 2001 From: Mario Stief Date: Mon, 27 Oct 2025 22:39:01 +0100 Subject: [PATCH 2/3] Fix protocol compatibility: Add feature negotiation to work with version <=3.4.1 --- compat.c | 7 +++++++ generator.c | 10 ++++++++-- io.c | 3 ++- options.c | 1 + 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/compat.c b/compat.c index 5b3bfff0c..c73e9643d 100644 --- a/compat.c +++ b/compat.c @@ -82,6 +82,7 @@ int proper_seed_order = 0; int inplace_partial = 0; int do_negotiated_strings = 0; int xmit_id0_names = 0; +int skip_unchanged_negotiated = 0; struct name_num_item *xattr_sum_nni; int xattr_sum_len = 0; @@ -124,6 +125,7 @@ struct name_num_obj valid_compressions = { #define CF_INPLACE_PARTIAL_DIR (1<<6) #define CF_VARINT_FLIST_FLAGS (1<<7) #define CF_ID0_NAMES (1<<8) +#define CF_SKIP_UNCHANGED (1<<9) static const char *client_info; @@ -727,6 +729,8 @@ void setup_protocol(int f_out,int f_in) compat_flags |= CF_INPLACE_PARTIAL_DIR; if (strchr(client_info, 'u') != NULL) compat_flags |= CF_ID0_NAMES; + if (strchr(client_info, 'U') != NULL) + compat_flags |= CF_SKIP_UNCHANGED; if (strchr(client_info, 'v') != NULL) { do_negotiated_strings = 1; compat_flags |= CF_VARINT_FLIST_FLAGS; @@ -748,6 +752,9 @@ void setup_protocol(int f_out,int f_in) proper_seed_order = compat_flags & CF_CHKSUM_SEED_FIX ? 1 : 0; xfer_flags_as_varint = compat_flags & CF_VARINT_FLIST_FLAGS ? 1 : 0; xmit_id0_names = compat_flags & CF_ID0_NAMES ? 1 : 0; + if (compat_flags & CF_SKIP_UNCHANGED) { + skip_unchanged_negotiated = 1; + } if (!xfer_flags_as_varint && preserve_crtimes) { fprintf(stderr, "Both rsync versions must be at least 3.2.0 for --crtimes.\n"); exit_cleanup(RERR_PROTOCOL); diff --git a/generator.c b/generator.c index 626910e49..53ebe5122 100644 --- a/generator.c +++ b/generator.c @@ -34,6 +34,7 @@ extern int am_sender; extern int am_daemon; extern int inc_recurse; extern int no_i_r_skip_unchanged; +extern int skip_unchanged_negotiated; extern int relative_paths; extern struct stats stats; extern int implied_dirs; @@ -2241,7 +2242,8 @@ static void prescan_for_unchanged(const char *local_name, int f_out) char fbuf[MAXPATHLEN]; STRUCT_STAT st; - if (!no_i_r_skip_unchanged || !cur_flist) + /* Only prescan if feature was negotiated with remote side */ + if (!no_i_r_skip_unchanged || !skip_unchanged_negotiated || !cur_flist) return; if (DEBUG_GTE(GENR, 1)) @@ -2299,7 +2301,7 @@ static void prescan_for_unchanged(const char *local_name, int f_out) skipped_count, active_count, (double)stats.total_size / 1024 / 1024 / 1024); /* Send skipped count and adjusted total_size to sender for accurate progress display */ - if (f_out >= 0) { + if (f_out >= 0 && skip_unchanged_negotiated) { char buf[12]; SIVAL(buf, 0, skipped_count); SIVAL64(buf, 4, stats.total_size); @@ -2364,6 +2366,10 @@ void generate_files(int f_out, const char *local_name) dflt_perms = (ACCESSPERMS & ~orig_umask); /* Pre-scan to mark unchanged files for accurate progress reporting */ + if (no_i_r_skip_unchanged && !skip_unchanged_negotiated) { + rprintf(FWARNING, "WARNING: --no-i-r-skip-unchanged requested but not supported by remote rsync.\n"); + rprintf(FWARNING, " Falling back to standard --no-i-r behavior (no progress optimization).\n"); + } prescan_for_unchanged(local_name, f_out); do { diff --git a/io.c b/io.c index 52c63c2ee..e0cd900ad 100644 --- a/io.c +++ b/io.c @@ -60,6 +60,7 @@ extern int daemon_connection; extern int protocol_version; extern int remove_source_files; extern int preserve_hard_links; +extern int skip_unchanged_negotiated; extern BOOL extra_flist_sending_enabled; extern BOOL flush_ok_after_signal; extern struct stats stats; @@ -1512,7 +1513,7 @@ static void read_a_msg(void) iobuf.in_multiplexed = 1; break; case MSG_FLIST_COUNT: - if (msg_bytes != 12 || !am_sender) + if (msg_bytes != 12 || !am_sender || !skip_unchanged_negotiated) goto invalid_msg; val = raw_read_int(); stats.num_skipped_files = val; diff --git a/options.c b/options.c index 23363b625..0ca2fa20e 100644 --- a/options.c +++ b/options.c @@ -3050,6 +3050,7 @@ int maybe_add_e_option(char *buf, int buf_len) buf[x++] = 'I'; /* support inplace_partial behavior */ buf[x++] = 'v'; /* use varint for flist & compat flags; negotiate checksum */ buf[x++] = 'u'; /* include name of uid 0 & gid 0 in the id map */ + buf[x++] = 'U'; /* support --no-i-r-skip-unchanged feature */ /* NOTE: Avoid using 'V' -- it was represented with the high bit of a write_byte() that became a write_varint(). */ } From 03c8fd59976ea62c8bec9096aaa866f1d524b441 Mon Sep 17 00:00:00 2001 From: Mario Stief Date: Wed, 29 Oct 2025 21:09:59 +0100 Subject: [PATCH 3/3] Fix --no-i-r-s-u: add IPC for stats sync and active file tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes progress reporting for local→remote transfers by sending adjusted stats from receiver to sender via MSG_FLIST_COUNT IPC message. --- compat.c | 7 ------- generator.c | 25 +++++-------------------- io.c | 8 +------- main.c | 13 +++++++++++++ options.c | 4 +++- progress.c | 6 +++++- rsync.h | 3 ++- 7 files changed, 29 insertions(+), 37 deletions(-) diff --git a/compat.c b/compat.c index c73e9643d..5b3bfff0c 100644 --- a/compat.c +++ b/compat.c @@ -82,7 +82,6 @@ int proper_seed_order = 0; int inplace_partial = 0; int do_negotiated_strings = 0; int xmit_id0_names = 0; -int skip_unchanged_negotiated = 0; struct name_num_item *xattr_sum_nni; int xattr_sum_len = 0; @@ -125,7 +124,6 @@ struct name_num_obj valid_compressions = { #define CF_INPLACE_PARTIAL_DIR (1<<6) #define CF_VARINT_FLIST_FLAGS (1<<7) #define CF_ID0_NAMES (1<<8) -#define CF_SKIP_UNCHANGED (1<<9) static const char *client_info; @@ -729,8 +727,6 @@ void setup_protocol(int f_out,int f_in) compat_flags |= CF_INPLACE_PARTIAL_DIR; if (strchr(client_info, 'u') != NULL) compat_flags |= CF_ID0_NAMES; - if (strchr(client_info, 'U') != NULL) - compat_flags |= CF_SKIP_UNCHANGED; if (strchr(client_info, 'v') != NULL) { do_negotiated_strings = 1; compat_flags |= CF_VARINT_FLIST_FLAGS; @@ -752,9 +748,6 @@ void setup_protocol(int f_out,int f_in) proper_seed_order = compat_flags & CF_CHKSUM_SEED_FIX ? 1 : 0; xfer_flags_as_varint = compat_flags & CF_VARINT_FLIST_FLAGS ? 1 : 0; xmit_id0_names = compat_flags & CF_ID0_NAMES ? 1 : 0; - if (compat_flags & CF_SKIP_UNCHANGED) { - skip_unchanged_negotiated = 1; - } if (!xfer_flags_as_varint && preserve_crtimes) { fprintf(stderr, "Both rsync versions must be at least 3.2.0 for --crtimes.\n"); exit_cleanup(RERR_PROTOCOL); diff --git a/generator.c b/generator.c index 53ebe5122..a67f58bef 100644 --- a/generator.c +++ b/generator.c @@ -34,7 +34,6 @@ extern int am_sender; extern int am_daemon; extern int inc_recurse; extern int no_i_r_skip_unchanged; -extern int skip_unchanged_negotiated; extern int relative_paths; extern struct stats stats; extern int implied_dirs; @@ -2236,14 +2235,14 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo) /* Pre-scan the file list to mark unchanged files and adjust stats.total_size. * This allows accurate progress reporting on resumed transfers. */ -static void prescan_for_unchanged(const char *local_name, int f_out) +void prescan_for_unchanged(const char *local_name) { int i, active_count = 0, skipped_count = 0; char fbuf[MAXPATHLEN]; STRUCT_STAT st; - /* Only prescan if feature was negotiated with remote side */ - if (!no_i_r_skip_unchanged || !skip_unchanged_negotiated || !cur_flist) + /* Only prescan if feature is enabled */ + if (!no_i_r_skip_unchanged || !cur_flist) return; if (DEBUG_GTE(GENR, 1)) @@ -2292,21 +2291,13 @@ static void prescan_for_unchanged(const char *local_name, int f_out) } } - /* Update stats to reflect skipped files */ + /* Update stats to reflect only active files for progress display */ stats.num_files = active_count; stats.num_skipped_files = skipped_count; if (DEBUG_GTE(GENR, 1)) rprintf(FINFO, "skipped %d unchanged files, %d active, adjusted size: %.2f GB\n", skipped_count, active_count, (double)stats.total_size / 1024 / 1024 / 1024); - - /* Send skipped count and adjusted total_size to sender for accurate progress display */ - if (f_out >= 0 && skip_unchanged_negotiated) { - char buf[12]; - SIVAL(buf, 0, skipped_count); - SIVAL64(buf, 4, stats.total_size); - send_msg(MSG_FLIST_COUNT, buf, 12, -1); - } } void generate_files(int f_out, const char *local_name) @@ -2364,13 +2355,7 @@ void generate_files(int f_out, const char *local_name) } dflt_perms = (ACCESSPERMS & ~orig_umask); - - /* Pre-scan to mark unchanged files for accurate progress reporting */ - if (no_i_r_skip_unchanged && !skip_unchanged_negotiated) { - rprintf(FWARNING, "WARNING: --no-i-r-skip-unchanged requested but not supported by remote rsync.\n"); - rprintf(FWARNING, " Falling back to standard --no-i-r behavior (no progress optimization).\n"); - } - prescan_for_unchanged(local_name, f_out); + stats.current_active_index = 0; do { #ifdef SUPPORT_HARD_LINKS diff --git a/io.c b/io.c index e0cd900ad..38b1b7b4d 100644 --- a/io.c +++ b/io.c @@ -60,7 +60,6 @@ extern int daemon_connection; extern int protocol_version; extern int remove_source_files; extern int preserve_hard_links; -extern int skip_unchanged_negotiated; extern BOOL extra_flist_sending_enabled; extern BOOL flush_ok_after_signal; extern struct stats stats; @@ -1513,18 +1512,13 @@ static void read_a_msg(void) iobuf.in_multiplexed = 1; break; case MSG_FLIST_COUNT: - if (msg_bytes != 12 || !am_sender || !skip_unchanged_negotiated) + if (msg_bytes != 12 || !am_sender) goto invalid_msg; val = raw_read_int(); stats.num_skipped_files = val; - /* Adjust total file count to reflect only active files */ stats.num_files -= val; - /* Read and update adjusted total_size for accurate progress percentage */ raw_read_buf((char*)&stats.total_size, 8); iobuf.in_multiplexed = 1; - if (DEBUG_GTE(IO, 1)) - rprintf(FINFO, "[%s] received MSG_FLIST_COUNT: %d skipped, %d active, size: %.2f GB\n", - who_am_i(), val, stats.num_files, (double)stats.total_size / 1024 / 1024 / 1024); break; case MSG_REDO: if (msg_bytes != 4 || !am_generator) diff --git a/main.c b/main.c index 9d764e16b..5dd59673b 100644 --- a/main.c +++ b/main.c @@ -40,6 +40,7 @@ extern int am_server; extern int am_sender; extern int am_daemon; extern int inc_recurse; +extern int no_i_r_skip_unchanged; extern int blocking_io; extern int always_checksum; extern int remove_source_files; @@ -1029,6 +1030,18 @@ static int do_recv(int f_in, int f_out, char *local_name) io_flush(FULL_FLUSH); + /* Pre-scan for unchanged files before forking so both processes get updated stats */ + prescan_for_unchanged(local_name); + + /* Send adjusted stats to sender for accurate progress (local→remote transfers) */ + if (no_i_r_skip_unchanged && stats.num_skipped_files > 0) { + char buf[12]; + SIVAL(buf, 0, stats.num_skipped_files); + SIVAL64(buf, 4, stats.total_size); + send_msg(MSG_FLIST_COUNT, buf, 12, 0); + io_flush(NORMAL_FLUSH); + } + if ((pid = do_fork()) == -1) { rsyserr(FERROR, errno, "fork failed in do_recv"); exit_cleanup(RERR_IPC); diff --git a/options.c b/options.c index 0ca2fa20e..f31a53115 100644 --- a/options.c +++ b/options.c @@ -2996,6 +2996,9 @@ void server_options(char **args, int *argc_p) if (mkpath_dest_arg && am_sender) args[ac++] = "--mkpath"; + if (no_i_r_skip_unchanged) + args[ac++] = "--no-i-r-skip-unchanged"; + if (ac > MAX_SERVER_ARGS) { /* Not possible... */ rprintf(FERROR, "argc overflow in server_options().\n"); exit_cleanup(RERR_MALLOC); @@ -3050,7 +3053,6 @@ int maybe_add_e_option(char *buf, int buf_len) buf[x++] = 'I'; /* support inplace_partial behavior */ buf[x++] = 'v'; /* use varint for flist & compat flags; negotiate checksum */ buf[x++] = 'u'; /* include name of uid 0 & gid 0 in the id map */ - buf[x++] = 'U'; /* support --no-i-r-skip-unchanged feature */ /* NOTE: Avoid using 'V' -- it was represented with the high bit of a write_byte() that became a write_varint(). */ } diff --git a/progress.c b/progress.c index d9eb12ae8..d5f70bf29 100644 --- a/progress.c +++ b/progress.c @@ -78,7 +78,7 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l int len = snprintf(eol, sizeof eol, " (xfr#%d, %s-chk=%d/%d)\n", stats.xferred_files, flist_eof ? "to" : "ir", - stats.num_files - (current_file_index - stats.num_skipped_files) - 1, + stats.num_files - stats.current_active_index, stats.num_files); if (INFO_GTE(PROGRESS, 2)) { static int last_len = 0; @@ -153,6 +153,10 @@ void set_current_file_index(struct file_struct *file, int ndx) else current_file_index = ndx; current_file_index -= cur_flist->flist_num; + + /* Track active file index for accurate progress with --no-i-r-skip-unchanged */ + if (file && F_IS_ACTIVE(file)) + stats.current_active_index++; } void instant_progress(const char *fname) diff --git a/rsync.h b/rsync.h index d9f6b8540..50f8d1e86 100644 --- a/rsync.h +++ b/rsync.h @@ -269,7 +269,7 @@ enum msgcode { MSG_LOG=FLOG, MSG_CLIENT=FCLIENT, /* sibling logging */ MSG_REDO=9, /* reprocess indicated flist index */ MSG_STATS=10, /* message has stats data for generator */ - MSG_FLIST_COUNT=11, /* generator sends updated file count to sender */ + MSG_FLIST_COUNT=11, /* receiver sends adjusted stats to sender after prescan */ MSG_IO_ERROR=22,/* the sending side had an I/O error */ MSG_IO_TIMEOUT=33,/* tell client about a daemon's timeout value */ MSG_NOOP=42, /* a do-nothing message (legacy protocol-30 only) */ @@ -1046,6 +1046,7 @@ struct stats { int deleted_files, deleted_dirs, deleted_symlinks, deleted_devices, deleted_specials; int xferred_files; int num_skipped_files; /* files marked as unchanged by --no-i-r-skip-unchanged */ + int current_active_index; /* current index among active files (excluding skipped) */ }; struct chmod_mode_struct;