Files
FastSync/src/client/client_report.c
T

1054 lines
42 KiB
C

#include "client_send_internal.h"
#include "array_list.h"
#include "change_list.h"
#include "charset.h"
#include "config.h"
#include "file.h"
#include "format.h"
#include "log.h"
#include "protocol.h"
#include "utils.h"
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/* Surface a server rejection to the user. When the last status exchange
carried a STATUS_ERROR_DETAIL reason (protocol 2.21.0) it is appended to the
client-side context; a bare STATUS_ERROR still logs the context alone. */
void log_server_rejection(const char* context) {
const char* detail = protocol_last_error();
if (detail && detail[0] != '\0') {
/* The detail is peer-controlled: escape it so terminal/log-format
* metacharacters cannot be injected into the client's output. */
char* escaped = output_escape(detail, log_get_8_bit_output());
log_message(LOG_LEVEL_ERROR, "%s: %s", context, escaped ? escaped : "<allocation failed>");
free(escaped);
} else {
log_message(LOG_LEVEL_ERROR, "%s", context);
}
}
const char* display_bytes(unsigned long long bytes, bool human_readable, char* buffer,
size_t buffer_size) {
if (human_readable && format_human_size_decimal(bytes, buffer, buffer_size))
return buffer;
snprintf(buffer, buffer_size, "%.1f MB", (double)bytes / (double)BYTES_PER_MIB);
return buffer;
}
/* rsync byte count: human-readable decimal when -h was given, otherwise a
* comma-grouped integer (rsync's big_num in the C locale). */
static const char* stats_bytes(const Config* config, unsigned long long bytes, char* buffer,
size_t buffer_size) {
if (!format_big_num(bytes, config->human_readable, buffer, buffer_size))
snprintf(buffer, buffer_size, "%llu", bytes);
return buffer;
}
/* Build rsync's per-type parenthetical: each non-zero category, in
reg/dir/link/special order. Empty when every count is zero. */
static void type_breakdown(unsigned long long reg, unsigned long long dir, unsigned long long link,
unsigned long long special, char* out, size_t out_size) {
if (reg + dir + link + special == 0) {
out[0] = '\0';
return;
}
out[0] = '\0';
size_t used = 0;
const struct {
const char* name;
unsigned long long count;
} parts[4] = {{"reg", reg}, {"dir", dir}, {"link", link}, {"special", special}};
bool first = true;
for (size_t i = 0; i < 4; i++) {
if (parts[i].count == 0)
continue;
int written = snprintf(out + used, out_size - used, "%s%s: %llu", first ? "(" : ", ",
parts[i].name, parts[i].count);
if (written < 0 || (size_t)written >= out_size - used)
break;
used += (size_t)written;
first = false;
}
if (!first && used + 1 < out_size)
out[used++] = ')';
out[used] = '\0';
}
/* Build rsync's `Number of files` parenthetical from the scan's flist counts. */
static void stats_type_breakdown(const TransferStats* stats, char* out, size_t out_size) {
type_breakdown(stats->flist_reg, stats->flist_dir, stats->flist_link, stats->flist_special, out,
out_size);
}
/* rsync's `Number of files` counts every directory. A recursive scan that
preserves a directory attribute captures them in `dir_entries`; a `-r` scan
(no -t/-p) captures nothing, so fall back to the scanner's shared counter of
traversed directories that are not already represented by an inline
directory entry. The -d generator counts its explicit directory entries
inline and does not traverse, so it is excluded here. */
unsigned long long dir_count_for_stats(const Config* config, const ArrayList* dir_entries,
atomic_ullong* counter) {
if (config == NULL || config->dirs || config->list_only)
return 0;
if (dir_metadata_should_capture(config))
return dir_entries != NULL ? (unsigned long long)dir_entries->size : 0;
return counter != NULL ? (unsigned long long)atomic_load(counter) : 0;
}
/* Print the rsync `--stats` block on stdout. The source-side flist and
transferred counters come from `stats` (filled while scanning/sending), the
receiver-only counters from the STATUS_STATS frame, and the wire byte totals
from the process-wide protocol counters. The labels, layout and
rate/speedup formulas match rsync 3.4.1. Shared by the single-threaded and
multithreaded send paths. */
void report_transfer_stats(const Config* config, const TransferStats* stats, time_t start,
const ReceiverStats* recv) {
if (!config->stats || config->quiet)
return;
TransferStats empty = {0};
if (stats == NULL)
stats = &empty;
ReceiverStats none = {0};
if (recv == NULL)
recv = &none;
unsigned long long sent = protocol_bytes_written();
unsigned long long received = protocol_bytes_read();
/* rsync: bytes_per_sec = (written + read) / (0.5 + (end - start)). */
double elapsed = difftime(time(NULL), start);
double rate = (double)(sent + received) / (0.5 + elapsed);
char total_buffer[32];
char transferred_buffer[32];
char literal_buffer[32];
char matched_buffer[32];
char sent_buffer[32];
char recv_buffer[32];
char rate_buffer[32] = {0};
char human_rate[32] = {0};
const char* total =
stats_bytes(config, stats->total_file_size, total_buffer, sizeof(total_buffer));
const char* transferred = stats_bytes(config, stats->transferred_file_size, transferred_buffer,
sizeof(transferred_buffer));
/* Protocol 2.28.0: the receiver reports the bytes it literally stored, which
is exact for a delta transfer (the sender's own literal_data counts each
stored file's whole source size and is only an upper bound). Fall back to
the sender total when the receiver reported no delta/literal accounting
(e.g. a local no-server path). */
unsigned long long literal_bytes = (recv->literal_bytes != 0 || recv->matched_data != 0)
? recv->literal_bytes
: stats->literal_data;
const char* literal = stats_bytes(config, literal_bytes, literal_buffer, sizeof(literal_buffer));
const char* sent_s = stats_bytes(config, sent, sent_buffer, sizeof(sent_buffer));
const char* recv_s = stats_bytes(config, received, recv_buffer, sizeof(recv_buffer));
const char* rate_str = rate_buffer;
if (config->human_readable) {
if (!format_human_size_decimal((unsigned long long)rate, human_rate, sizeof(human_rate)))
snprintf(human_rate, sizeof(human_rate), "0");
rate_str = human_rate;
} else {
snprintf(rate_buffer, sizeof(rate_buffer), "%.2f", rate);
}
double speedup =
(sent + received) > 0 ? (double)stats->total_file_size / (double)(sent + received) : 0.0;
char breakdown[128];
stats_type_breakdown(stats, breakdown, sizeof(breakdown));
unsigned long long flist_total =
stats->flist_reg + stats->flist_dir + stats->flist_link + stats->flist_special;
char created_breakdown[128];
type_breakdown(recv->created_reg, recv->created_dir, recv->created_link, recv->created_special,
created_breakdown, sizeof(created_breakdown));
unsigned long long created_total =
recv->created_reg + recv->created_dir + recv->created_link + recv->created_special;
printf("\n");
if (breakdown[0] != '\0')
printf("Number of files: %llu %s\n", flist_total, breakdown);
else
printf("Number of files: %llu\n", flist_total);
/* Protocol 2.28.0: the receiver reports which destination entries it newly
created, split by type, so this line matches rsync exactly. */
if (created_breakdown[0] != '\0')
printf("Number of created files: %llu %s\n", created_total, created_breakdown);
else
printf("Number of created files: %llu\n", created_total);
printf("Number of deleted files: %llu\n", recv->deleted_files);
printf("Number of regular files transferred: %llu\n", stats->transferred_regular);
printf("Total file size: %s bytes\n", total);
printf("Total transferred file size: %s bytes\n", transferred);
printf("Literal data: %s bytes\n", literal);
const char* matched =
stats_bytes(config, recv->matched_data, matched_buffer, sizeof(matched_buffer));
printf("Matched data: %s bytes\n", matched);
printf("File list size: 0\n");
printf("File list generation time: 0.000 seconds\n");
printf("File list transfer time: 0.000 seconds\n");
printf("Total bytes sent: %s\n", sent_s);
printf("Total bytes received: %s\n", recv_s);
printf("\n");
printf("sent %s bytes received %s bytes %s bytes/sec\n", sent_s, recv_s, rate_str);
printf("total size is %s speedup is %.2f%s\n", total, speedup,
config->dry_run ? " (DRY RUN)" : "");
fflush(stdout);
}
/* Classify one scanned source entry into the rsync flist counters. Called for
every entry the sender walks, transferred or skipped. Directory entries are
counted here only for the explicit -d/--dirs generator; a recursive scan's
directories are accounted from the scanner's dir_entries list at report time. */
void transfer_stats_note_entry(TransferStats* stats, const File* file) {
if (stats == NULL || file == NULL)
return;
if (file->is_dir) {
stats->flist_dir++;
return;
}
if (file->is_symlink) {
stats->flist_link++;
stats->total_file_size += file->symlink_target ? strlen(file->symlink_target) : 0;
return;
}
if (file->is_special) {
stats->flist_special++;
return;
}
stats->flist_reg++;
stats->total_file_size += file->data ? file->data->size : 0;
}
/* Account for a regular file (or a whole-file append) the receiver actually
stored: rsync's transferred-file count and transferred/literal byte totals.
`literal_data` counts the whole source size, which is exact for a whole-file
send but an upper bound for a delta send (the receiver reuses basis blocks
the sender never ships); see TransferStats.literal_data in format.h. */
void transfer_stats_note_transferred(TransferStats* stats, const File* file) {
if (stats == NULL || file == NULL)
return;
if (file->is_dir || file->is_symlink || file->is_special)
return;
if (file->link_group != 0 && !file->link_first)
return;
unsigned long long size = file->data ? file->data->size : 0;
stats->transferred_regular++;
stats->transferred_file_size += size;
stats->literal_data += size;
}
/* ---- rsync-style per-file --progress ------------------------------------
* rsync prints, for each transferred regular file, the file name followed by a
* two-frame progress line: the first at the initial 32 KiB read window (always
* 0.00 kB/s / 0:00:00 on a sub-second transfer) and a final 100% frame carrying
* `(xfr#N, to-chk=X/Y)`. Rates are wall-clock dependent, so only the final
* rate is measured here; the layout matches rsync 3.4.1's progress.c. */
#define RSYNC_PROGRESS_IO_WINDOW (32ULL * 1024ULL)
/* Paths-only pre-count of the source file list, built once at transfer start
* when progress output or -i/--out-format needs it. rsync's `to-chk`
* denominator is the whole file list -- every regular file, directory, symlink
* and special plus the transfer root -- while the streaming scan only emits
* empty directories. A metadata-only walk (no file reads, no hashing) supplies
* that total and a metadata-bearing File for every directory, so --progress can
* name them and -i/--out-format can itemize them without a second full scan. */
/* One directory in the pre-count, keyed by its transfer-relative display name
* ("" is the transfer root). `file` is owned by ProgressPrecount.dir_files and
* carries the source metadata needed by -i/--out-format (%M/%B/%U/%G). */
typedef struct {
char* name; /* owned */
File* file;
} DirRef;
typedef struct {
unsigned long long total;
ArrayList* dir_paths; /* owned char* in transfer-relative display form */
ArrayList* dir_files; /* owned File* captured during the metadata walk */
ArrayList* dir_refs; /* owned DirRef*, sorted by name for prefix lookup */
} ProgressPrecount;
static bool g_progress_active;
/* True when -i/--out-format need the pre-counted directory entries fed into the
* change-event stream (independent of --progress). */
static bool g_change_dirs_active;
static unsigned long long g_progress_xferred;
static unsigned long long g_progress_index;
static unsigned long long g_progress_total;
static struct timespec g_progress_file_start;
static ProgressPrecount g_progress_precount;
static PathIndex g_progress_dir_index;
static bool g_progress_dir_index_valid;
static StrHashSet g_progress_emitted;
static bool g_progress_emitted_valid;
static ArrayList* g_progress_emitted_keys;
bool progress_requested(const Config* config) {
return config != NULL && !config->quiet &&
(config->show_progress || (config->info_level & LOG_INFO_PROGRESS) != 0);
}
static void dir_ref_destroy(void* item) {
DirRef* ref = (DirRef*)item;
if (ref == NULL)
return;
free(ref->name);
free(ref);
}
/* Sort DirRef pointers by their transfer-relative name for binary search. */
static int dir_ref_compare(const void* left, const void* right) {
const DirRef* a = *(const DirRef* const*)left;
const DirRef* b = *(const DirRef* const*)right;
return strcmp(a->name, b->name);
}
/* Look up the pre-counted directory File for a transfer-relative name ("" is
* the transfer root). Returns NULL when no pre-count was built or the name is
* not a known directory. */
static File* progress_dir_lookup(const char* name) {
if (name == NULL || g_progress_precount.dir_refs == NULL)
return NULL;
ArrayList* refs = g_progress_precount.dir_refs;
size_t lo = 0;
size_t hi = (size_t)refs->size;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2;
DirRef* ref = (DirRef*)refs->items[mid];
int cmp = strcmp(ref->name, name);
if (cmp < 0)
lo = mid + 1;
else if (cmp > 0)
hi = mid;
else
return ref->file;
}
return NULL;
}
static void progress_precount_dispose(ProgressPrecount* p) {
if (p->dir_paths != NULL) {
array_list_delete(p->dir_paths);
p->dir_paths = NULL;
}
if (p->dir_files != NULL) {
array_list_delete(p->dir_files);
p->dir_files = NULL;
}
if (p->dir_refs != NULL) {
array_list_delete(p->dir_refs);
p->dir_refs = NULL;
}
p->total = 0;
}
/* Record the transfer root's pre-transfer state for -i/--out-format. The
* receive root always exists, so rsync never marks it `cd`; its only observable
* change is its timestamp, which FastSync cannot observe remotely. Force a time
* mismatch so the root renders rsync's `.d..t...... ./` rather than the `cd`
* a zeroed destination state would produce. */
static void progress_precount_mark_root(File* root) {
if (root == NULL)
return;
root->dest_state.known = true;
root->dest_state.existed = true;
root->dest_state.mode = root->metadata != NULL ? root->metadata->mode : 0;
root->dest_state.uid = root->metadata != NULL ? root->metadata->uid : 0;
root->dest_state.gid = root->metadata != NULL ? root->metadata->gid : 0;
root->dest_state.size = 0;
root->dest_state.mtime_sec = (root->metadata != NULL ? root->metadata->mtime_sec : 0) - 3600;
root->dest_state.mtime_nsec = root->metadata != NULL ? root->metadata->mtime_nsec : 0;
}
/* Append one DirRef (name -> file) to the pre-count, marking the transfer
* root's destination state. Returns false on allocation failure. */
static bool progress_precount_add_ref(ProgressPrecount* p, const Config* config, File* file) {
const char* rel = delete_display_path(config, file_wire_path(file));
char* name = rel != NULL ? str_dup(rel) : NULL;
if (name == NULL)
return false;
DirRef* ref = malloc(sizeof(*ref));
if (ref == NULL) {
free(name);
return false;
}
ref->name = name;
ref->file = file;
if (name[0] == '\0')
progress_precount_mark_root(file);
if (!array_list_add(p->dir_refs, ref)) {
dir_ref_destroy(ref);
return false;
}
return true;
}
void client_progress_cleanup(void) {
if (g_progress_dir_index_valid) {
path_index_free(&g_progress_dir_index);
g_progress_dir_index_valid = false;
}
if (g_progress_emitted_valid) {
str_hash_set_free(&g_progress_emitted);
g_progress_emitted_valid = false;
}
if (g_progress_emitted_keys != NULL) {
array_list_delete(g_progress_emitted_keys);
g_progress_emitted_keys = NULL;
}
progress_precount_dispose(&g_progress_precount);
g_progress_active = false;
g_change_dirs_active = false;
g_progress_total = 0;
g_progress_index = 0;
g_progress_xferred = 0;
}
static void progress_first_frame(unsigned long long size, char* out, size_t out_size) {
char ofs_buf[32];
unsigned long long ofs = size < RSYNC_PROGRESS_IO_WINDOW ? size : RSYNC_PROGRESS_IO_WINDOW;
if (!format_big_num(ofs, false, ofs_buf, sizeof(ofs_buf)))
snprintf(ofs_buf, sizeof(ofs_buf), "%llu", ofs);
int pct = size == 0 ? 100 : (ofs == size ? 100 : (int)(100.0 * (double)ofs / (double)size));
snprintf(out, out_size, "\r%15s %3d%% %7.2f%s %s%s", ofs_buf, pct, 0.0, "kB/s", " 0:00:00",
" ");
}
static void progress_final_frame(unsigned long long size, char* out, size_t out_size) {
char ofs_buf[32];
char rembuf[32];
unsigned long long last_ofs = size < RSYNC_PROGRESS_IO_WINDOW ? size : RSYNC_PROGRESS_IO_WINDOW;
if (!format_big_num(size, false, ofs_buf, sizeof(ofs_buf)))
snprintf(ofs_buf, sizeof(ofs_buf), "%llu", size);
struct timespec now;
clock_gettime(CLOCK_MONOTONIC, &now);
long long diff_ms = (long long)(now.tv_sec - g_progress_file_start.tv_sec) * 1000 +
(now.tv_nsec - g_progress_file_start.tv_nsec) / 1000000;
if (diff_ms <= 0)
diff_ms = 1;
double rate =
size > last_ofs ? (double)(size - last_ofs) * 1000.0 / (double)diff_ms / 1024.0 : 0.0;
const char* units = "kB/s";
if (rate > 1024.0 * 1024.0) {
rate /= 1024.0 * 1024.0;
units = "GB/s";
} else if (rate > 1024.0) {
rate /= 1024.0;
units = "MB/s";
}
unsigned long long remain = (unsigned long long)(diff_ms / 1000);
snprintf(rembuf, sizeof(rembuf), "%4u:%02u:%02u", (unsigned)(remain / 3600),
(unsigned)((remain / 60) % 60), (unsigned)(remain % 60));
/* rsync's `to-chk` denominator is the whole file list (the pre-count); the
numerator falls as each entry is processed, root first. Without a
pre-count (the paths-only walk failed) fall back to the transferred-file
count so the single-file layout stays intact. */
unsigned long long total = g_progress_total > 0 ? g_progress_total : g_progress_xferred + 1;
unsigned long long to_chk = total > g_progress_index ? total - g_progress_index - 1 : 0;
snprintf(out, out_size, "\r%15s %3d%% %7.2f%s %s (xfr#%llu, to-chk=%llu/%llu)\n", ofs_buf, 100,
rate, units, rembuf, g_progress_xferred, to_chk, total);
}
bool info_flag_enabled(const Config* config, LogInfoFlag flag) {
return config != NULL && (config->info_level & flag) != 0;
}
/* Print rsync's deletion lines for a received list of destination-relative
* paths: `*deleting PATH` when itemizing, the --out-format expansion when a
* format is set, else `deleting PATH` for --info=del. Used by both the dry-run
* would-delete report and the real --info=del report. */
void print_delete_reports(const Config* config, const ArrayList* paths) {
if (!config || !paths || config->quiet)
return;
/* --debug=del is independent of the --info=del/itemize/out-format display:
emit the debug trace even when no deletion line would be printed. */
if (log_debug_enabled(LOG_DEBUG_DEL)) {
for (int i = 0; i < paths->size; i++) {
const char* raw = (const char*)paths->items[i];
const char* path = delete_display_path(config, raw);
log_debug_message(LOG_DEBUG_DEL, "del: %s", path ? path : raw);
}
}
if (!(config->itemize_changes || config->out_format != NULL ||
info_flag_enabled(config, LOG_INFO_DEL)))
return;
for (int i = 0; i < paths->size; i++) {
const char* raw = (const char*)paths->items[i];
const char* path = delete_display_path(config, raw);
if (config->out_format != NULL) {
ChangeEvent event;
memset(&event, 0, sizeof(event));
event.decision = CHANGE_SENT;
event.deleted = true;
event.name = path;
event.path = path;
char* line = change_render_format(config->out_format, config, &event);
if (line) {
char* escaped = output_escape(line, config->eight_bit_output);
printf("%s\n", escaped ? escaped : line);
free(escaped);
free(line);
}
} else {
char* escaped = output_escape(path, config->eight_bit_output);
if (config->itemize_changes)
printf("*deleting %s\n", escaped ? escaped : path);
else
printf("deleting %s\n", escaped ? escaped : path);
free(escaped);
}
}
fflush(stdout);
}
/* Emit every not-yet-seen ancestor directory of `rel`, outermost first, in the
* order rsync's depth-first flist walk visits them. With -i/--out-format each
* ancestor becomes a real change line (`cd+++++++++ sub/`, `.d..t...... ./`)
* rendered by the shared itemize code; otherwise it is the `--info=name` /
* --progress directory name line. */
static void client_progress_emit_ancestors(const Config* config, const char* rel) {
if (!g_progress_dir_index_valid || !g_progress_emitted_valid || g_progress_emitted_keys == NULL ||
rel == NULL)
return;
size_t rel_len = strlen(rel);
for (size_t i = 0; i < rel_len; i++) {
if (rel[i] != '/')
continue;
char* prefix = malloc(i + 1);
if (prefix == NULL)
return;
memcpy(prefix, rel, i);
prefix[i] = '\0';
if (path_index_contains(&g_progress_dir_index, prefix) &&
!str_hash_set_lookup(&g_progress_emitted, prefix)) {
char* key = str_dup(prefix);
if (key != NULL && array_list_add(g_progress_emitted_keys, key)) {
str_hash_set_insert_ref(&g_progress_emitted, key);
if (g_change_dirs_active) {
const File* dir = progress_dir_lookup(prefix);
if (dir != NULL)
change_emit_dir_sent(config, dir);
} else {
char* escaped = output_escape(prefix, config->eight_bit_output);
printf("%s/\n", escaped ? escaped : prefix);
free(escaped);
}
g_progress_index++;
} else {
free(key);
}
}
free(prefix);
}
}
/* Feed a transferred entry's ancestor directories into the change-event stream
* before the entry's own line, so -i/--out-format and --progress report
* directories in rsync's depth-first order. Every directory is an ancestor of
* some emitted entry (a file, symlink, special, hard link or the empty-directory
* entry the scanner emits for a leaf), so this covers the whole tree. */
void client_change_emit_ancestors(const Config* config, const File* file) {
if (config == NULL || file == NULL)
return;
if (!g_progress_active && !g_change_dirs_active)
return;
const char* rel = delete_display_path(config, file_wire_path(file));
client_progress_emit_ancestors(config, rel);
}
/* rsync's --info=name/progress line for one entry: transfer-relative name (a
* trailing slash for directories) plus the ` -> target` symlink suffix. */
static char* progress_entry_line(const File* file, const char* rel) {
const char* arrow = NULL;
const char* target = NULL;
if (file->is_symlink && file->symlink_target != NULL) {
arrow = " -> ";
target = file->symlink_target;
} else if (file->link_group != 0 && !file->link_first && file->hardlink_target != NULL) {
arrow = " => ";
target = file->hardlink_target;
}
size_t rel_len = strlen(rel);
bool dir_slash = file->is_dir && (rel_len == 0 || rel[rel_len - 1] != '/');
size_t extra = (dir_slash ? 1u : 0u) + (target != NULL ? 4u + strlen(target) : 0u);
char* line = malloc(rel_len + extra + 1);
if (line == NULL)
return NULL;
memcpy(line, rel, rel_len);
size_t off = rel_len;
if (dir_slash)
line[off++] = '/';
if (target != NULL) {
memcpy(line + off, arrow, 4);
off += 4;
memcpy(line + off, target, strlen(target));
off += strlen(target);
}
line[off] = '\0';
return line;
}
void client_progress_begin(const Config* config) {
change_reset_name_root();
g_progress_active = progress_requested(config);
g_progress_xferred = 0;
g_progress_index = 1; /* the transfer root is file-list entry #0 */
if (!g_progress_active && !g_change_dirs_active) {
/* `--info=flist` prints rsync's file-list header even without progress. */
if (!config->quiet && info_flag_enabled(config, LOG_INFO_FLIST)) {
printf("sending incremental file list\n");
fflush(stdout);
}
return;
}
/* -i/--out-format alone do not print the header, but --progress always does
and --info=flist does under any output mode (rsync prints it for
`-i --info=flist` and `--out-format=... --info=flist` too). */
if (g_progress_active || (!config->quiet && info_flag_enabled(config, LOG_INFO_FLIST)))
printf("sending incremental file list\n");
/* rsync prints the transfer-root directory before the first entry. Under
-i/--out-format it is the root change line (`.d..t...... ./`); otherwise it
is the plain --info=name / --progress name line. */
if (g_change_dirs_active) {
const File* root = progress_dir_lookup("");
if (root != NULL)
change_emit_dir_sent(config, root);
} else {
printf("./\n");
}
fflush(stdout);
}
/* Emit the name (unless itemize/out-format already did) and the two progress
* frames for one transferred regular file. */
void client_progress_file(const Config* config, const File* file) {
if (!g_progress_active || file == NULL || !file->data)
return;
g_progress_xferred++;
unsigned long long size = file->data->size;
if (!config->itemize_changes && config->out_format == NULL) {
const char* rel = delete_display_path(config, file_wire_path(file));
char* escaped = output_escape(rel, config->eight_bit_output);
printf("%s\n", escaped ? escaped : (rel ? rel : ""));
free(escaped);
}
clock_gettime(CLOCK_MONOTONIC, &g_progress_file_start);
char frame[160];
progress_first_frame(size, frame, sizeof(frame));
fputs(frame, stdout);
progress_final_frame(size, frame, sizeof(frame));
fputs(frame, stdout);
g_progress_index++;
fflush(stdout);
}
/* Emit the name line for a transferred non-regular entry (directory, symlink,
* special or hard-link sibling): rsync prints these in the file list but has no
* progress frame for them. */
void client_progress_name(const Config* config, const File* file) {
if (!g_progress_active || file == NULL)
return;
const char* rel = delete_display_path(config, file_wire_path(file));
if (!config->itemize_changes && config->out_format == NULL) {
char* line = progress_entry_line(file, rel ? rel : "");
if (line != NULL) {
char* escaped = output_escape(line, config->eight_bit_output);
printf("%s\n", escaped ? escaped : line);
free(escaped);
free(line);
fflush(stdout);
}
}
g_progress_index++;
}
/* An entry the receiver already had prints no name under --progress but still
* occupies a file-list slot in the `to-chk` numerator. */
void client_progress_uptodate(const Config* config, const File* file) {
(void)config;
(void)file;
if (!g_progress_active)
return;
g_progress_index++;
}
static bool progress_precount_add_dir(ProgressPrecount* p, const char* path) {
if (path == NULL || path[0] == '\0')
return true;
char* dup = str_dup(path);
if (dup == NULL)
return false;
if (array_list_add(p->dir_paths, dup))
return true;
free(dup);
return false;
}
static int progress_path_compare(const void* left, const void* right) {
const char* const* a = (const char* const*)left;
const char* const* b = (const char* const*)right;
return strcmp(*a, *b);
}
/* Sort the collected directory paths and drop duplicates so a large
* --files-from list (many entries sharing an implied ancestor) cannot grow the
* list without bound. */
static void progress_precount_dedup_dirs(ArrayList* dir_paths) {
if (dir_paths == NULL || dir_paths->size < 2)
return;
qsort(dir_paths->items, (size_t)dir_paths->size, sizeof(char*), progress_path_compare);
int write = 0;
for (int read = 0; read < dir_paths->size; read++) {
char* current = (char*)dir_paths->items[read];
if (write > 0 && strcmp((char*)dir_paths->items[write - 1], current) == 0) {
free(current);
continue;
}
dir_paths->items[write++] = current;
}
dir_paths->size = write;
}
/* Create a metadata-bearing directory File for the transfer-relative directory
* `rel` ("" is the transfer root), stat'ing it below config->send_directory.
* The -d/--files-from dirs generator never traverses directories, so this
* synthesizes the metadata the recursive scanner captures through
* scanner_capture_dir_time, letting -i/--out-format render %M/%B/%U/%G and the
* transfer-root/ancestor lines identically on both paths. Returns NULL when
* the path cannot be stat'd as a directory or on allocation failure (the line
* is then simply omitted, exactly as before). */
static File* progress_precount_make_dir(const Config* config, const char* rel) {
if (config == NULL || config->send_directory == NULL)
return NULL;
char* fs_path = (rel == NULL || rel[0] == '\0') ? str_dup(config->send_directory)
: path_cat(config->send_directory, rel);
if (fs_path == NULL)
return NULL;
struct stat st;
if (stat(fs_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
free(fs_path);
return NULL;
}
File* file = file_create(fs_path);
if (file == NULL) {
free(fs_path);
return NULL;
}
file->is_dir = true;
file->metadata =
file_metadata_create(fs_path, &st, config->preserve_atimes, config->preserve_crtimes);
file->send_path = str_dup(rel != NULL ? rel : "");
free(fs_path);
if (file->metadata == NULL || file->send_path == NULL) {
file_destroy(file);
return NULL;
}
return file;
}
/* The -d/--files-from dirs generator neither traverses nor records directories,
* so its metadata walk captures no Files. Synthesize the transfer root and
* every listed/implied directory from `entry_rels` so -i/--out-format emits the
* same root and ancestor lines the recursive scan does. `root_emitted` is true
* when the generator itself emits the root entry (bare `-d <dir>`), whose
* data-pass line must not be duplicated. Returns false only on allocation
* failure. */
static bool progress_precount_synthesize_dirs(const Config* config, ProgressPrecount* out,
const ArrayList* entry_rels, bool root_emitted) {
for (int i = 0; i < entry_rels->size; i++) {
const char* rel = (const char*)entry_rels->items[i];
if (rel == NULL)
continue;
size_t len = strlen(rel);
for (size_t j = 1; j < len; j++) {
if (rel[j] != '/')
continue;
/* --no-implied-dirs: rsync neither creates nor itemizes an implied parent,
so only explicitly listed directories get a line. */
if (config->no_implied_dirs)
break;
char* prefix = malloc(j + 1);
if (prefix == NULL)
return false;
memcpy(prefix, rel, j);
prefix[j] = '\0';
if (!progress_precount_add_dir(out, prefix)) {
free(prefix);
return false;
}
free(prefix);
}
}
progress_precount_dedup_dirs(out->dir_paths);
for (int i = 0; i < out->dir_paths->size; i++) {
const char* rel = (const char*)out->dir_paths->items[i];
File* dir = progress_precount_make_dir(config, rel);
if (dir == NULL)
continue;
if (!array_list_add(out->dir_files, dir)) {
file_destroy(dir);
return false;
}
}
/* Emit the transfer root only when the generator actually emitted an entry:
--prune-empty-dirs (or an empty --files-from list) transfers nothing, and
rsync prints no root line then either. */
if (!root_emitted && entry_rels->size > 0) {
File* root = progress_precount_make_dir(config, "");
if (root != NULL && !array_list_add(out->dir_files, root)) {
file_destroy(root);
return false;
}
}
return true;
}
/* Metadata-only walk collecting the full file-list total and every directory
* name. It uses its own scanner (fresh filter compilation and hard-link table)
* so the data pass's link-group state is never perturbed. */
static bool progress_precount_scan(const Config* config, ProgressPrecount* out) {
out->dir_paths = array_list_create(free);
out->dir_files = array_list_create(file_destroy);
out->dir_refs = array_list_create(dir_ref_destroy);
if (out->dir_paths == NULL || out->dir_files == NULL || out->dir_refs == NULL) {
progress_precount_dispose(out);
return false;
}
out->total = 0;
/* The -d/--files-from dirs generator never calls scanner_capture_dir_time, so
the walk below captures no directory Files. Record every emitted entry's
transfer-relative name so the implied ancestors can be synthesized once the
walk is done. */
bool synthesize = g_change_dirs_active && config->dirs;
ArrayList* entry_rels = synthesize ? array_list_create(free) : NULL;
if (synthesize && entry_rels == NULL) {
progress_precount_dispose(out);
return false;
}
bool root_emitted = false;
PreparedScanner prepared;
memset(&prepared, 0, sizeof(prepared));
if (!prepare_scanner(config, 0, &prepared)) {
array_list_delete(entry_rels);
progress_precount_dispose(out);
return false;
}
ScannerOptions local = prepared.options;
local.list_dirs = true;
local.note_nonreg = false;
local.note_mount = false;
local.dir_count = NULL;
local.use_metadata = false;
local.preserve_xattrs = false;
local.preserve_acls = false;
local.checksum = false;
/* Capture one metadata-bearing File per traversed directory (including the
transfer root) so -i/--out-format can render %M/%B/%U/%G for directories. */
local.capture_dir_times = true;
local.excluded_paths = NULL;
local.size_skipped_paths = NULL;
local.synced_dirs = NULL;
local.plan_dirs = NULL;
local.dir_entries = out->dir_files;
local.dir_entries_mutex = NULL;
local.hardlinks = NULL;
DirectoryScanner* scanner = directory_scanner_create_with_options(config->send_directory, &local);
bool ok = scanner != NULL;
if (scanner != NULL) {
Chunk* chunk;
while (ok && (chunk = directory_scanner_next(scanner)) != NULL) {
out->total += (unsigned long long)chunk->element_count;
for (int i = 0; i < chunk->element_count && ok; i++) {
const File* f = chunk->items[i];
if (f == NULL)
continue;
const char* rel = delete_display_path(config, file_wire_path(f));
if (f->is_dir) {
if (rel != NULL && rel[0] == '\0')
root_emitted = true;
ok = progress_precount_add_dir(out, rel);
if (!ok)
break;
}
if (synthesize && rel != NULL) {
char* dup = str_dup(rel);
if (dup == NULL || !array_list_add(entry_rels, dup)) {
free(dup);
ok = false;
}
}
}
chunk_destroy(chunk);
}
if (ok && directory_scanner_failed(scanner))
ok = false;
directory_scanner_destroy(scanner);
}
prepared_scanner_destroy(&prepared);
if (!ok) {
array_list_delete(entry_rels);
progress_precount_dispose(out);
return false;
}
if (synthesize) {
bool synth_ok = progress_precount_synthesize_dirs(config, out, entry_rels, root_emitted);
array_list_delete(entry_rels);
if (!synth_ok) {
progress_precount_dispose(out);
return false;
}
}
/* Build the name -> File lookup from the captured directory Files. */
for (int i = 0; i < out->dir_files->size; i++) {
File* f = (File*)out->dir_files->items[i];
if (f == NULL)
continue;
if (!progress_precount_add_ref(out, config, f)) {
progress_precount_dispose(out);
return false;
}
}
if (out->dir_refs->size > 1)
qsort(out->dir_refs->items, (size_t)out->dir_refs->size, sizeof(DirRef*), dir_ref_compare);
out->total += 1; /* the transfer root "." */
return true;
}
/* Reuse the --delete-during/--delete-delay keep-set pre-scan: its traversed
* directory list already holds every directory and `non_dir_count` the entries
* counted during that same pass, so progress costs no second walk. */
static bool progress_precount_from_plan_dirs(const Config* config, const ArrayList* plan_dirs,
unsigned long long non_dir_count,
ProgressPrecount* out) {
out->dir_paths = array_list_create(free);
out->dir_files = array_list_create(file_destroy);
out->dir_refs = array_list_create(dir_ref_destroy);
if (out->dir_paths == NULL || out->dir_files == NULL || out->dir_refs == NULL) {
progress_precount_dispose(out);
return false;
}
out->total = non_dir_count + 1;
/* The delete pre-scan's plan list omits the transfer root, so synthesize its
entry here; it is only used for the root change line. */
File* root = file_create("");
if (root == NULL || !array_list_add(out->dir_files, root)) {
file_destroy(root);
progress_precount_dispose(out);
return false;
}
root->is_dir = true;
if (!progress_precount_add_ref(out, config, root)) {
progress_precount_dispose(out);
return false;
}
for (int i = 0; i < plan_dirs->size; i++) {
const char* path = (const char*)plan_dirs->items[i];
const char* rel = config->send_directory != NULL
? utils_strip_transfer_root(path, config->send_directory)
: path;
if (!progress_precount_add_dir(out, rel)) {
progress_precount_dispose(out);
return false;
}
File* dir = file_create("");
if (dir == NULL) {
progress_precount_dispose(out);
return false;
}
dir->is_dir = true;
dir->send_path = str_dup(rel != NULL ? rel : "");
if (dir->send_path == NULL || !array_list_add(out->dir_files, dir)) {
file_destroy(dir);
progress_precount_dispose(out);
return false;
}
if (!progress_precount_add_ref(out, config, dir)) {
progress_precount_dispose(out);
return false;
}
}
if (out->dir_refs->size > 1)
qsort(out->dir_refs->items, (size_t)out->dir_refs->size, sizeof(DirRef*), dir_ref_compare);
out->total += (unsigned long long)out->dir_paths->size;
return true;
}
/* Build the optional progress pre-count. A failed pre-count is non-fatal: the
* transfer proceeds and the progress denominator falls back to the transferred
* file count. */
void client_progress_prepare(const Config* config, const ArrayList* plan_dirs,
unsigned long long plan_non_dir_count) {
client_progress_cleanup();
g_progress_active = progress_requested(config);
g_change_dirs_active = config->itemize_changes || config->out_format != NULL;
if (!g_progress_active && !g_change_dirs_active)
return;
/* -i/--out-format render directory metadata (%M/%B/%U/%G) that only the
metadata walk captures; the --delete-during/--delete-delay plan list has no
metadata, so prefer the walk whenever a change line is rendered. Pure
--progress keeps reusing the plan list and its cheaper path-only pass. */
bool ok = (plan_dirs != NULL && !g_change_dirs_active)
? progress_precount_from_plan_dirs(config, plan_dirs, plan_non_dir_count,
&g_progress_precount)
: progress_precount_scan(config, &g_progress_precount);
if (!ok) {
g_progress_total = 0;
return;
}
g_progress_total = g_progress_precount.total;
if (g_progress_precount.dir_paths != NULL && g_progress_precount.dir_paths->size > 0 &&
path_index_build(&g_progress_dir_index,
(const char* const*)g_progress_precount.dir_paths->items,
(size_t)g_progress_precount.dir_paths->size))
g_progress_dir_index_valid = true;
if (str_hash_set_init(&g_progress_emitted, (size_t)(g_progress_precount.dir_paths != NULL
? g_progress_precount.dir_paths->size + 1
: 1)))
g_progress_emitted_valid = true;
g_progress_emitted_keys = array_list_create(free);
}
/* Read the optional STATUS_STATS record (protocol 2.25.0) that the receiver
* sends just before its terminal status when report_stats was negotiated.
* Consumes the would-delete path list into `would_delete` (optional). */
bool receive_stats_record(int fd, ReceiverStats* stats, ArrayList* would_delete) {
if (!format_stats_receive(fd, stats))
return false;
int count = 0;
if (!receive_int(fd, &count) || count < 0 || count > MAX_MANIFEST_ENTRIES)
return false;
/* Mirror the delete-plan parser: every retained path must be a valid
destination-relative path, and the whole list shares one MAX_MANIFEST_BYTES
budget so a hostile peer cannot make the client retain unbounded memory. */
size_t bytes = 0;
for (int i = 0; i < count; i++) {
char* path = receive_wire_str(fd);
if (!path)
return false;
if (path[0] == '\0' || path[0] == '/' || has_path_traversal(path)) {
free(path);
return false;
}
if (would_delete) {
size_t entry_size = strlen(path) + sizeof(char*) + 16;
if (entry_size > MAX_MANIFEST_BYTES - bytes) {
free(path);
return false;
}
bytes += entry_size;
if (!array_list_add(would_delete, path)) {
free(path);
return false;
}
} else {
free(path);
}
}
return true;
}
/* Strip the transfer-root prefix from a receiver-reported destination-relative
* delete path so a `*deleting` line matches rsync's transfer-relative name
* (FastSync's destination mirror includes the source's absolute path). */
const char* delete_display_path(const Config* config, const char* path) {
if (!config || !path || !config->send_directory)
return path;
return utils_strip_transfer_root(path, config->send_directory);
}