From 4872d425a38acf29d96ac73d1b31a15a2ae79f80 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 16:13:56 +0200 Subject: [PATCH] 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(); }