diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 9fd67ac..5fffe32 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -206,6 +206,7 @@ static const OptionEntry OPTION_TABLE[] = { {"--remove-source-files", NULL, OPT_FLAG, offsetof(Config, remove_source_files)}, {"--delete", NULL, OPT_FLAG, offsetof(Config, use_delete)}, {"--incremental", NULL, OPT_FLAG, offsetof(Config, use_incremental)}, + {"--ignore-times", "-I", OPT_FLAG, offsetof(Config, ignore_times)}, {"--delta", NULL, OPT_FLAG, offsetof(Config, use_delta)}, {"--whole-file", "-W", OPT_FLAG, offsetof(Config, whole_file)}, {"--save-to-disk", NULL, OPT_FLAG, offsetof(Config, save_to_disk)}, diff --git a/src/client/usage.c b/src/client/usage.c index 6a2968b..1d0aab3 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -30,6 +30,7 @@ void print_usage(void) { 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(" -I, --ignore-times Transfer files even when size and mtime match\n"); printf(" --delta Delta transfer for changed files (requires --incremental)\n"); printf(" -W, --whole-file Transfer changed files without delta processing\n"); printf(" --delta-block Delta block size in bytes (default: %d)\n", diff --git a/src/server/receiver.c b/src/server/receiver.c index 2235cf9..a84cbc3 100644 --- a/src/server/receiver.c +++ b/src/server/receiver.c @@ -60,7 +60,7 @@ static bool receiver_process_batch(Config* config, int file_descriptor) { } struct stat st; bool has_old = file_stat_secure(full_path, &st); - bool match = has_old && (unsigned long long)st.st_size == check_size && + bool match = !config->ignore_times && has_old && (unsigned long long)st.st_size == check_size && (long long)st.st_mtime == check_mtime; bool sent = send_status(file_descriptor, match ? STATUS_OK : STATUS_NEXT); free(full_path); diff --git a/src/server/server.c b/src/server/server.c index f8a2fcc..b1970af 100644 --- a/src/server/server.c +++ b/src/server/server.c @@ -174,7 +174,8 @@ int receive_files(Config* config, int fd) { return -1; } bool has_old = full_path && file_stat_secure(full_path, &st); - bool match = has_old && (unsigned long long)st.st_size == check_size && + bool match = !config->ignore_times && has_old && + (unsigned long long)st.st_size == check_size && (long long)st.st_mtime == check_mtime; bool sent = send_status(fd, match ? STATUS_OK : STATUS_NEXT); free(full_path); diff --git a/src/shared/config.c b/src/shared/config.c index 3f45183..47f529a 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -35,6 +35,7 @@ static void config_set_defaults(Config* config) { config->max_size = 0; config->min_size = 0; config->use_incremental = false; + config->ignore_times = false; config->use_delta = false; config->whole_file = false; config->delta_block_size = DELTA_BLOCK_SIZE_DEFAULT; @@ -122,9 +123,9 @@ static bool validate_received_config(const Config* config) { valid_wire_bool(config->use_compression) && valid_wire_bool(config->use_metadata) && valid_wire_bool(config->use_sendfile) && valid_wire_bool(config->use_delete) && valid_wire_bool(config->use_incremental) && valid_wire_bool(config->use_delta) && - valid_wire_bool(config->backup) && valid_wire_bool(config->follow_symlinks) && - valid_wire_bool(config->copy_links) && valid_wire_bool(config->safe_links) && - valid_wire_bool(config->copy_unsafe_links) && + valid_wire_bool(config->ignore_times) && valid_wire_bool(config->backup) && + valid_wire_bool(config->follow_symlinks) && valid_wire_bool(config->copy_links) && + valid_wire_bool(config->safe_links) && valid_wire_bool(config->copy_unsafe_links) && valid_wire_bool(config->preserve_hard_links) && valid_wire_bool(config->preserve_acls) && valid_wire_bool(config->preserve_xattrs) && valid_wire_bool(config->preserve_devices) && valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->update) && @@ -235,7 +236,7 @@ static bool send_core_fields(int fd, const Config* c) { static bool send_delta_fields(int fd, const Config* c) { return send_int(fd, c->use_delete) && send_int(fd, c->use_incremental) && - send_int(fd, c->use_delta && !c->whole_file) && + send_int(fd, c->ignore_times) && send_int(fd, c->use_delta && !c->whole_file) && send_n_data(fd, &c->delta_block_size, sizeof(c->delta_block_size)) && send_n_data(fd, &c->delta_max_file_size, sizeof(unsigned long long)); } @@ -292,6 +293,8 @@ static bool receive_delta_fields(int fd, Config* c) { return false; if (!receive_wire_bool(fd, &c->use_incremental)) return false; + if (!receive_wire_bool(fd, &c->ignore_times)) + return false; if (!receive_wire_bool(fd, &c->use_delta)) return false; return receive_n_data(fd, &c->delta_block_size, sizeof(c->delta_block_size)) && diff --git a/src/shared/config.h b/src/shared/config.h index 930268e..541ca15 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -35,6 +35,7 @@ typedef struct Config { unsigned long long max_size; unsigned long long min_size; bool use_incremental; + bool ignore_times; bool use_delta; bool whole_file; uint32_t delta_block_size; diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index cbca23c..6c44c55 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -378,7 +378,8 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) { close(old_fd); } - bool match = has_old_file && (unsigned long long)st.st_size == check_size; + bool match = + !config->ignore_times && has_old_file && (unsigned long long)st.st_size == check_size; if (match && config->checksum) { uint64_t old_checksum = old_size == 0 ? delta_xxhash64("", 0) : 0; if (old_data) @@ -427,6 +428,8 @@ File* receive_incremental_check(int fd, const Config* config, bool* skipped) { } if (!try_delta) { + free(old_data); + old_data = NULL; if (!send_status(fd, STATUS_NEXT)) { free(full_path); free(check_path); diff --git a/tests/integration/test_features.py b/tests/integration/test_features.py index 713383f..eeb287f 100644 --- a/tests/integration/test_features.py +++ b/tests/integration/test_features.py @@ -331,6 +331,26 @@ class TestIncremental: with open(received_file, "rb") as f: assert f.read() == b"hello world\n" + def test_ignore_times_transfers_same_size_and_mtime(self, shared_server): + clean_dir(DEST_DIR) + result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port) + assert result.returncode == 0 + + received = get_dest_received_dir(DEST_DIR, SOURCE_DIR) + source_file = os.path.join(SOURCE_DIR, "small.txt") + received_file = os.path.join(received, "small.txt") + source_stat = os.stat(source_file) + with open(received_file, "wb") as f: + f.write(b"stale data!\n") + os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime)) + + result, _ = run_client(SOURCE_DIR, DEST_DIR, + flags=["-M", "--incremental", "--ignore-times"], + port=shared_server.port) + assert result.returncode == 0, f"Ignore-times sync failed: {result.stderr[:200]}" + with open(received_file, "rb") as f: + assert f.read() == b"hello world\n" + def test_whole_file_disables_delta_and_keeps_compression(self, shared_server): clean_dir(DEST_DIR) result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-M"], port=shared_server.port) diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 6755668..bcb0e94 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -441,6 +441,24 @@ static void test_parse_args_fsync() { config_delete(cfg); } +static void test_parse_args_ignore_times() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "-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->ignore_times); + config_delete(cfg); + + cfg = config_create(); + char* long_argv[] = {"fastsync", "--ignore-times", "/src", "/dst"}; + positional_count = 0; + EXPECT_EQ_INT(parse_args(cfg, 4, long_argv, positional_args, &positional_count), 0); + EXPECT_TRUE(cfg->ignore_times); + config_delete(cfg); +} + /* --secluded-args is accepted for compatibility but has no effect. */ static void test_parse_args_secluded_args() { Config* cfg = config_create(); @@ -564,6 +582,7 @@ void test_client_cli() { test_parse_args_human_readable(); test_parse_args_archive(); test_parse_args_fsync(); + test_parse_args_ignore_times(); test_parse_args_8_bit_output(); test_parse_args_stderr_modes(); test_parse_args_rejects_unsupported_stderr_modes(); diff --git a/tests/test_config.c b/tests/test_config.c index 6f88026..748e53a 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -127,6 +127,7 @@ static void test_config_send_receive() { send_cfg->use_fsync = true; send_cfg->use_delta = true; send_cfg->whole_file = true; + send_cfg->ignore_times = true; send_cfg->compression_level = 5; send_cfg->chunk_size = 1024; send_cfg->eight_bit_output = true; @@ -160,6 +161,8 @@ static void test_config_send_receive() { ok = false; if (!recv_cfg->use_chunk_serialization) ok = false; + if (!recv_cfg->ignore_times) + ok = false; if (recv_cfg->compression_level != 5) ok = false; if (recv_cfg->chunk_size != 1024)