Skip to content

Commit 616c84c

Browse files
committed
Add --no-i-r-skip-unchanged for accurate progress on resumed transfers
1 parent 797e17f commit 616c84c

File tree

7 files changed

+161
-2
lines changed

7 files changed

+161
-2
lines changed

NEWS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## Changes in this version:
44

5+
### ENHANCEMENTS:
6+
7+
- Added `--no-i-r-skip-unchanged` option to provide accurate progress reporting
8+
for resumed transfers. This option pre-scans the destination to identify
9+
unchanged files, skips them from processing, and adjusts `stats.total_size`
10+
accordingly. This fixes the issue where interrupted transfers show incorrect
11+
progress percentages (e.g., 1% to 80% instead of 0% to 100%) when resumed.
12+
The option works for all transfer types (local, local→remote, remote→local,
13+
daemon) and implies `--no-i-r`.
14+
515
### BUG FIXES:
616

717
- ...

generator.c

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,12 @@ extern int stdout_format_has_i;
3030
extern int logfile_format_has_i;
3131
extern int am_root;
3232
extern int am_server;
33+
extern int am_sender;
3334
extern int am_daemon;
3435
extern int inc_recurse;
36+
extern int no_i_r_skip_unchanged;
3537
extern int relative_paths;
38+
extern struct stats stats;
3639
extern int implied_dirs;
3740
extern int keep_dirlinks;
3841
extern int write_devices;
@@ -1242,6 +1245,13 @@ static void recv_generator(char *fname, struct file_struct *file, int ndx,
12421245
return;
12431246
}
12441247

1248+
if (!F_IS_ACTIVE(file)) {
1249+
/* File was marked as inactive (unchanged) during pre-scan */
1250+
if (DEBUG_GTE(GENR, 2))
1251+
rprintf(FINFO, "skipping inactive file: %s\n", fname);
1252+
return;
1253+
}
1254+
12451255
maybe_ATTRS_ACCURATE_TIME = always_checksum ? ATTRS_ACCURATE_TIME : 0;
12461256

12471257
if (skip_dir) {
@@ -2223,6 +2233,80 @@ void check_for_finished_files(int itemizing, enum logcode code, int check_redo)
22232233
}
22242234
}
22252235

2236+
/* Pre-scan the file list to mark unchanged files and adjust stats.total_size.
2237+
* This allows accurate progress reporting on resumed transfers. */
2238+
static void prescan_for_unchanged(const char *local_name, int f_out)
2239+
{
2240+
int i, active_count = 0, skipped_count = 0;
2241+
char fbuf[MAXPATHLEN];
2242+
STRUCT_STAT st;
2243+
2244+
if (!no_i_r_skip_unchanged || !cur_flist)
2245+
return;
2246+
2247+
if (DEBUG_GTE(GENR, 1))
2248+
rprintf(FINFO, "pre-scanning for unchanged files\n");
2249+
2250+
for (i = 0; i < cur_flist->used; i++) {
2251+
struct file_struct *file = cur_flist->files[i];
2252+
enum filetype ftype;
2253+
2254+
if (!file || !F_IS_ACTIVE(file))
2255+
continue;
2256+
2257+
ftype = get_file_type(file->mode);
2258+
2259+
/* Only check regular files */
2260+
if (ftype != FT_REG) {
2261+
active_count++;
2262+
continue;
2263+
}
2264+
2265+
/* Construct destination path */
2266+
if (local_name)
2267+
strlcpy(fbuf, local_name, sizeof fbuf);
2268+
else
2269+
f_name(file, fbuf);
2270+
2271+
/* Stat destination file */
2272+
if (do_stat(fbuf, &st) < 0) {
2273+
active_count++;
2274+
continue;
2275+
}
2276+
2277+
/* Check if file is unchanged */
2278+
if (quick_check_ok(ftype, fbuf, file, &st)) {
2279+
if (DEBUG_GTE(GENR, 2))
2280+
rprintf(FINFO, "skipping unchanged: %s\n", fbuf);
2281+
2282+
/* Subtract from total size for accurate progress */
2283+
stats.total_size -= F_LENGTH(file);
2284+
2285+
/* Mark as inactive to remove from file list */
2286+
clear_file(file);
2287+
skipped_count++;
2288+
} else {
2289+
active_count++;
2290+
}
2291+
}
2292+
2293+
/* Update stats to reflect skipped files */
2294+
stats.num_files = active_count;
2295+
stats.num_skipped_files = skipped_count;
2296+
2297+
if (DEBUG_GTE(GENR, 1))
2298+
rprintf(FINFO, "skipped %d unchanged files, %d active, adjusted size: %.2f GB\n",
2299+
skipped_count, active_count, (double)stats.total_size / 1024 / 1024 / 1024);
2300+
2301+
/* Send skipped count and adjusted total_size to sender for accurate progress display */
2302+
if (f_out >= 0) {
2303+
char buf[12];
2304+
SIVAL(buf, 0, skipped_count);
2305+
SIVAL64(buf, 4, stats.total_size);
2306+
send_msg(MSG_FLIST_COUNT, buf, 12, -1);
2307+
}
2308+
}
2309+
22262310
void generate_files(int f_out, const char *local_name)
22272311
{
22282312
int i, ndx, next_loopchk = 0;
@@ -2279,6 +2363,9 @@ void generate_files(int f_out, const char *local_name)
22792363

22802364
dflt_perms = (ACCESSPERMS & ~orig_umask);
22812365

2366+
/* Pre-scan to mark unchanged files for accurate progress reporting */
2367+
prescan_for_unchanged(local_name, f_out);
2368+
22822369
do {
22832370
#ifdef SUPPORT_HARD_LINKS
22842371
if (preserve_hard_links && inc_recurse) {

io.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,20 @@ static void read_a_msg(void)
15111511
raw_read_buf((char*)&stats.total_read, sizeof stats.total_read);
15121512
iobuf.in_multiplexed = 1;
15131513
break;
1514+
case MSG_FLIST_COUNT:
1515+
if (msg_bytes != 12 || !am_sender)
1516+
goto invalid_msg;
1517+
val = raw_read_int();
1518+
stats.num_skipped_files = val;
1519+
/* Adjust total file count to reflect only active files */
1520+
stats.num_files -= val;
1521+
/* Read and update adjusted total_size for accurate progress percentage */
1522+
raw_read_buf((char*)&stats.total_size, 8);
1523+
iobuf.in_multiplexed = 1;
1524+
if (DEBUG_GTE(IO, 1))
1525+
rprintf(FINFO, "[%s] received MSG_FLIST_COUNT: %d skipped, %d active, size: %.2f GB\n",
1526+
who_am_i(), val, stats.num_files, (double)stats.total_size / 1024 / 1024 / 1024);
1527+
break;
15141528
case MSG_REDO:
15151529
if (msg_bytes != 4 || !am_generator)
15161530
goto invalid_msg;

options.c

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ int human_readable = 1;
112112
int recurse = 0;
113113
int mkpath_dest_arg = 0;
114114
int allow_inc_recurse = 1;
115+
int no_i_r_skip_unchanged = 0;
115116
int xfer_dirs = -1;
116117
int am_daemon = 0;
117118
int connect_timeout = 0;
@@ -585,7 +586,7 @@ enum {OPT_SERVER = 1000, OPT_DAEMON, OPT_SENDER, OPT_EXCLUDE, OPT_EXCLUDE_FROM,
585586
OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG, OPT_BLOCK_SIZE,
586587
OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN, OPT_BWLIMIT, OPT_STDERR,
587588
OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS, OPT_OLD_ARGS,
588-
OPT_STOP_AFTER, OPT_STOP_AT,
589+
OPT_STOP_AFTER, OPT_STOP_AT, OPT_NO_I_R_SKIP_UNCHANGED,
589590
OPT_REFUSED_BASE = 9000};
590591

591592
static struct poptOption long_options[] = {
@@ -616,6 +617,9 @@ static struct poptOption long_options[] = {
616617
{"no-inc-recursive", 0, POPT_ARG_VAL, &allow_inc_recurse, 0, 0, 0 },
617618
{"i-r", 0, POPT_ARG_VAL, &allow_inc_recurse, 1, 0, 0 },
618619
{"no-i-r", 0, POPT_ARG_VAL, &allow_inc_recurse, 0, 0, 0 },
620+
{"no-inc-recursive-skip-unchanged", 0, POPT_ARG_NONE, 0, OPT_NO_I_R_SKIP_UNCHANGED, 0, 0 },
621+
{"no-i-r-skip-unchanged", 0, POPT_ARG_NONE, 0, OPT_NO_I_R_SKIP_UNCHANGED, 0, 0 },
622+
{"no-i-r-s-u", 0, POPT_ARG_NONE, 0, OPT_NO_I_R_SKIP_UNCHANGED, 0, 0 },
619623
{"dirs", 'd', POPT_ARG_VAL, &xfer_dirs, 2, 0, 0 },
620624
{"no-dirs", 0, POPT_ARG_VAL, &xfer_dirs, 0, 0, 0 },
621625
{"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)
19001904
break;
19011905
#endif
19021906

1907+
case OPT_NO_I_R_SKIP_UNCHANGED:
1908+
no_i_r_skip_unchanged = 1;
1909+
allow_inc_recurse = 0;
1910+
break;
1911+
19031912
case OPT_STDERR: {
19041913
int len;
19051914
arg = poptGetOptArg(pc);

progress.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ static void rprint_progress(OFF_T ofs, OFF_T size, struct timeval *now, int is_l
7878
int len = snprintf(eol, sizeof eol,
7979
" (xfr#%d, %s-chk=%d/%d)\n",
8080
stats.xferred_files, flist_eof ? "to" : "ir",
81-
stats.num_files - current_file_index - 1,
81+
stats.num_files - (current_file_index - stats.num_skipped_files) - 1,
8282
stats.num_files);
8383
if (INFO_GTE(PROGRESS, 2)) {
8484
static int last_len = 0;

rsync.1.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -909,6 +909,43 @@ expand it.
909909
before it begins to transfer files. See [`--inc-recursive`](#opt) for more
910910
info.
911911

912+
0. `--no-inc-recursive-skip-unchanged`, `--no-i-r-skip-unchanged`, `--no-i-r-s-u`
913+
914+
This option combines [`--no-i-r`](#opt) with pre-scanning to skip unchanged
915+
files, providing accurate progress reporting for resumed transfers. When
916+
using [`--info=progress2`](#opt), interrupted transfers that are resumed
917+
normally show incorrect progress percentages because `stats.total_size`
918+
includes already-transferred files. This option pre-scans the destination
919+
during generator initialization, marks unchanged files for skipping, and
920+
adjusts `stats.total_size` accordingly, resulting in accurate 0% to 100%
921+
progress reporting.
922+
923+
The pre-scan uses the same comparison logic as normal rsync operations
924+
(checking size, mtime, checksums if [`--checksum`](#opt) is used, etc.).
925+
Files determined to be unchanged are completely skipped from processing,
926+
reducing both CPU and I/O overhead while fixing progress reporting.
927+
928+
This option works for all transfer types: local-to-local, local-to-remote,
929+
remote-to-local, and daemon transfers. Because the generator runs on the
930+
receiver side and has access to destination files in all scenarios, the
931+
feature functions correctly regardless of transfer direction.
932+
933+
This option implies [`--no-i-r`](#opt), so it requires the full file list
934+
to be available before processing begins. The performance overhead is
935+
minimal since the pre-scan performs the same stat operations that would
936+
occur anyway during normal generator operation, just earlier in the pipeline.
937+
938+
Example use cases:
939+
940+
- Resuming interrupted transfers with accurate progress:
941+
`rsync -av --no-i-r-s-u --info=progress2 src/ host:dest/`
942+
943+
- Large synchronization with mostly unchanged files:
944+
`rsync -av --no-inc-recursive-skip-unchanged /data/ /backup/`
945+
946+
- Remote-to-local transfer with progress tracking:
947+
`rsync -av --no-i-r-skip-unchanged --info=progress2 host:src/ dest/`
948+
912949
0. `--relative`, `-R`
913950

914951
Use relative paths. This means that the full path names specified on the

rsync.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,7 @@ enum msgcode {
269269
MSG_LOG=FLOG, MSG_CLIENT=FCLIENT, /* sibling logging */
270270
MSG_REDO=9, /* reprocess indicated flist index */
271271
MSG_STATS=10, /* message has stats data for generator */
272+
MSG_FLIST_COUNT=11, /* generator sends updated file count to sender */
272273
MSG_IO_ERROR=22,/* the sending side had an I/O error */
273274
MSG_IO_TIMEOUT=33,/* tell client about a daemon's timeout value */
274275
MSG_NOOP=42, /* a do-nothing message (legacy protocol-30 only) */
@@ -1044,6 +1045,7 @@ struct stats {
10441045
int created_files, created_dirs, created_symlinks, created_devices, created_specials;
10451046
int deleted_files, deleted_dirs, deleted_symlinks, deleted_devices, deleted_specials;
10461047
int xferred_files;
1048+
int num_skipped_files; /* files marked as unchanged by --no-i-r-skip-unchanged */
10471049
};
10481050

10491051
struct chmod_mode_struct;

0 commit comments

Comments
 (0)