feat: add rsync-compatible update option
CI / lint (pull_request) Successful in 11s
CI / sanitizers (address) (pull_request) Successful in 39s
CI / sanitizers (undefined) (pull_request) Successful in 39s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m16s
CI / valgrind (pull_request) Successful in 33s
CI / lint (pull_request) Successful in 11s
CI / sanitizers (address) (pull_request) Successful in 39s
CI / sanitizers (undefined) (pull_request) Successful in 39s
CI / fuzz-build (pull_request) Successful in 14s
CI / coverage (pull_request) Successful in 32s
CI / build-and-test (pull_request) Successful in 1m16s
CI / valgrind (pull_request) Successful in 33s
This commit is contained in:
@@ -146,6 +146,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
|||||||
{"--backup", NULL, OPT_FLAG, offsetof(Config, backup)},
|
{"--backup", NULL, OPT_FLAG, offsetof(Config, backup)},
|
||||||
{"--stats", NULL, OPT_FLAG, offsetof(Config, stats)},
|
{"--stats", NULL, OPT_FLAG, offsetof(Config, stats)},
|
||||||
{"--partial", NULL, OPT_FLAG, offsetof(Config, partial)},
|
{"--partial", NULL, OPT_FLAG, offsetof(Config, partial)},
|
||||||
|
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
|
||||||
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
|
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
|
||||||
{"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_links)},
|
{"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_links)},
|
||||||
{"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)},
|
{"--safe-links", NULL, OPT_FLAG, offsetof(Config, safe_links)},
|
||||||
@@ -189,6 +190,8 @@ static int apply_table_option(Config* config, const OptionEntry* entry, const ch
|
|||||||
switch (entry->kind) {
|
switch (entry->kind) {
|
||||||
case OPT_FLAG:
|
case OPT_FLAG:
|
||||||
*(bool*)field = true;
|
*(bool*)field = true;
|
||||||
|
if (entry->offset == offsetof(Config, update))
|
||||||
|
config->use_metadata = true;
|
||||||
return 0;
|
return 0;
|
||||||
case OPT_STRING:
|
case OPT_STRING:
|
||||||
return set_string_option((char**)field, value, entry->name);
|
return set_string_option((char**)field, value, entry->name);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ void print_usage(void) {
|
|||||||
printf(" --max-size <n> Skip files larger than n bytes\n");
|
printf(" --max-size <n> Skip files larger than n bytes\n");
|
||||||
printf(" --min-size <n> Skip files smaller than n bytes\n");
|
printf(" --min-size <n> Skip files smaller than n bytes\n");
|
||||||
printf(" --incremental Skip files unchanged since last transfer\n");
|
printf(" --incremental Skip files unchanged since last transfer\n");
|
||||||
|
printf(" -u, --update Skip files newer than the source on receiver\n");
|
||||||
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
|
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
|
||||||
printf(" --delta-block <n> Delta block size in bytes (default: %d)\n",
|
printf(" --delta-block <n> Delta block size in bytes (default: %d)\n",
|
||||||
DELTA_BLOCK_SIZE_DEFAULT);
|
DELTA_BLOCK_SIZE_DEFAULT);
|
||||||
|
|||||||
@@ -21,6 +21,17 @@
|
|||||||
#define MAX_SERVER_DELETE_COUNT 100000U
|
#define MAX_SERVER_DELETE_COUNT 100000U
|
||||||
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE
|
#define MAX_FILE_DATA_SIZE MAX_RECEIVE_FILE_SIZE
|
||||||
|
|
||||||
|
static bool destination_is_newer(const char* path, const FileMetadata* source_metadata) {
|
||||||
|
struct stat destination_stat;
|
||||||
|
if (!source_metadata || !file_stat_secure(path, &destination_stat))
|
||||||
|
return false;
|
||||||
|
time_t destination_sec = destination_stat.st_mtim.tv_sec;
|
||||||
|
long destination_nsec = destination_stat.st_mtim.tv_nsec;
|
||||||
|
return destination_sec > source_metadata->mtime_sec ||
|
||||||
|
(destination_sec == source_metadata->mtime_sec &&
|
||||||
|
destination_nsec > source_metadata->mtime_nsec);
|
||||||
|
}
|
||||||
|
|
||||||
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
|
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
|
||||||
bool backup_enabled = config && config->backup;
|
bool backup_enabled = config && config->backup;
|
||||||
bool inplace = config && config->inplace;
|
bool inplace = config && config->inplace;
|
||||||
@@ -62,15 +73,11 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --update is receiver-side policy: never replace a newer destination. */
|
/* --update is receiver-side policy: never replace a newer destination. */
|
||||||
if (config && config->update) {
|
if (config && config->update && destination_is_newer(disk_path, file->metadata)) {
|
||||||
struct stat destination_stat;
|
free(confined_backup);
|
||||||
if (file_stat_secure(disk_path, &destination_stat) && file->metadata &&
|
free(confined_partial);
|
||||||
destination_stat.st_mtime > file->metadata->mtime_sec) {
|
free(disk_path);
|
||||||
free(confined_backup);
|
return true;
|
||||||
free(confined_partial);
|
|
||||||
free(disk_path);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (backup_enabled) {
|
if (backup_enabled) {
|
||||||
|
|||||||
@@ -214,6 +214,40 @@ class TestIncremental:
|
|||||||
assert f.read() == b"hello world\n"
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
|
|
||||||
|
class TestUpdate:
|
||||||
|
def test_update_skips_older_destination_and_allows_equal_or_newer_source(self, shared_server):
|
||||||
|
clean_dir(DEST_DIR)
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-u"], 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"newer destination\n")
|
||||||
|
os.utime(received_file, (source_stat.st_mtime + 10, source_stat.st_mtime + 10))
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-u"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0
|
||||||
|
with open(received_file, "rb") as f:
|
||||||
|
assert f.read() == b"newer destination\n"
|
||||||
|
|
||||||
|
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime))
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-u"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0
|
||||||
|
with open(received_file, "rb") as f:
|
||||||
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
|
with open(received_file, "wb") as f:
|
||||||
|
f.write(b"older destination\n")
|
||||||
|
os.utime(received_file, (source_stat.st_atime, source_stat.st_mtime - 10))
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR, flags=["-u"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0
|
||||||
|
with open(received_file, "rb") as f:
|
||||||
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
|
|
||||||
class TestDelete:
|
class TestDelete:
|
||||||
def test_delete_removes_extra_files(self, shared_server):
|
def test_delete_removes_extra_files(self, shared_server):
|
||||||
clean_dir(DEST_DIR)
|
clean_dir(DEST_DIR)
|
||||||
|
|||||||
+15
-2
@@ -282,8 +282,6 @@ static void test_parse_args_rejects_unimplemented_options() {
|
|||||||
"--list-only",
|
"--list-only",
|
||||||
"-h",
|
"-h",
|
||||||
"--human-readable",
|
"--human-readable",
|
||||||
"-u",
|
|
||||||
"--update",
|
|
||||||
"--append",
|
"--append",
|
||||||
"--append-verify",
|
"--append-verify",
|
||||||
"--delete-excluded",
|
"--delete-excluded",
|
||||||
@@ -323,6 +321,20 @@ static void test_parse_args_rejects_unimplemented_options() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_parse_args_update() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", "-u", "/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->update);
|
||||||
|
EXPECT_TRUE(cfg->use_metadata);
|
||||||
|
EXPECT_EQ_INT(positional_count, 2);
|
||||||
|
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
/* Test parse_args with --archive flag */
|
/* Test parse_args with --archive flag */
|
||||||
static void test_parse_args_archive() {
|
static void test_parse_args_archive() {
|
||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
@@ -359,5 +371,6 @@ void test_client_cli() {
|
|||||||
test_parse_args_valid_compression_level();
|
test_parse_args_valid_compression_level();
|
||||||
test_parse_args_unknown_option();
|
test_parse_args_unknown_option();
|
||||||
test_parse_args_rejects_unimplemented_options();
|
test_parse_args_rejects_unimplemented_options();
|
||||||
|
test_parse_args_update();
|
||||||
test_parse_args_archive();
|
test_parse_args_archive();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user