diff --git a/README.md b/README.md index 04bc1df..d6b7a42 100644 --- a/README.md +++ b/README.md @@ -40,12 +40,10 @@ A high-performance file synchronization system with SSH and TCP transport, TLS e - Optional progress display with throughput - Bandwidth limiting via token-bucket algorithm - Configurable I/O and connection timeouts (`--timeout`, `--contimeout`) -- Quiet mode (`-q`/`--quiet`) suppresses all non-error output - Backup overwritten files (`--backup`) with optional directory (`--backup-dir`) - Transfer statistics summary (`--stats`) - Maximum directory depth control (`--max-depth`) - Log file output (`--log-file`) -- Configurable multithreaded queue size (`--queue-size`) - Exclude patterns from file (`--exclude-from`) ### Server @@ -116,8 +114,6 @@ Config negotiation is sender-driven: the client serializes transfer options and | `-n, --dry-run` | Scan and print what would be transferred | | `-p ` | SSH port (default: 22) | | `-v, --verbose` | Enable debug logging | -| `-q, --quiet` | Suppress all non-error output | -| `--silent` | Alias for `--quiet` | | `--progress` | Show real-time transfer speed | | `--delete` | Delete files on receiver not present in source | | `--exclude ` | Exclude files matching glob pattern (repeatable) | @@ -135,7 +131,6 @@ Config negotiation is sender-driven: the client serializes transfer options and | `--stats` | Print transfer statistics at end (bytes, files, timing) | | `--max-depth ` | Maximum directory depth to recurse (0 = unlimited, default: 0) | | `--log-file ` | Write log messages to file instead of stderr | -| `--queue-size ` | Queue capacity for multithreaded mode (default: 100) | | `--source-dir ` | Source directory (overrides `FASTSYNC_SOURCE_DIR`) | | `--dest-dir ` | Server destination directory (overrides `FASTSYNC_DEST_DIR`) | | `--save-to-disk` | Write received files to disk | @@ -296,8 +291,8 @@ Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh u # Bandwidth limit to 1 MB/s ./build/client --bwlimit 1024 /src user@host:/dst -# With timeouts, quiet mode, and stats -./build/client --timeout 60 --contimeout 15 --quiet --stats /src user@host:/dst +# With timeouts and stats +./build/client --timeout 60 --contimeout 15 --stats /src user@host:/dst # Backup overwritten files to a directory ./build/client --backup --backup-dir /backups /src user@host:/dst @@ -305,9 +300,6 @@ Place the `fastsync-server` binary in the remote `$PATH`. The client runs `ssh u # Exclude patterns from file, limit depth ./build/client --exclude-from ignore.txt --max-depth 3 /src user@host:/dst -# Custom queue size for multithreading -./build/client -m --queue-size 200 /src user@host:/dst - # Log to file ./build/client --log-file /tmp/fastsync.log /src user@host:/dst @@ -334,7 +326,7 @@ The benchmark prints throughput metrics, best configuration, and speedup vs rsyn 1. Chunk size (~10 MB default) balances memory and transfer efficiency 2. Compression level trades CPU for bandwidth 3. `sendfile()` bypasses userspace — ~2× faster on localhost for large files -4. Multithreading scales with core count; `--queue-size` controls pipeline buffering +4. Multithreading scales with core count and uses memory-based pipeline sizing 5. Metadata transfer adds negligible overhead (~24 bytes per file when enabled) 6. SSH socketpair buffer set to 1 MB for improved pipe throughput 7. SSH ControlMaster reuses connections across repeated invocations diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index e9f6dff..65a265b 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -6,10 +6,10 @@ This document maps rsync's full feature set to FastSync's current implementation | Status | Count | Description | |--------|-------|-------------| -| ✅ Implemented | 39 | Feature works end-to-end | -| 🔀 Alt Arg | 5 | Functionality exists but under different flag/semantics | -| ⚠️ Partial | 30 | Flag parsed/stored but behavior incomplete | -| ❌ Not Implemented | 62 | Flag not recognized or no behavior | +| ✅ Implemented | 34 | Feature works end-to-end | +| 🔀 Alt Arg | 3 | Functionality exists but under different flag/semantics | +| ⚠️ Partial | 1 | Flag parsed/stored but behavior incomplete | +| ❌ Not Implemented | 98 | Flag not recognized or no behavior | | **Total** | **136** | | --- @@ -20,31 +20,31 @@ 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 | ✅ Implemented | `quiet` config field | -| `-h`, `--help` | Show help | ✅ Implemented | Prints usage and exits | -| `-V`, `--version` | Print version | ❌ Not Implemented | | -| `--info=FLAGS` | Fine-grained info verbosity | ⚠️ Partial | `info_level` stored, not wired | -| `--debug=FLAGS` | Fine-grained debug verbosity | ⚠️ Partial | `debug_level` stored, not wired | +| `-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 | +| `--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 | | | `--exclude=PATTERN` | Exclude files matching pattern | ✅ Implemented | Glob matching in scanner | | `--include=PATTERN` | Include files matching pattern | ✅ Implemented | Glob matching in scanner | -| `-C`, `--cvs-exclude` | Auto-ignore CVS files | ⚠️ Partial | `cvs_exclude` stored, not wired | +| `-C`, `--cvs-exclude` | Auto-ignore CVS files | ❌ Not Implemented | Removed because it had no effect | ## 2. Modifying Output | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `--stats` | Give transfer stats | ✅ Implemented | Prints file/byte counts | -| `-h`, `--human-readable` | Human-readable numbers | ⚠️ Partial | `human_readable` stored, not wired | -| `-i`, `--itemize-changes` | Per-file change summary | ⚠️ Partial | `itemize_changes` stored, not wired | +| `-h`, `--human-readable` | Human-readable numbers | ❌ Not Implemented | Removed because it had no effect | +| `-i`, `--itemize-changes` | Per-file change summary | ❌ Not Implemented | Removed because it had no effect | | `--progress` | Show progress | ✅ Implemented | Progress callback in sender | | `-P` | Same as --partial --progress | ❌ Not Implemented | | -| `--out-format=FORMAT` | Custom output format | ⚠️ Partial | `out_format` stored, not wired | +| `--out-format=FORMAT` | Custom output format | ❌ Not Implemented | Removed because it had no effect | | `--log-file=FILE` | Log to file | ✅ Implemented | `log_file` config field | | `--log-file-format=FMT` | Log format | ❌ Not Implemented | | | `--8-bit-output` | Leave high-bit chars unescaped | ❌ Not Implemented | | -| `--list-only` | List files instead of copying | ⚠️ Partial | `list_only` stored, not wired | +| `--list-only` | List files instead of copying | ❌ Not Implemented | Removed because it had no effect | ## 3. File Selection @@ -52,8 +52,8 @@ This document maps rsync's full feature set to FastSync's current implementation |------|-------------------|-----------------|-------| | `--exclude-from=FILE` | Read exclude patterns from file | ✅ Implemented | Reads patterns from file | | `--include-from=FILE` | Read include patterns from file | ✅ Implemented | Reads patterns from file | -| `--filter=RULE` | Add file-filtering rule | ⚠️ Partial | `filters` ArrayList stored, not wired | -| `--files-from=FILE` | Read source file list from file | ⚠️ Partial | `files_from` stored, not wired | +| `--filter=RULE` | Add file-filtering rule | ❌ Not Implemented | Removed because it had no effect | +| `--files-from=FILE` | Read source file list from file | ❌ Not Implemented | Removed because it had no effect | | `-0`, `--from0` | Delimit *-from files with NULs | ❌ Not Implemented | | | `--max-size=SIZE` | Skip files larger than SIZE | ✅ Implemented | `max_size` in scanner | | `--min-size=SIZE` | Skip files smaller than SIZE | ✅ Implemented | `min_size` in scanner | @@ -69,7 +69,7 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `-r`, `--recursive` | Recurse into directories | ✅ Implemented | Default behavior | -| `-R`, `--relative` | Use relative path names | ⚠️ Partial | `relative` stored, not wired | +| `-R`, `--relative` | Use relative path names | ❌ Not Implemented | Removed because it had no effect | | `--no-implied-dirs` | Don't send implied dirs with -R | ❌ Not Implemented | | | `-d`, `--dirs` | Transfer dirs without recursing | ❌ Not Implemented | | | `--mkpath` | Create missing path components | ❌ Not Implemented | | @@ -78,10 +78,10 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `-u`, `--update` | Skip files newer on receiver | ⚠️ Partial | `update` stored, not wired | +| `-u`, `--update` | Skip files newer on receiver | ❌ Not Implemented | Removed because it had no effect | | `--inplace` | Update files in-place | ✅ Implemented | Direct write mode | -| `--append` | Append data to shorter files | ⚠️ Partial | `append` stored, not wired | -| `--append-verify` | Append with old-data checksum | ⚠️ Partial | `append_verify` stored, not wired | +| `--append` | Append data to shorter files | ❌ Not Implemented | Removed because it had no effect | +| `--append-verify` | Append with old-data checksum | ❌ Not Implemented | Removed because it had no effect | | `-W`, `--whole-file` | Copy whole file (no delta) | ❌ Not Implemented | | | `--block-size=SIZE` | Force checksum block-size | ⚠️ Partial | Parsed as `--delta-block`; controls delta transfer block size | @@ -100,15 +100,15 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `--delete` | Delete extraneous files from dest | ✅ Implemented | `use_delete` config field | -| `--delete-before` | Delete before transfer | ⚠️ Partial | `delete_before` stored, not wired | +| `--delete-before` | Delete before transfer | ❌ Not Implemented | Removed because it had no effect | | `--delete-during` | Delete during transfer | ❌ Not Implemented | | | `--delete-delay` | Find deletions during, delete after | ❌ Not Implemented | | -| `--delete-after` | Delete after transfer | ⚠️ Partial | `delete_after` stored, not wired | -| `--delete-excluded` | Also delete excluded files | ⚠️ Partial | `delete_excluded` stored, not wired | -| `--max-delete=NUM` | Max files to delete | ⚠️ Partial | `max_delete` stored, not wired | +| `--delete-after` | Delete after transfer | ❌ Not Implemented | Removed because it had no effect | +| `--delete-excluded` | Also delete excluded files | ❌ Not Implemented | Removed because it had no effect | +| `--max-delete=NUM` | Max files to delete | ❌ Not Implemented | Removed because it had no effect | | `--ignore-errors` | Delete even with I/O errors | ❌ Not Implemented | | | `--force` | Force deletion of non-empty dirs | ❌ Not Implemented | | -| `--prune-empty-dirs` | Prune empty dir chains | ⚠️ Partial | `prune_empty_dirs` stored, not wired | +| `--prune-empty-dirs` | Prune empty dir chains | ❌ Not Implemented | Removed because it had no effect | ## 8. Metadata Preservation @@ -121,11 +121,11 @@ This document maps rsync's full feature set to FastSync's current implementation | `-t`, `--times` | Preserve modification times | ✅ Implemented | Part of -M | | `-E`, `--executability` | Preserve executability | ❌ Not Implemented | | | `--chmod=CHMOD` | Affect file permissions | ❌ Not Implemented | | -| `-A`, `--acls` | Preserve ACLs | ⚠️ Partial | `preserve_acls` stored, not wired | -| `-X`, `--xattrs` | Preserve extended attributes | ⚠️ Partial | `preserve_xattrs` stored, not wired | -| `-H`, `--hard-links` | Preserve hard links | ⚠️ Partial | `preserve_hard_links` stored, not wired | -| `-D` | Same as --devices --specials | 🔀 Alt Arg | Maps to --devices only (no --specials) | -| `--devices` | Preserve device files | ⚠️ Partial | `preserve_devices` stored, not wired | +| `-A`, `--acls` | Preserve ACLs | ❌ Not Implemented | Removed because it had no effect | +| `-X`, `--xattrs` | Preserve extended attributes | ❌ Not Implemented | Removed because it had no effect | +| `-H`, `--hard-links` | Preserve hard links | ❌ Not Implemented | Removed because it had no effect | +| `-D` | Same as --devices --specials | ❌ Not Implemented | Removed because device-file handling is not implemented | +| `--devices` | Preserve device files | ❌ Not Implemented | Removed because it had no effect | | `--specials` | Preserve special files | ❌ Not Implemented | | | `--copy-devices` | Copy device contents as file | ❌ Not Implemented | | | `--write-devices` | Write to devices as files | ❌ Not Implemented | | @@ -159,11 +159,11 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `-c`, `--checksum` | Skip based on checksum | 🔀 Alt Arg | `-c` means compression; `--checksum` stored and forwarded to scanner | +| `--checksum` | Skip based on checksum | ❌ Not Implemented | Removed because it had no effect; `-c` means compression | | `--checksum-choice=STR` | Choose checksum algorithm | ❌ Not Implemented | xxHash used internally | -| `--compare-dest=DIR` | Compare dest files relative to DIR | ⚠️ Partial | `compare_dest` stored, not wired | -| `--copy-dest=DIR` | Include copies of unchanged files | ⚠️ Partial | `copy_dest` stored, not wired | -| `--link-dest=DIR` | Hardlink to files when unchanged | ⚠️ Partial | `link_dest` stored, not wired | +| `--compare-dest=DIR` | Compare dest files relative to DIR | ❌ Not Implemented | Removed because it had no effect | +| `--copy-dest=DIR` | Include copies of unchanged files | ❌ Not Implemented | Removed because it had no effect | +| `--link-dest=DIR` | Hardlink to files when unchanged | ❌ Not Implemented | Removed because it had no effect | | `--fuzzy`, `--no-fuzzy` | Find similar file for basis | ❌ Not Implemented | | ## 12. Compression @@ -171,7 +171,7 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| | `-z`, `--compress` | Compress file data | 🔀 Alt Arg | Always uses zstd (rsync supports multiple algorithms) | -| `--compress-choice=STR` | Choose compression algorithm | ⚠️ Partial | `compress_choice` stored, always zstd | +| `--compress-choice=STR` | Choose compression algorithm | ❌ Not Implemented | Removed because it had no effect; FastSync always uses zstd | | `--compress-level=NUM` | Set compression level | ✅ Implemented | 1-22, default 5 | | `--compress-threads=NUM` | Set compression threads | ❌ Not Implemented | | | `--skip-compress=LIST` | Skip compress for suffixes | ❌ Not Implemented | Internal skip for hardcoded types; not user-configurable | @@ -180,22 +180,22 @@ This document maps rsync's full feature set to FastSync's current implementation | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `-e`, `--rsh=COMMAND` | Remote shell to use | ✅ Implemented | SSH transport support | -| `--rsync-path=PROGRAM` | rsync binary on remote | ✅ Implemented | `rsync_path` config field | +| `-e`, `--rsh=COMMAND` | Remote shell to use | ❌ Not Implemented | Removed; SSH invokes `ssh` directly | +| `--rsync-path=PROGRAM` | rsync binary on remote | ❌ Not Implemented | Removed; use `--fastsync-server-path` | | `--port=PORT` | Alternate daemon port | ✅ Implemented | `server_port` config field | | `--sockopts=OPTIONS` | Custom TCP options | ❌ Not Implemented | | | `--blocking-io` | Use blocking I/O for remote shell | ❌ Not Implemented | | | `--outbuf=N\|L\|B` | Set output buffering | ❌ Not Implemented | | -| `--address=ADDRESS` | Bind address for outgoing socket | ✅ Implemented | `address` config field | -| `-4`, `--ipv4` | Prefer IPv4 | ✅ Implemented | `ipv4` config field | -| `-6`, `--ipv6` | Prefer IPv6 | ✅ Implemented | `ipv6` config field | +| `--address=ADDRESS` | Bind address for outgoing socket | ❌ Not Implemented | Removed because it had no effect | +| `-4`, `--ipv4` | Prefer IPv4 | ❌ Not Implemented | Removed because it had no effect | +| `-6`, `--ipv6` | Prefer IPv6 | ❌ Not Implemented | Removed because it had no effect | ## 14. Daemon Mode | Flag | Rsync Description | FastSync Status | Notes | |------|-------------------|-----------------|-------| -| `--daemon` | Run as rsync daemon | ⚠️ Partial | `daemon` stored, not wired | -| `--config=FILE` | Alternate rsyncd.conf file | ⚠️ Partial | `daemon_config` stored, not wired | +| `--daemon` | Run as rsync daemon | ❌ Not Implemented | Removed because it had no effect | +| `--config=FILE` | Alternate rsyncd.conf file | ❌ Not Implemented | Removed because it had no effect | | `--dparam=OVERRIDE` | Override global daemon config | ❌ Not Implemented | | | `--no-detach` | Don't detach from parent | ❌ Not Implemented | | | `--password-file=FILE` | Read daemon password from file | ❌ Not Implemented | | @@ -266,7 +266,6 @@ Ranked by user demand, implementation complexity, and interoperability impact: | `-f` / `--sendfile` | Zero-copy sendfile() syscall (TCP only) | | `-c [level]` | zstd compression level (1-22) | | `--chunk-size` | Configurable chunk size | -| `--queue-size` | Pipeline queue capacity | | `--tls` | TLS encryption (mutual auth) | | `--fastsync-server-path` | Path to fastsync-server binary | | `--server-host` / `--server-port` | Direct TCP connection | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 78b28ec..c00a9f2 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -8,9 +8,11 @@ #include "utils.h" #include #include +#include #include #include #include +#include "usage.c" #ifndef FASTSYNC_TEST_BUILD /* Parse environment variables for source/destination directories and save-to-disk flag. */ @@ -25,19 +27,6 @@ static void parse_environment(const char** out_env_source, const char** out_env_ } #endif -/* Parse a string as a positive integer, returning true on success. */ -static bool parse_positive_int(const char* s, int* out_val) { - if (!s || *s == '\0') - return false; - char* endptr; - errno = 0; - long val = strtol(s, &endptr, 10); - if (errno != 0 || *endptr != '\0' || val <= 0 || val > INT_MAX) - return false; - *out_val = (int)val; - return true; -} - /* Parse a string as a non-negative integer, returning true on success. */ static bool parse_nonneg_int(const char* s, int* out_val) { if (!s || *s == '\0') @@ -51,7 +40,20 @@ static bool parse_nonneg_int(const char* s, int* out_val) { return true; } -/* Duplicate a string argument into *dest, freeing the old value. Returns 0 on success, -1 on +/* Parse a string as a positive integer, returning true on success. */ +static bool parse_positive_int(const char* s, int* out_val) { + int temp; + if (!parse_nonneg_int(s, &temp)) { + return false; + } + if (temp == 0) { + return false; + } + *out_val = temp; + return true; +} + +/* Duplicate a string argument into *dest, freeing the old value. Returns true on success, false on * failure. */ static int set_string_option(char** dest, const char* value, const char* option_name) { char* dup = str_dup(value); @@ -64,7 +66,7 @@ static int set_string_option(char** dest, const char* value, const char* option_ return 0; } -/* Parse a string as a positive integer into *dest. Returns 0 on success, -1 on error. */ +/* Parse a string as a positive integer into *dest. Returns true on success, false on error. */ static int set_positive_int_option(int* dest, const char* value, const char* option_name) { if (!parse_positive_int(value, dest)) { fprintf(stderr, "Error: %s must be a positive integer\n", option_name); @@ -73,7 +75,7 @@ static int set_positive_int_option(int* dest, const char* value, const char* opt return 0; } -/* Parse a string as a non-negative integer into *dest. Returns 0 on success, -1 on error. */ +/* Parse a string as a non-negative integer into *dest. Returns true on success, false on error. */ static int set_nonneg_int_option(int* dest, const char* value, const char* option_name) { if (!parse_nonneg_int(value, dest)) { fprintf(stderr, "Error: %s must be a non-negative integer\n", option_name); @@ -82,7 +84,6 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio return 0; } -static void print_usage(void); static int read_patterns_from_file(const char* filepath, char*** patterns, int* count); /* Parse CLI arguments into config. Returns 0 on success, -1 on error, 1 for help/clean-exit. */ @@ -103,12 +104,10 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (strcmp(argv[i], "-n") == 0 || strcmp(argv[i], "--dry-run") == 0) { config->dry_run = true; } else if (strcmp(argv[i], "-p") == 0 && i + 1 < argc) { - if (!parse_positive_int(argv[++i], &config->ssh_port)) { - fprintf(stderr, "Error: invalid --port/-p value: %s\n", argv[i]); + if (set_positive_int_option(&config->ssh_port, argv[++i], "-p") != 0) return -1; - } if (config->ssh_port > 65535) { - fprintf(stderr, "Error: SSH port must be 1-65535\n"); + log_message(LOG_LEVEL_ERROR, "SSH port must be 1-65535\n"); return -1; } } else if (strcmp(argv[i], "--delete") == 0) { @@ -275,9 +274,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } else if (strcmp(argv[i], "--contimeout") == 0 && i + 1 < argc) { if (set_positive_int_option(&config->contimeout, argv[++i], "--contimeout") != 0) return -1; - } else if (strcmp(argv[i], "-q") == 0 || strcmp(argv[i], "--quiet") == 0 || - strcmp(argv[i], "--silent") == 0) { - config->quiet = true; } else if (strcmp(argv[i], "--backup") == 0) { config->backup = true; } else if (strcmp(argv[i], "--backup-dir") == 0 && i + 1 < argc) { @@ -301,9 +297,6 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, } config->log_file = lf; log_set_file(lf); - } else if (strcmp(argv[i], "--queue-size") == 0 && i + 1 < argc) { - if (set_positive_int_option(&config->queue_size, argv[++i], "--queue-size") != 0) - return -1; } else if (strcmp(argv[i], "--exclude-from") == 0 && i + 1 < argc) { if (read_patterns_from_file(argv[++i], &config->exclude_patterns, &config->exclude_count) != 0) @@ -328,126 +321,21 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args, config->safe_links = true; } else if (strcmp(argv[i], "--copy-unsafe-links") == 0) { config->copy_unsafe_links = true; - } else if (strcmp(argv[i], "-H") == 0 || strcmp(argv[i], "--hard-links") == 0) { - config->preserve_hard_links = true; - } else if (strcmp(argv[i], "-A") == 0 || strcmp(argv[i], "--acls") == 0) { - config->preserve_acls = true; - } else if (strcmp(argv[i], "-X") == 0 || strcmp(argv[i], "--xattrs") == 0) { - config->preserve_xattrs = true; - } else if (strcmp(argv[i], "-D") == 0 || strcmp(argv[i], "--devices") == 0) { - config->preserve_devices = true; } else if (strcmp(argv[i], "-S") == 0 || strcmp(argv[i], "--sparse") == 0) { config->preserve_sparse = true; - } else if (strcmp(argv[i], "-i") == 0 || strcmp(argv[i], "--itemize-changes") == 0) { - config->itemize_changes = true; - } else if (strcmp(argv[i], "--out-format") == 0 && i + 1 < argc) { - if (set_string_option(&config->out_format, argv[++i], "--out-format") != 0) - return -1; - } else if (strcmp(argv[i], "--info") == 0 && i + 1 < argc) { - if (set_nonneg_int_option(&config->info_level, argv[++i], "--info") != 0) - return -1; - } else if (strcmp(argv[i], "--debug") == 0 && i + 1 < argc) { - if (set_nonneg_int_option(&config->debug_level, argv[++i], "--debug") != 0) - return -1; - } else if (strcmp(argv[i], "--list-only") == 0) { - config->list_only = true; - } else if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--human-readable") == 0) { - config->human_readable = true; - } else if (strcmp(argv[i], "-u") == 0 || strcmp(argv[i], "--update") == 0) { - config->update = true; } else if (strcmp(argv[i], "--inplace") == 0) { config->inplace = true; - } else if (strcmp(argv[i], "--append") == 0) { - config->append = true; - } else if (strcmp(argv[i], "--append-verify") == 0) { - config->append_verify = true; - } else if (strcmp(argv[i], "--delete-excluded") == 0) { - config->delete_excluded = true; - } else if (strcmp(argv[i], "--delete-after") == 0) { - config->delete_after = true; - } else if (strcmp(argv[i], "--max-delete") == 0 && i + 1 < argc) { - if (set_nonneg_int_option(&config->max_delete, argv[++i], "--max-delete") != 0) - return -1; - } else if (strcmp(argv[i], "--filter") == 0 && i + 1 < argc) { - if (!config->filters) - config->filters = array_list_create(free); - char* dup = str_dup(argv[++i]); - if (!dup) - return -1; - array_list_add(config->filters, dup); - } else if (strcmp(argv[i], "--files-from") == 0 && i + 1 < argc) { - if (set_string_option(&config->files_from, argv[++i], "--files-from") != 0) - return -1; - } else if (strcmp(argv[i], "--cvs-exclude") == 0) { - config->cvs_exclude = true; - } else if (strcmp(argv[i], "--prune-empty-dirs") == 0) { - config->prune_empty_dirs = true; - } else if (strcmp(argv[i], "-R") == 0 || strcmp(argv[i], "--relative") == 0) { - config->relative = true; - } else if (strcmp(argv[i], "-e") == 0 || strcmp(argv[i], "--rsh") == 0) { - if (i + 1 < argc) { - if (set_string_option(&config->rsh_command, argv[++i], "-e/--rsh") != 0) - return -1; - } else { - fprintf(stderr, "Error: -e/--rsh requires a command argument\n"); - return -1; - } - } else if (strcmp(argv[i], "--rsync-path") == 0 && i + 1 < argc) { - if (set_string_option(&config->rsync_path, argv[++i], "--rsync-path") != 0) - return -1; - } else if (strcmp(argv[i], "--temp-dir") == 0 && i + 1 < argc) { - if (set_string_option(&config->temp_dir, argv[++i], "--temp-dir") != 0) - return -1; - } else if (strcmp(argv[i], "--compare-dest") == 0 && i + 1 < argc) { - if (set_string_option(&config->compare_dest, argv[++i], "--compare-dest") != 0) - return -1; - } else if (strcmp(argv[i], "--copy-dest") == 0 && i + 1 < argc) { - if (set_string_option(&config->copy_dest, argv[++i], "--copy-dest") != 0) - return -1; - } else if (strcmp(argv[i], "--link-dest") == 0 && i + 1 < argc) { - if (set_string_option(&config->link_dest, argv[++i], "--link-dest") != 0) - return -1; } else if (strcmp(argv[i], "--partial-dir") == 0 && i + 1 < argc) { if (set_string_option(&config->partial_dir, argv[++i], "--partial-dir") != 0) return -1; } else if (strcmp(argv[i], "--suffix") == 0 && i + 1 < argc) { if (set_string_option(&config->suffix, argv[++i], "--suffix") != 0) return -1; - } else if (strcmp(argv[i], "--delete-before") == 0) { - config->delete_before = true; } else if (strcmp(argv[i], "-T") == 0 && i + 1 < argc) { if (set_positive_int_option(&config->timeout, argv[++i], "-T") != 0) return -1; - } else if (strcmp(argv[i], "--address") == 0 && i + 1 < argc) { - if (set_string_option(&config->address, argv[++i], "--address") != 0) - return -1; - } else if (strcmp(argv[i], "--bind-address") == 0 && i + 1 < argc) { - if (set_string_option(&config->bind_address, argv[++i], "--bind-address") != 0) - return -1; - } else if (strcmp(argv[i], "--ipv6") == 0) { - config->ipv6 = true; - } else if (strcmp(argv[i], "--ipv4") == 0) { - config->ipv4 = true; - } else if (strcmp(argv[i], "--daemon") == 0) { - config->daemon = true; - } else if (strcmp(argv[i], "--config") == 0 && i + 1 < argc) { - if (set_string_option(&config->daemon_config, argv[++i], "--config") != 0) - return -1; - } else if (strcmp(argv[i], "--server") == 0) { - config->server_mode = true; } else if (strcmp(argv[i], "--checksum") == 0) { config->checksum = true; - } else if (strcmp(argv[i], "--compress-choice") == 0 && i + 1 < argc) { - if (set_string_option(&config->compress_choice, argv[++i], "--compress-choice") != 0) - return -1; - if (strcmp(config->compress_choice, "zstd") == 0) - config->use_compression = true; - else if (strcmp(config->compress_choice, "none") == 0) - config->use_compression = false; - else { - fprintf(stderr, "Error: --compress-choice must be 'zstd' or 'none'\n"); - return -1; - } } else if (strcmp(argv[i], "--compress-level") == 0 && i + 1 < argc) { if (set_positive_int_option(&config->compression_level, argv[++i], "--compress-level") != 0) return -1; @@ -521,116 +409,6 @@ static bool validate_config(const Config* config) { } #endif /* FASTSYNC_TEST_BUILD */ -static void print_usage(void) { - printf("Usage:\n"); - printf(" fastsync [options] \n"); - printf(" fastsync [options] --source-dir --dest-dir \n"); - printf("\n"); - printf("Destination formats:\n"); - printf(" user@host:/path SSH transport (rsync-style)\n"); - printf(" host:/path SSH transport (current user)\n"); - printf(" /local/path TCP transport (requires server on localhost:8080)\n"); - printf("\n"); - printf("Options:\n"); - printf(" -c [level] Enable compression (level 1-22, default 5)\n"); - printf(" -z [level] Alias for -c\n"); - printf(" -a, --archive Archive mode (-c -m -M)\n"); - printf(" -n, --dry-run Show what would be transferred\n"); - printf(" -p SSH port (default: 22)\n"); - printf(" --progress Show transfer progress\n"); - printf(" --delete Delete files on receiver not in source\n"); - printf(" --exclude Exclude files matching pattern\n"); - printf(" --include Only include files matching pattern\n"); - printf(" --exclude-from Read exclude patterns from file\n"); - printf(" --include-from Read include patterns from file\n"); - printf(" --max-size Skip files larger than n bytes\n"); - printf(" --min-size Skip files smaller than n bytes\n"); - printf(" --incremental Skip files unchanged since last transfer\n"); - printf(" --delta Delta transfer for changed files (requires --incremental)\n"); - printf(" --delta-block Delta block size in bytes (default: %d)\n", - DELTA_BLOCK_SIZE_DEFAULT); - printf(" --delta-max Max file size for delta transfer (default: %llu)\n", - DELTA_MAX_FILE_SIZE); - printf(" -m Enable multithreading\n"); - 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(" -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"); - printf(" --dest-dir Destination directory\n"); - printf(" --save-to-disk Write received files to disk\n"); - printf(" --server-host Server IP address (default: 127.0.0.1)\n"); - printf(" --server-port Server port (default: 8080)\n"); - printf(" --bwlimit Bandwidth limit in kilobytes per second\n"); - printf(" --tls Enable TLS encryption\n"); - printf(" --cert TLS certificate file (PEM)\n"); - printf(" --key TLS private key file (PEM)\n"); - printf(" --ca TLS CA certificate file (PEM)\n"); - printf(" --timeout I/O timeout in seconds (default: 30)\n"); - printf(" -T Alias for --timeout\n"); - printf(" --contimeout Connection timeout in seconds (default: 10)\n"); - printf(" --address Server hostname/IP to connect to\n"); - printf(" --bind-address Bind to specific local address\n"); - printf(" --ipv6 Prefer IPv6 connections\n"); - printf(" --ipv4 Prefer IPv4 connections\n"); - printf(" -q, --quiet Suppress non-error output\n"); - printf(" --silent Alias for --quiet\n"); - printf(" --backup Backup existing files before overwriting\n"); - printf(" --backup-dir Directory for backups (requires --backup)\n"); - printf(" --suffix Backup suffix (default: ~)\n"); - printf(" --stats Print transfer statistics at end\n"); - printf(" --max-depth Maximum directory depth (0=unlimited)\n"); - printf(" --log-file Write log messages to file\n"); - printf(" --queue-size Queue capacity for multithreaded mode (default: 100)\n"); - printf(" --partial Keep partial files on interrupted transfer\n"); - printf(" --partial-dir Directory for partial files\n"); - printf(" --fastsync-server-path \n"); - printf(" Path to fastsync-server on remote (default: fastsync-server)\n"); - printf(" -l, --links Copy symlinks as symlinks\n"); - printf(" --copy-links Transform symlinks into referent files\n"); - printf(" --safe-links Skip symlinks that point outside transfer tree\n"); - printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n"); - printf(" -H, --hard-links Preserve hard links\n"); - printf(" -A, --acls Preserve ACLs\n"); - printf(" -X, --xattrs Preserve extended attributes\n"); - printf(" -D, --devices Preserve device files\n"); - printf(" -S, --sparse Handle sparse files efficiently\n"); - printf(" -i, --itemize-changes Show per-file change summary\n"); - printf(" --out-format Custom output format string\n"); - printf(" --info Info verbosity level\n"); - printf(" --debug Debug verbosity level\n"); - printf(" --list-only List files without transferring\n"); - printf(" -h, --human-readable Human-readable numbers\n"); - printf(" -u, --update Skip files newer on destination\n"); - printf(" --inplace Update files in-place (no temp+rename)\n"); - printf(" --append Append data to shorter files\n"); - printf(" --append-verify Append with verify\n"); - printf(" --delete-before Delete before transfer\n"); - printf(" --delete-excluded Also delete excluded files\n"); - printf(" --delete-after Delete after transfer, not before\n"); - printf(" --max-delete Maximum number of files to delete\n"); - printf(" --filter Add file filtering rule\n"); - printf(" --files-from Read file list from file\n"); - printf(" --cvs-exclude Auto-ignore CVS files\n"); - printf(" --prune-empty-dirs Omit empty directories from transfer\n"); - printf(" -R, --relative Use relative paths\n"); - printf(" -e, --rsh Specify remote shell\n"); - printf(" --rsync-path Path to remote binary\n"); - printf(" --daemon Run in daemon mode\n"); - printf(" --config Path to configuration file\n"); - printf(" --server Run in server mode\n"); - printf(" --checksum Skip files based on checksum, not mod-time/size\n"); - printf(" --compress-choice Compression algorithm (default: zstd)\n"); - printf(" --compress-level Compression level (default: 5)\n"); - printf(" --temp-dir Temporary directory for files\n"); - printf(" --compare-dest Compare destination\n"); - printf(" --copy-dest Copy destination\n"); - printf(" --link-dest Link destination\n"); - printf(" --help Show this help\n"); - printf(" -V, --version Show version\n"); -} - static int read_patterns_from_file(const char* filepath, char*** patterns, int* count) { FILE* fp = fopen(filepath, "r"); if (!fp) { diff --git a/src/client/usage.c b/src/client/usage.c new file mode 100644 index 0000000..918c293 --- /dev/null +++ b/src/client/usage.c @@ -0,0 +1,73 @@ +#include "stdio.h" +#include +#include + +static __attribute__((unused)) void print_usage() { + printf("Usage:\n"); + printf(" fastsync [options] \n"); + printf(" fastsync [options] --source-dir --dest-dir \n"); + printf("\n"); + printf("Destination formats:\n"); + printf(" user@host:/path SSH transport (rsync-style)\n"); + printf(" host:/path SSH transport (current user)\n"); + printf(" /local/path TCP transport (requires server on localhost:8080)\n"); + printf("\n"); + printf("Options:\n"); + printf(" -c [level] Enable compression (level 1-22, default 5)\n"); + printf(" -z [level] Alias for -c\n"); + printf(" -a, --archive Archive mode (-c -m -M)\n"); + printf(" -n, --dry-run Show what would be transferred\n"); + printf(" -p SSH port (default: 22)\n"); + printf(" --progress Show transfer progress\n"); + printf(" --delete Delete files on receiver not in source\n"); + printf(" --exclude Exclude files matching pattern\n"); + printf(" --include Only include files matching pattern\n"); + printf(" --exclude-from Read exclude patterns from file\n"); + printf(" --include-from Read include patterns from file\n"); + printf(" --max-size Skip files larger than n bytes\n"); + printf(" --min-size Skip files smaller than n bytes\n"); + printf(" --incremental Skip files unchanged since last transfer\n"); + printf(" --delta Delta transfer for changed files (requires --incremental)\n"); + printf(" --delta-block Delta block size in bytes (default: %d)\n", + DELTA_BLOCK_SIZE_DEFAULT); + printf(" --delta-max Max file size for delta transfer (default: %llu)\n", + DELTA_MAX_FILE_SIZE); + printf(" -m Enable multithreading\n"); + 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(" -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"); + printf(" --dest-dir Destination directory\n"); + printf(" --save-to-disk Write received files to disk\n"); + printf(" --server-host Server IP address (default: 127.0.0.1)\n"); + printf(" --server-port Server port (default: 8080)\n"); + printf(" --bwlimit Bandwidth limit in kilobytes per second\n"); + printf(" --tls Enable TLS encryption\n"); + printf(" --cert TLS certificate file (PEM)\n"); + printf(" --key TLS private key file (PEM)\n"); + printf(" --ca TLS CA certificate file (PEM)\n"); + printf(" --timeout I/O timeout in seconds (default: 30)\n"); + printf(" -T Alias for --timeout\n"); + printf(" --contimeout Connection timeout in seconds (default: 10)\n"); + printf(" --backup Backup existing files before overwriting\n"); + printf(" --backup-dir Directory for backups (requires --backup)\n"); + printf(" --suffix Backup suffix (default: ~)\n"); + printf(" --stats Print transfer statistics at end\n"); + printf(" --max-depth Maximum directory depth (0=unlimited)\n"); + printf(" --log-file Write log messages to file\n"); + printf(" --partial Keep partial files on interrupted transfer\n"); + printf(" --partial-dir Directory for partial files\n"); + printf(" --fastsync-server-path \n"); + printf(" Path to fastsync-server on remote (default: fastsync-server)\n"); + printf(" -l, --links Copy symlinks as symlinks\n"); + printf(" --copy-links Transform symlinks into referent files\n"); + printf(" --safe-links Skip symlinks that point outside transfer tree\n"); + printf(" --copy-unsafe-links Only transform unsafe symlinks into referent files\n"); + printf(" -S, --sparse Handle sparse files efficiently\n"); + printf(" --inplace Update files in-place (no temp+rename)\n"); + printf(" --compress-level Compression level (default: 5)\n"); + printf(" --help Show this help\n"); + printf(" -V, --version Show version\n"); +} diff --git a/src/shared/log.c b/src/shared/log.c index fbd9400..37a1a73 100644 --- a/src/shared/log.c +++ b/src/shared/log.c @@ -15,6 +15,15 @@ void log_set_file(FILE* fp) { log_fp = fp; } +static inline void write_message(FILE* dest_io, LogLevel log_level, struct tm t, const char* format, + va_list args) { + fprintf(dest_io, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, + t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]); + + vfprintf(dest_io, format, args); + fprintf(dest_io, "\n"); +} + void log_message(LogLevel log_level, const char* format, ...) { if (log_level < current_log_level) return; @@ -25,22 +34,19 @@ void log_message(LogLevel log_level, const char* format, ...) { if (!localtime_r(&now, &t)) return; - fprintf(stderr, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, - t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]); + FILE* dest_io = stdout; + if (log_level == LOG_LEVEL_ERROR) { + dest_io = stderr; + } va_list args; va_start(args, format); - vfprintf(stderr, format, args); + write_message(dest_io, log_level, t, format, args); va_end(args); - fprintf(stderr, "\n"); if (log_fp) { - fprintf(log_fp, "%04d-%02d-%02d %02d:%02d:%02d [%s]: ", t.tm_year + 1900, t.tm_mon + 1, - t.tm_mday, t.tm_hour, t.tm_min, t.tm_sec, log_level_strings[log_level]); va_start(args, format); - vfprintf(log_fp, format, args); + write_message(log_fp, log_level, t, format, args); va_end(args); - fprintf(log_fp, "\n"); - fflush(log_fp); } } diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index cf13dfe..9c7ef2b 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -207,6 +207,69 @@ static void test_parse_args_unknown_option() { config_delete(cfg); } +/* 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", + "--queue-size", + "-H", + "--hard-links", + "-A", + "--acls", + "-X", + "--xattrs", + "-D", + "--devices", + "-i", + "--itemize-changes", + "--out-format", + "--info", + "--debug", + "--list-only", + "-h", + "--human-readable", + "-u", + "--update", + "--append", + "--append-verify", + "--delete-excluded", + "--delete-after", + "--max-delete", + "--filter", + "--files-from", + "--cvs-exclude", + "--prune-empty-dirs", + "-R", + "--relative", + "-e", + "--rsh", + "--rsync-path", + "--temp-dir", + "--compare-dest", + "--copy-dest", + "--link-dest", + "--delete-before", + "--address", + "--bind-address", + "--ipv6", + "--ipv4", + "--daemon", + "--config", + "--server", + "--compress-choice"}; + + for (size_t i = 0; i < sizeof(options) / sizeof(options[0]); i++) { + Config* cfg = config_create(); + char* argv[] = {"fastsync", (char*)options[i], "dummy", "/src", "/dst"}; + int positional_args[2]; + int positional_count = 0; + + EXPECT_EQ_INT(parse_args(cfg, 5, 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(); @@ -238,5 +301,6 @@ void test_client_cli() { test_parse_args_invalid_compression_level(); test_parse_args_valid_compression_level(); test_parse_args_unknown_option(); + test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); }