From 4872d425a38acf29d96ac73d1b31a15a2ae79f80 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 16:13:56 +0200 Subject: [PATCH 1/3] feat: add rsync info flags --- RSYNC_COMPAT.md | 2 +- src/client/client_cli.c | 89 ++++++++++++++++++++++++++---- src/client/client_send.c | 5 ++ src/client/usage.c | 2 + src/shared/log.c | 30 ++++++++++ src/shared/log.h | 20 +++++++ tests/integration/test_features.py | 21 +++++++ tests/test_client_cli.c | 26 ++++++++- 8 files changed, 183 insertions(+), 12 deletions(-) diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 4b236ec..cd149c1 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -23,7 +23,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `-q`, `--quiet` | Suppress non-error messages | ❌ Not Implemented | Removed because it had no effect | | `--help` | Show help | ✅ Implemented | Prints usage and exits; `-h` is not accepted | | `-V`, `--version` | Print version | ✅ Implemented | | -| `--info=FLAGS` | Fine-grained info verbosity | ❌ Not Implemented | Removed because it had no effect | +| `--info=FLAGS` | Fine-grained info verbosity | ✅ Implemented | Supports comma-separated rsync info names and `all`/`none` | | `--debug=FLAGS` | Fine-grained debug verbosity | ❌ Not Implemented | Removed because it had no effect | | `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | | | `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 191cc07..6198815 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -88,6 +88,69 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio static int read_patterns_from_file(const char* filepath, char*** patterns, int* count); +static int parse_info_flags(const char* value, Config* config) { + if (!value || value[0] == '\0' || value[0] == ',' || value[strlen(value) - 1] == ',' || + strstr(value, ",,")) { + log_message(LOG_LEVEL_ERROR, "--info requires at least one flag"); + return -1; + } + char* flags = str_dup(value); + if (!flags) { + log_message(LOG_LEVEL_ERROR, "memory allocation failed for --info"); + return -1; + } + + uint32_t parsed = (uint32_t)config->info_level; + char* saveptr = NULL; + for (char* token = strtok_r(flags, ",", &saveptr); token != NULL; + token = strtok_r(NULL, ",", &saveptr)) { + uint32_t flag = 0; + if (strcmp(token, "all") == 0) { + parsed = LOG_INFO_ALL; + continue; + } + if (strcmp(token, "none") == 0) { + parsed = 0; + continue; + } + if (strcmp(token, "copy") == 0) + flag = LOG_INFO_COPY; + else if (strcmp(token, "del") == 0) + flag = LOG_INFO_DEL; + else if (strcmp(token, "flist") == 0) + flag = LOG_INFO_FLIST; + else if (strcmp(token, "misc") == 0) + flag = LOG_INFO_MISC; + else if (strcmp(token, "mount") == 0) + flag = LOG_INFO_MOUNT; + else if (strcmp(token, "name") == 0) + flag = LOG_INFO_NAME; + else if (strcmp(token, "nonreg") == 0) + flag = LOG_INFO_NONREG; + else if (strcmp(token, "progress") == 0) + flag = LOG_INFO_PROGRESS; + else if (strcmp(token, "skip") == 0) + flag = LOG_INFO_SKIP; + else if (strcmp(token, "stats") == 0) + flag = LOG_INFO_STATS; + else if (strcmp(token, "symsafe") == 0) + flag = LOG_INFO_SYMSAFE; + else if (strcmp(token, "backup") == 0) + flag = LOG_INFO_BACKUP; + else { + log_message(LOG_LEVEL_ERROR, "unknown --info flag: %s", token); + free(flags); + return -1; + } + parsed |= flag; + } + free(flags); + config->info_level = (int)parsed; + set_log_info_flags(parsed); + set_log_level(LOG_LEVEL_INFO); + return 0; +} + /* Parse a string as an unsigned long long. Returns 0 on success, -1 on error. */ static int parse_ull_arg(const char* val, unsigned long long* out, const char* optname) { char* end; @@ -236,7 +299,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, config->use_compression = true; config->use_multithreading = true; config->use_metadata = true; - log_message(LOG_LEVEL_INFO, "Enabled archive mode (-c -m -M)"); + log_info_message(LOG_INFO_MISC, "Enabled archive mode (-c -m -M)"); } else if (opt_is(argv[i], "-p", NULL) && i + 1 < argc) { if (set_positive_int_option(&config->ssh_port, argv[++i], "-p") != 0) return -1; @@ -270,7 +333,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, log_message(LOG_LEVEL_WARNING, "--delta-max value %llu too small, using default", val); } else if (opt_is(argv[i], "-c", "-z")) { config->use_compression = true; - log_message(LOG_LEVEL_INFO, "Enabled Compression"); + log_info_message(LOG_INFO_MISC, "Enabled Compression"); if (i + 1 < argc) { char* end_ptr; long level = strtol(argv[i + 1], &end_ptr, 10); @@ -280,22 +343,22 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, return -1; } config->compression_level = (int)level; - log_message(LOG_LEVEL_INFO, "Set Compression level to %ld", level); + log_info_message(LOG_INFO_MISC, "Set Compression level to %ld", level); i++; } } } else if (opt_is(argv[i], "-M", "--preserve")) { config->use_metadata = true; - log_message(LOG_LEVEL_INFO, "Enabled metadata preservation"); + log_info_message(LOG_INFO_MISC, "Enabled metadata preservation"); } else if (opt_is(argv[i], "-f", "--sendfile")) { config->use_sendfile = true; - log_message(LOG_LEVEL_INFO, "Enabled sendfile"); + log_info_message(LOG_INFO_MISC, "Enabled sendfile"); } else if (opt_is(argv[i], "-m", NULL)) { config->use_multithreading = true; - log_message(LOG_LEVEL_INFO, "Enabled Multithreading"); + log_info_message(LOG_INFO_MISC, "Enabled Multithreading"); } else if (opt_is(argv[i], "-s", NULL)) { config->use_chunk_serialization = true; - log_message(LOG_LEVEL_INFO, "Enabled Chunk Serialization"); + log_info_message(LOG_INFO_MISC, "Enabled Chunk Serialization"); } else if (opt_is(argv[i], "--server-port", NULL) && i + 1 < argc) { if (!parse_positive_int(argv[++i], &config->server_port)) { log_message(LOG_LEVEL_ERROR, "invalid --server-port value: %s", argv[i]); @@ -318,7 +381,7 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, return -1; } io_set_bwlimit(kbps * 1024); - log_message(LOG_LEVEL_INFO, "Set bandwidth limit to %llu KB/s", kbps); + log_info_message(LOG_INFO_MISC, "Set bandwidth limit to %llu KB/s", kbps); } else if (opt_is(argv[i], "--chunk-size", NULL) && i + 1 < argc) { unsigned long long val; if (parse_ull_arg(argv[++i], &val, "--chunk-size") != 0) @@ -351,6 +414,12 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, return -1; } else if (opt_is(argv[i], "-v", "--verbose")) { set_log_level(LOG_LEVEL_DEBUG); + } else if (strncmp(argv[i], "--info=", 7) == 0) { + if (parse_info_flags(argv[i] + 7, config) != 0) + return -1; + } else if (opt_is(argv[i], "--info", NULL)) { + if (i + 1 >= argc || parse_info_flags(argv[++i], config) != 0) + return -1; } else if (opt_is(argv[i], "-T", NULL) && i + 1 < argc) { if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) return -1; @@ -483,11 +552,11 @@ int main(int argc, char* argv[]) { /* Enable implicit flags */ if (config->use_incremental && !config->use_metadata) { - log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --incremental"); + log_info_message(LOG_INFO_MISC, "Enabling metadata preservation for --incremental"); config->use_metadata = true; } if (config->use_delta && !config->use_metadata) { - log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for --delta"); + log_info_message(LOG_INFO_MISC, "Enabling metadata preservation for --delta"); config->use_metadata = true; } diff --git a/src/client/client_send.c b/src/client/client_send.c index 0b4ca5c..44ba9cc 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -272,6 +272,7 @@ static bool send_file_direct_sendfile(File* file, int fd, bool use_metadata) { static int send_single_file(Client* client, File* file, Config* config, bool use_incremental, bool use_sendfile) { int compression_level = config->use_compression ? config->compression_level : 0; + log_info_message(LOG_INFO_COPY, "Transferring %s", file->path); if (!use_incremental) { if (use_sendfile) { @@ -288,6 +289,7 @@ static int send_single_file(Client* client, File* file, Config* config, bool use DeltaSignature* sig = NULL; int rc = incremental_check(client, file, config, &sig); if (rc == 1) { + log_info_message(LOG_INFO_SKIP, "Skipping unchanged %s", file->path); delta_signature_destroy(sig); return 1; } @@ -318,6 +320,7 @@ static int send_single_file(Client* client, File* file, Config* config, bool use return -1; } if (rc == 1) { + log_info_message(LOG_INFO_SKIP, "Skipping unchanged %s", file->path); delta_signature_destroy(sig); return 1; } @@ -680,6 +683,8 @@ int send_files(Config* config) { fprintf(stderr, "Stats: %d files, %.1f MB, %.1f MB/s\n", total_files, total_bytes / 1048576.0, rate); } + log_info_message(LOG_INFO_STATS, "Transfer summary: %d files, %.1f MB", total_files, + total_bytes / 1048576.0); ret = ok ? 0 : 1; send_fail: diff --git a/src/client/usage.c b/src/client/usage.c index 2f800af..f1a5fcb 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -37,6 +37,8 @@ void print_usage(void) { printf(" -s Enable chunk serialization\n"); printf(" -f Enable sendfile (TCP only, not with -c or -s)\n"); printf(" -v, --verbose Enable debug logging\n"); + printf(" --info=FLAGS Fine-grained info: copy,del,flist,misc,mount,name,nonreg,\n"); + printf(" progress,skip,stats,symsafe,backup,all,none\n"); printf(" -M, --preserve Preserve file metadata\n"); printf(" --chunk-size Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE); printf(" --source-dir Source directory\n"); diff --git a/src/shared/log.c b/src/shared/log.c index 29bbe0d..29d2501 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -7,12 +7,21 @@ static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"}; static LogLevel current_log_level = LOG_LEVEL_WARNING; +static uint32_t info_flags = 0; static FILE* log_fp = NULL; void set_log_level(LogLevel level) { current_log_level = level; } +void set_log_info_flags(uint32_t flags) { + info_flags = flags; +} + +uint32_t get_log_info_flags(void) { + return info_flags; +} + void log_set_file(FILE* fp) { log_fp = fp; } @@ -53,6 +62,27 @@ void log_message(LogLevel log_level, const char* format, ...) { } } +void log_info_message(LogInfoFlag flag, const char* format, ...) { + if ((info_flags & flag) == 0 && current_log_level > LOG_LEVEL_DEBUG) + return; + + time_t now = time(NULL); + struct tm t; + if (!localtime_r(&now, &t)) + return; + + va_list args; + va_start(args, format); + write_message(stdout, LOG_LEVEL_INFO, t, format, args); + va_end(args); + + if (log_fp) { + va_start(args, format); + write_message(log_fp, LOG_LEVEL_INFO, t, format, args); + va_end(args); + } +} + void log_perror(const char* context) { log_message(LOG_LEVEL_ERROR, "%s: %s", context, strerror(errno)); } diff --git a/src/shared/log.h b/src/shared/log.h index acea629..4e9928f 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -2,12 +2,32 @@ #define LOG_H #include +#include typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR } LogLevel; +typedef enum { + LOG_INFO_COPY = 1u << 0, + LOG_INFO_DEL = 1u << 1, + LOG_INFO_FLIST = 1u << 2, + LOG_INFO_MISC = 1u << 3, + LOG_INFO_MOUNT = 1u << 4, + LOG_INFO_NAME = 1u << 5, + LOG_INFO_NONREG = 1u << 6, + LOG_INFO_PROGRESS = 1u << 7, + LOG_INFO_SKIP = 1u << 8, + LOG_INFO_STATS = 1u << 9, + LOG_INFO_SYMSAFE = 1u << 10, + LOG_INFO_BACKUP = 1u << 11, + LOG_INFO_ALL = (1u << 12) - 1, +} LogInfoFlag; + void log_message(LogLevel log_level, const char* message, ...); void log_perror(const char* context); void set_log_level(LogLevel level); +void set_log_info_flags(uint32_t flags); +uint32_t get_log_info_flags(void); +void log_info_message(LogInfoFlag flag, const char* message, ...); void log_set_file(FILE* fp); #endif diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index 312add4..a2500cb 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -264,6 +264,27 @@ class TestProgress: assert "Done." in output, "--progress did not report completion" +class TestInfo: + def test_info_copy_reports_transfers(self, shared_server): + clean_dir(DEST_DIR) + result, _ = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--info=copy"], + port=shared_server.port, + ) + assert result.returncode == 0, f"Info sync failed: {(result.stderr or result.stdout)[:200]}" + output = result.stdout + result.stderr + assert "[INFO]" in output and "Transferring" in output + + def test_info_rejects_unknown_flag(self): + result, _ = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--info=unknown"], + ) + assert result.returncode != 0 + assert "unknown --info flag" in result.stderr + + class TestBandwidthLimit: def test_bwlimit_runs(self, shared_server): clean_dir(DEST_DIR) diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 656fd40..9f8e1ca 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -1,6 +1,7 @@ #include "test_client_cli.h" #include "client_validation.h" #include "config.h" +#include "log.h" #include "test_utils.h" #include "utils.h" #include @@ -277,7 +278,6 @@ static void test_parse_args_rejects_unimplemented_options() { "-i", "--itemize-changes", "--out-format", - "--info", "--debug", "--list-only", "-h", @@ -323,6 +323,28 @@ static void test_parse_args_rejects_unimplemented_options() { } } +static void test_parse_args_info_flags() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--info=copy,skip", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); + EXPECT_EQ_INT(cfg->info_level, LOG_INFO_COPY | LOG_INFO_SKIP); + EXPECT_EQ_INT(get_log_info_flags(), LOG_INFO_COPY | LOG_INFO_SKIP); + config_delete(cfg); +} + +static void test_parse_args_rejects_invalid_info_flag() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--info=copy,unknown", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), -1); + config_delete(cfg); +} + /* Test parse_args with --archive flag */ static void test_parse_args_archive() { Config* cfg = config_create(); @@ -359,5 +381,7 @@ void test_client_cli() { test_parse_args_valid_compression_level(); test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); + test_parse_args_info_flags(); + test_parse_args_rejects_invalid_info_flag(); test_parse_args_archive(); } From 21e6b1c1bc82bc9ac6f26bd710447fcd5b8c8344 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 21:25:56 +0200 Subject: [PATCH 2/3] fix: address rsync info review findings --- RSYNC_COMPAT.md | 2 +- src/client/client_cli.c | 32 +++++++++++++----------------- src/client/client_send.c | 29 ++++++++++++++++++--------- src/client/usage.c | 3 +-- src/shared/log.h | 16 ++++----------- src/shared/multiprocessing.c | 2 ++ src/shared/multiprocessing.h | 2 ++ tests/integration/test_features.py | 13 +++++++++++- tests/test_client_cli.c | 12 +++++++++++ 9 files changed, 68 insertions(+), 43 deletions(-) diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index cd149c1..92fa333 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -23,7 +23,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `-q`, `--quiet` | Suppress non-error messages | ❌ Not Implemented | Removed because it had no effect | | `--help` | Show help | ✅ Implemented | Prints usage and exits; `-h` is not accepted | | `-V`, `--version` | Print version | ✅ Implemented | | -| `--info=FLAGS` | Fine-grained info verbosity | ✅ Implemented | Supports comma-separated rsync info names and `all`/`none` | +| `--info=FLAGS` | Fine-grained info verbosity | ✅ Implemented | Supports `copy`, `misc`, `skip`, `stats`, and `none`; unsupported names are rejected | | `--debug=FLAGS` | Fine-grained debug verbosity | ❌ Not Implemented | Removed because it had no effect | | `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | | | `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 6198815..96fa35c 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -115,30 +115,14 @@ static int parse_info_flags(const char* value, Config* config) { } if (strcmp(token, "copy") == 0) flag = LOG_INFO_COPY; - else if (strcmp(token, "del") == 0) - flag = LOG_INFO_DEL; - else if (strcmp(token, "flist") == 0) - flag = LOG_INFO_FLIST; else if (strcmp(token, "misc") == 0) flag = LOG_INFO_MISC; - else if (strcmp(token, "mount") == 0) - flag = LOG_INFO_MOUNT; - else if (strcmp(token, "name") == 0) - flag = LOG_INFO_NAME; - else if (strcmp(token, "nonreg") == 0) - flag = LOG_INFO_NONREG; - else if (strcmp(token, "progress") == 0) - flag = LOG_INFO_PROGRESS; else if (strcmp(token, "skip") == 0) flag = LOG_INFO_SKIP; else if (strcmp(token, "stats") == 0) flag = LOG_INFO_STATS; - else if (strcmp(token, "symsafe") == 0) - flag = LOG_INFO_SYMSAFE; - else if (strcmp(token, "backup") == 0) - flag = LOG_INFO_BACKUP; else { - log_message(LOG_LEVEL_ERROR, "unknown --info flag: %s", token); + log_message(LOG_LEVEL_ERROR, "unsupported --info flag: %s", token); free(flags); return -1; } @@ -147,7 +131,6 @@ static int parse_info_flags(const char* value, Config* config) { free(flags); config->info_level = (int)parsed; set_log_info_flags(parsed); - set_log_level(LOG_LEVEL_INFO); return 0; } @@ -273,6 +256,19 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch /* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */ int parse_args(Config* config, int argc, char* argv[], int* positional_args, int* positional_count) { + /* Apply output controls before processing other options so their order is irrelevant. */ + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "-v") == 0 || strcmp(argv[i], "--verbose") == 0) { + set_log_level(LOG_LEVEL_DEBUG); + } else if (strncmp(argv[i], "--info=", 7) == 0) { + if (parse_info_flags(argv[i] + 7, config) != 0) + return -1; + } else if (strcmp(argv[i], "--info") == 0) { + if (i + 1 >= argc || parse_info_flags(argv[++i], config) != 0) + return -1; + } + } + for (int i = 1; i < argc; i++) { const OptionEntry* entry = find_table_option(argv[i]); if (entry) { diff --git a/src/client/client_send.c b/src/client/client_send.c index 44ba9cc..25d1bc2 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -422,6 +422,14 @@ static int send_chunks_multithreaded(void* pipeline_context) { goto send_fail; } bool ok = finalize_transfer(client); + mtx_lock(&context->mutex_progress); + int total_files = context->total_files; + unsigned long long total_bytes = context->total_bytes; + mtx_unlock(&context->mutex_progress); + if (context->config->stats) + fprintf(stderr, "Stats: %d files, %.1f MB\n", total_files, total_bytes / 1048576.0); + log_info_message(LOG_INFO_STATS, "Transfer summary: %d files, %.1f MB", total_files, + total_bytes / 1048576.0); disconnect_transfer_client(client); mark_sender_done(context); protocol_session_unbind(); @@ -443,16 +451,19 @@ static int send_chunks_multithreaded(void* pipeline_context) { protocol_session_unbind(); return thrd_error; } - if (context->config->show_progress) { - unsigned long long chunk_bytes = 0; - for (int i = 0; i < current_chunk->element_count; i++) { - if (current_chunk->items[i] && current_chunk->items[i]->data) - chunk_bytes += current_chunk->items[i]->data->size; + unsigned long long chunk_bytes = 0; + int chunk_files = 0; + for (int i = 0; i < current_chunk->element_count; i++) { + if (current_chunk->items[i] && current_chunk->items[i]->data) { + chunk_files++; + chunk_bytes += current_chunk->items[i]->data->size; } - mtx_lock(&context->mutex_progress); - context->progress_bytes += chunk_bytes; - mtx_unlock(&context->mutex_progress); } + mtx_lock(&context->mutex_progress); + context->total_files += chunk_files; + context->total_bytes += chunk_bytes; + context->progress_bytes = context->total_bytes; + mtx_unlock(&context->mutex_progress); chunk_destroy(current_chunk); } } @@ -653,8 +664,8 @@ int send_files(Config* config) { manifest = NULL; break; } + total_bytes += chunk_bytes; if (config->show_progress) { - total_bytes += chunk_bytes; time_t now = time(NULL); if (now - last_progress >= 1) { last_progress = now; diff --git a/src/client/usage.c b/src/client/usage.c index f1a5fcb..581f414 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -37,8 +37,7 @@ void print_usage(void) { printf(" -s Enable chunk serialization\n"); printf(" -f Enable sendfile (TCP only, not with -c or -s)\n"); printf(" -v, --verbose Enable debug logging\n"); - printf(" --info=FLAGS Fine-grained info: copy,del,flist,misc,mount,name,nonreg,\n"); - printf(" progress,skip,stats,symsafe,backup,all,none\n"); + printf(" --info=FLAGS Fine-grained info: copy,misc,skip,stats,all,none\n"); printf(" -M, --preserve Preserve file metadata\n"); printf(" --chunk-size Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE); printf(" --source-dir Source directory\n"); diff --git a/src/shared/log.h b/src/shared/log.h index 4e9928f..756a21e 100644 --- a/src/shared/log.h +++ b/src/shared/log.h @@ -8,18 +8,10 @@ typedef enum { LOG_LEVEL_DEBUG, LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERR typedef enum { LOG_INFO_COPY = 1u << 0, - LOG_INFO_DEL = 1u << 1, - LOG_INFO_FLIST = 1u << 2, - LOG_INFO_MISC = 1u << 3, - LOG_INFO_MOUNT = 1u << 4, - LOG_INFO_NAME = 1u << 5, - LOG_INFO_NONREG = 1u << 6, - LOG_INFO_PROGRESS = 1u << 7, - LOG_INFO_SKIP = 1u << 8, - LOG_INFO_STATS = 1u << 9, - LOG_INFO_SYMSAFE = 1u << 10, - LOG_INFO_BACKUP = 1u << 11, - LOG_INFO_ALL = (1u << 12) - 1, + LOG_INFO_MISC = 1u << 1, + LOG_INFO_SKIP = 1u << 2, + LOG_INFO_STATS = 1u << 3, + LOG_INFO_ALL = LOG_INFO_COPY | LOG_INFO_MISC | LOG_INFO_SKIP | LOG_INFO_STATS, } LogInfoFlag; void log_message(LogLevel log_level, const char* message, ...); diff --git a/src/shared/multiprocessing.c b/src/shared/multiprocessing.c index 6b677f3..98508e2 100644 --- a/src/shared/multiprocessing.c +++ b/src/shared/multiprocessing.c @@ -26,7 +26,9 @@ PipelineContextSender* pipeline_context_sender_create(Config* config, Queue* que context->scanner_done = false; context->loader_done = false; context->manifest = NULL; + context->total_files = 0; context->progress_bytes = 0; + context->total_bytes = 0; context->sender_done = false; atomic_init(&context->cancelled, false); int init = 0; diff --git a/src/shared/multiprocessing.h b/src/shared/multiprocessing.h index 37f09d5..6ddea26 100644 --- a/src/shared/multiprocessing.h +++ b/src/shared/multiprocessing.h @@ -25,7 +25,9 @@ typedef struct { bool loader_done; ArrayList* manifest; mtx_t mutex_progress; + int total_files; unsigned long long progress_bytes; + unsigned long long total_bytes; bool sender_done; atomic_bool cancelled; } PipelineContextSender; diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index a2500cb..f0836cb 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -276,13 +276,24 @@ class TestInfo: output = result.stdout + result.stderr assert "[INFO]" in output and "Transferring" in output + def test_info_stats_reports_multithreaded_transfer(self, shared_server): + clean_dir(DEST_DIR) + result, _ = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-m", "--info=stats"], + port=shared_server.port, + ) + assert result.returncode == 0, f"Info stats sync failed: {(result.stderr or result.stdout)[:200]}" + output = result.stdout + result.stderr + assert "[INFO]" in output and "Transfer summary:" in output + def test_info_rejects_unknown_flag(self): result, _ = run_client( SOURCE_DIR, DEST_DIR, flags=["--info=unknown"], ) assert result.returncode != 0 - assert "unknown --info flag" in result.stderr + assert "unsupported --info flag" in result.stderr class TestBandwidthLimit: diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 9f8e1ca..d303f7a 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -335,6 +335,17 @@ static void test_parse_args_info_flags() { config_delete(cfg); } +static void test_parse_args_info_verbose_order() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--info=copy", "--verbose", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0); + EXPECT_EQ_INT(get_log_info_flags(), LOG_INFO_COPY); + config_delete(cfg); +} + static void test_parse_args_rejects_invalid_info_flag() { Config* cfg = config_create(); char* argv[] = {"fastsync", "--info=copy,unknown", "/src", "/dst"}; @@ -382,6 +393,7 @@ void test_client_cli() { test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); test_parse_args_info_flags(); + test_parse_args_info_verbose_order(); test_parse_args_rejects_invalid_info_flag(); test_parse_args_archive(); } From 605f79a45077c51e8d48033e6d1d1f730d14c3f5 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 22:44:44 +0200 Subject: [PATCH 3/3] fix: make rsync info none override verbose --- RSYNC_COMPAT.md | 2 +- src/client/usage.c | 1 + src/shared/log.c | 6 +++++- tests/integration/test_features.py | 13 +++++++++++++ tests/test_client_cli.c | 17 +++++++++++++---- 5 files changed, 33 insertions(+), 6 deletions(-) diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 92fa333..a3d6f36 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -23,7 +23,7 @@ This document maps rsync's full feature set to FastSync's current implementation | `-q`, `--quiet` | Suppress non-error messages | ❌ Not Implemented | Removed because it had no effect | | `--help` | Show help | ✅ Implemented | Prints usage and exits; `-h` is not accepted | | `-V`, `--version` | Print version | ✅ Implemented | | -| `--info=FLAGS` | Fine-grained info verbosity | ✅ Implemented | Supports `copy`, `misc`, `skip`, `stats`, and `none`; unsupported names are rejected | +| `--info=FLAGS` | Fine-grained info verbosity | ✅ Implemented | Supports `copy`, `misc`, `skip`, `stats`, and `none`; explicit flags override `--verbose`, and `none` suppresses info output; unsupported names are rejected | | `--debug=FLAGS` | Fine-grained debug verbosity | ❌ Not Implemented | Removed because it had no effect | | `--stderr=MODE` | Change stderr output mode | ❌ Not Implemented | | | `--no-motd` | Suppress daemon MOTD | ❌ Not Implemented | | diff --git a/src/client/usage.c b/src/client/usage.c index 581f414..95f444e 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -38,6 +38,7 @@ void print_usage(void) { printf(" -f Enable sendfile (TCP only, not with -c or -s)\n"); printf(" -v, --verbose Enable debug logging\n"); printf(" --info=FLAGS Fine-grained info: copy,misc,skip,stats,all,none\n"); + printf(" none suppresses info even with --verbose\n"); printf(" -M, --preserve Preserve file metadata\n"); printf(" --chunk-size Chunk size in bytes (default: %d)\n", DEFAULT_CHUNK_SIZE); printf(" --source-dir Source directory\n"); diff --git a/src/shared/log.c b/src/shared/log.c index 29d2501..899894c 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -1,5 +1,6 @@ #include "log.h" #include +#include #include #include #include @@ -8,6 +9,7 @@ static const char* log_level_strings[] = {"DEBUG", "INFO", "WARN", "ERROR"}; static LogLevel current_log_level = LOG_LEVEL_WARNING; static uint32_t info_flags = 0; +static bool info_flags_explicit = false; static FILE* log_fp = NULL; void set_log_level(LogLevel level) { @@ -16,6 +18,7 @@ void set_log_level(LogLevel level) { void set_log_info_flags(uint32_t flags) { info_flags = flags; + info_flags_explicit = true; } uint32_t get_log_info_flags(void) { @@ -63,7 +66,8 @@ void log_message(LogLevel log_level, const char* format, ...) { } void log_info_message(LogInfoFlag flag, const char* format, ...) { - if ((info_flags & flag) == 0 && current_log_level > LOG_LEVEL_DEBUG) + if ((info_flags_explicit && (info_flags & flag) == 0) || + (!info_flags_explicit && current_log_level > LOG_LEVEL_DEBUG)) return; time_t now = time(NULL); diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index f0836cb..1e0eeab 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -295,6 +295,19 @@ class TestInfo: assert result.returncode != 0 assert "unsupported --info flag" in result.stderr + @pytest.mark.parametrize("flags", [ + ["--info=none", "--verbose"], + ["--verbose", "--info=none"], + ]) + def test_info_none_suppresses_verbose_info(self, shared_server, flags): + clean_dir(DEST_DIR) + result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=flags, port=shared_server.port) + assert result.returncode == 0, f"Info sync failed: {(result.stderr or result.stdout)[:200]}" + output = result.stdout + result.stderr + assert "[INFO]" not in output + assert "Transferring" not in output + assert "Transfer summary:" not in output + class TestBandwidthLimit: def test_bwlimit_runs(self, shared_server): diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index d303f7a..bc75149 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -336,13 +336,22 @@ static void test_parse_args_info_flags() { } static void test_parse_args_info_verbose_order() { - Config* cfg = config_create(); - char* argv[] = {"fastsync", "--info=copy", "--verbose", "/src", "/dst"}; + char* argv_info_first[] = {"fastsync", "--info=none", "--verbose", "/src", "/dst"}; + char* argv_verbose_first[] = {"fastsync", "--verbose", "--info=none", "/src", "/dst"}; int positional_args[2]; int positional_count = 0; - EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0); - EXPECT_EQ_INT(get_log_info_flags(), LOG_INFO_COPY); + Config* cfg = config_create(); + EXPECT_EQ_INT(parse_args(cfg, 5, argv_info_first, positional_args, &positional_count), 0); + EXPECT_EQ_INT(cfg->info_level, 0); + EXPECT_EQ_INT(get_log_info_flags(), 0); + config_delete(cfg); + + cfg = config_create(); + positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 5, argv_verbose_first, positional_args, &positional_count), 0); + EXPECT_EQ_INT(cfg->info_level, 0); + EXPECT_EQ_INT(get_log_info_flags(), 0); config_delete(cfg); }