From dbacc4f0e5e4cbc4a7cbed1af2c01ea4ae175d26 Mon Sep 17 00:00:00 2001 From: TapTap Date: Thu, 3 Sep 2026 15:52:36 +0200 Subject: [PATCH] feat: add quiet client option --- README.md | 1 + RSYNC_COMPAT.md | 2 +- src/client/client_cli.c | 3 +++ src/client/client_send.c | 17 ++++++++++------- src/client/usage.c | 1 + tests/integration/test_features.py | 17 +++++++++++++++++ tests/test_client_cli.c | 20 +++++++++++++++++--- 7 files changed, 50 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fdbd9f3..8f192c2 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,7 @@ partial, alternate, and planned behavior. | `-n, --dry-run` | Scan and print what would be transferred | | `-p ` | SSH port (default: 22) | | `-v, --verbose` | Enable debug logging | +| `-q, --quiet` | Suppress non-error output | | `--progress` | Show real-time transfer speed | | `--delete` | Delete files on receiver not present in source | | `--exclude ` | Exclude files matching glob pattern (repeatable) | diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 4b236ec..19a27f1 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -20,7 +20,7 @@ This document maps rsync's full feature set to FastSync's current implementation |------|-------------------|-----------------|-------| | `-a`, `--archive` | Archive mode is -rlptgoD | 🔀 Alt Arg | Maps to -c -m -M (compression + multithread + metadata) | | `-v`, `--verbose` | Increase verbosity | ✅ Implemented | Sets `log_level=DEBUG` | -| `-q`, `--quiet` | Suppress non-error messages | ❌ Not Implemented | Removed because it had no effect | +| `-q`, `--quiet` | Suppress non-error messages | ✅ Implemented | Suppresses client output while preserving errors | | `--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 | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 191cc07..0fcd804 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -351,6 +351,9 @@ 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 (opt_is(argv[i], "-q", "--quiet")) { + config->quiet = true; + set_log_level(LOG_LEVEL_ERROR); } else if (opt_is(argv[i], "-T", NULL) && i + 1 < argc) { if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) return -1; diff --git a/src/client/client_send.c b/src/client/client_send.c index 0b4ca5c..ee5feab 100644 --- a/src/client/client_send.c +++ b/src/client/client_send.c @@ -134,17 +134,20 @@ static int send_dry_run_manifest(const Config* config) { Chunk* chunk; int file_count = 0; unsigned long long total_bytes = 0; - printf("Dry run: files to be transferred\n"); + if (!config->quiet) + printf("Dry run: files to be transferred\n"); while ((chunk = directory_scanner_next(scanner)) != NULL) { for (int i = 0; i < chunk->element_count; i++) { - printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); + if (!config->quiet) + printf(" %s (%zu bytes)\n", chunk->items[i]->path, chunk->items[i]->data->size); total_bytes += chunk->items[i]->data->size; file_count++; } chunk_destroy(chunk); } directory_scanner_destroy(scanner); - printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); + if (!config->quiet) + printf("Total: %d files, %.1f MB\n", file_count, total_bytes / 1048576.0); return 0; } @@ -650,7 +653,7 @@ int send_files(Config* config) { manifest = NULL; break; } - if (config->show_progress) { + if (config->show_progress && !config->quiet) { total_bytes += chunk_bytes; time_t now = time(NULL); if (now - last_progress >= 1) { @@ -672,9 +675,9 @@ int send_files(Config* config) { manifest = NULL; } bool ok = finalize_transfer(client); - if (config->show_progress) + if (config->show_progress && !config->quiet) print_transfer_progress(total_bytes, start, "Done.\n"); - if (config->stats) { + if (config->stats && !config->quiet) { double elapsed_total = difftime(time(NULL), start); double rate = elapsed_total > 0 ? total_bytes / (1048576.0 * elapsed_total) : 0; fprintf(stderr, "Stats: %d files, %.1f MB, %.1f MB/s\n", total_files, total_bytes / 1048576.0, @@ -765,7 +768,7 @@ int send_files_multithreaded(Config** config_ptr) { thrd_t progress; bool progress_created = false; - if (config->show_progress) { + if (config->show_progress && !config->quiet) { progress_created = (thrd_create(&progress, progress_thread_fn, context) == thrd_success); if (!progress_created) { log_perror("Error creating progress thread"); diff --git a/src/client/usage.c b/src/client/usage.c index 2f800af..52810cc 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -37,6 +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(" -q, --quiet Suppress non-error output\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/tests/integration/test_features.py b/tests/integration/test_features.py index 312add4..ebd1559 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -35,6 +35,23 @@ class TestDryRun: assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}" assert "Dry run:" in result.stdout, f"No dry run output: {result.stdout[:200]}" + def test_quiet_suppresses_dry_run_output(self): + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["-q", "-n", "--progress", "--stats"], + ) + assert result.returncode == 0, f"Exit {result.returncode}: {result.stderr[:100]}" + assert result.stdout == "" + assert result.stderr == "" + + def test_quiet_preserves_errors(self): + result, dur = run_client( + SOURCE_DIR, DEST_DIR, + flags=["--quiet", "--server-port", "1"], + ) + assert result.returncode != 0 + assert result.stderr != "" + class TestArchiveMode: def test_archive_mode(self, shared_server): diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 656fd40..5344b8b 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -262,9 +262,7 @@ static void test_parse_args_unknown_option() { /* Parsed-but-unimplemented options must fail instead of being silently accepted. */ static void test_parse_args_rejects_unimplemented_options() { - static const char* const options[] = {"-q", - "--quiet", - "--silent", + static const char* const options[] = {"--silent", "--queue-size", "-H", "--hard-links", @@ -323,6 +321,21 @@ static void test_parse_args_rejects_unimplemented_options() { } } +/* Test both rsync-compatible quiet spellings. */ +static void test_parse_args_quiet() { + static const char* const options[] = {"-q", "--quiet"}; + for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { + Config* cfg = config_create(); + char* argv[] = {"fastsync", (char*)options[i], "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0); + EXPECT_TRUE(cfg->quiet); + config_delete(cfg); + } +} + /* Test parse_args with --archive flag */ static void test_parse_args_archive() { Config* cfg = config_create(); @@ -359,5 +372,6 @@ void test_client_cli() { test_parse_args_valid_compression_level(); test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); + test_parse_args_quiet(); test_parse_args_archive(); }