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

This commit is contained in:
2026-09-03 17:07:02 +02:00
parent 190fc5d300
commit 5e67a721a4
5 changed files with 69 additions and 11 deletions
+3
View File
@@ -146,6 +146,7 @@ static const OptionEntry OPTION_TABLE[] = {
{"--backup", NULL, OPT_FLAG, offsetof(Config, backup)},
{"--stats", NULL, OPT_FLAG, offsetof(Config, stats)},
{"--partial", NULL, OPT_FLAG, offsetof(Config, partial)},
{"--update", "-u", OPT_FLAG, offsetof(Config, update)},
{"--links", "-l", OPT_FLAG, offsetof(Config, follow_symlinks)},
{"--copy-links", NULL, OPT_FLAG, offsetof(Config, copy_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) {
case OPT_FLAG:
*(bool*)field = true;
if (entry->offset == offsetof(Config, update))
config->use_metadata = true;
return 0;
case OPT_STRING:
return set_string_option((char**)field, value, entry->name);
+1
View File
@@ -28,6 +28,7 @@ void print_usage(void) {
printf(" --max-size <n> Skip files larger 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(" -u, --update Skip files newer than the source on receiver\n");
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
printf(" --delta-block <n> Delta block size in bytes (default: %d)\n",
DELTA_BLOCK_SIZE_DEFAULT);
+16 -9
View File
@@ -21,6 +21,17 @@
#define MAX_SERVER_DELETE_COUNT 100000U
#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 backup_enabled = config && config->backup;
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. */
if (config && config->update) {
struct stat destination_stat;
if (file_stat_secure(disk_path, &destination_stat) && file->metadata &&
destination_stat.st_mtime > file->metadata->mtime_sec) {
free(confined_backup);
free(confined_partial);
free(disk_path);
return true;
}
if (config && config->update && destination_is_newer(disk_path, file->metadata)) {
free(confined_backup);
free(confined_partial);
free(disk_path);
return true;
}
if (backup_enabled) {