Add rsync-compatible ignore-existing option
CI / lint (pull_request) Successful in 12s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 36s
CI / fuzz-build (pull_request) Successful in 15s
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 12s
CI / sanitizers (address) (pull_request) Successful in 37s
CI / sanitizers (undefined) (pull_request) Successful in 36s
CI / fuzz-build (pull_request) Successful in 15s
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:
@@ -153,6 +153,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
|||||||
{"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)},
|
{"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)},
|
||||||
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
|
{"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)},
|
||||||
{"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)},
|
{"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)},
|
||||||
|
{"--ignore-existing", NULL, OPT_FLAG, offsetof(Config, ignore_existing)},
|
||||||
|
|
||||||
{"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)},
|
{"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)},
|
||||||
{"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)},
|
{"--dest-dir", NULL, OPT_STRING, offsetof(Config, receive_root_directory)},
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ void print_usage(void) {
|
|||||||
printf(" -p <port> SSH port (default: 22)\n");
|
printf(" -p <port> SSH port (default: 22)\n");
|
||||||
printf(" --progress Show transfer progress\n");
|
printf(" --progress Show transfer progress\n");
|
||||||
printf(" --delete Delete files on receiver not in source\n");
|
printf(" --delete Delete files on receiver not in source\n");
|
||||||
|
printf(" --ignore-existing Skip files that already exist on receiver\n");
|
||||||
printf(" --exclude <pattern> Exclude files matching pattern\n");
|
printf(" --exclude <pattern> Exclude files matching pattern\n");
|
||||||
printf(" --include <pattern> Only include files matching pattern\n");
|
printf(" --include <pattern> Only include files matching pattern\n");
|
||||||
printf(" --exclude-from <file> Read exclude patterns from file\n");
|
printf(" --exclude-from <file> Read exclude patterns from file\n");
|
||||||
|
|||||||
+15
-12
@@ -68,6 +68,7 @@ static void config_set_defaults(Config* config) {
|
|||||||
config->debug_level = 0;
|
config->debug_level = 0;
|
||||||
config->list_only = false;
|
config->list_only = false;
|
||||||
config->human_readable = false;
|
config->human_readable = false;
|
||||||
|
config->ignore_existing = false;
|
||||||
config->update = false;
|
config->update = false;
|
||||||
config->inplace = false;
|
config->inplace = false;
|
||||||
config->append = false;
|
config->append = false;
|
||||||
@@ -123,12 +124,13 @@ static bool validate_received_config(const Config* config) {
|
|||||||
valid_wire_bool(config->copy_unsafe_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_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_xattrs) && valid_wire_bool(config->preserve_devices) &&
|
||||||
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->update) &&
|
valid_wire_bool(config->preserve_sparse) && valid_wire_bool(config->ignore_existing) &&
|
||||||
valid_wire_bool(config->inplace) && valid_wire_bool(config->append) &&
|
valid_wire_bool(config->update) && valid_wire_bool(config->inplace) &&
|
||||||
valid_wire_bool(config->append_verify) && valid_wire_bool(config->delete_excluded) &&
|
valid_wire_bool(config->append) && valid_wire_bool(config->append_verify) &&
|
||||||
valid_wire_bool(config->delete_after) && valid_wire_bool(config->relative) &&
|
valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->delete_after) &&
|
||||||
valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->partial) &&
|
valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) &&
|
||||||
valid_wire_bool(config->delete_before) && valid_wire_bool(config->checksum) &&
|
valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) &&
|
||||||
|
valid_wire_bool(config->checksum) &&
|
||||||
(!config->use_compression ||
|
(!config->use_compression ||
|
||||||
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
(config->compression_level >= 1 && config->compression_level <= 22)) &&
|
||||||
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE &&
|
||||||
@@ -243,10 +245,11 @@ static bool send_file_options(int fd, const Config* c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool send_selection_options(int fd, const Config* c) {
|
static bool send_selection_options(int fd, const Config* c) {
|
||||||
return send_int(fd, c->update) && send_int(fd, c->inplace) && send_int(fd, c->append) &&
|
return send_int(fd, c->ignore_existing) && send_int(fd, c->update) && send_int(fd, c->inplace) &&
|
||||||
send_int(fd, c->append_verify) && send_int(fd, c->delete_excluded) &&
|
send_int(fd, c->append) && send_int(fd, c->append_verify) &&
|
||||||
send_int(fd, c->delete_after) && send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) &&
|
send_int(fd, c->delete_excluded) && send_int(fd, c->delete_after) &&
|
||||||
send_int(fd, c->relative) && send_int(fd, c->prune_empty_dirs);
|
send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) && send_int(fd, c->relative) &&
|
||||||
|
send_int(fd, c->prune_empty_dirs);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool send_resume_options(int fd, const Config* c) {
|
static bool send_resume_options(int fd, const Config* c) {
|
||||||
@@ -304,8 +307,8 @@ static bool receive_file_options(int fd, Config* c) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static bool receive_selection_options(int fd, Config* c) {
|
static bool receive_selection_options(int fd, Config* c) {
|
||||||
bool* flags[] = {&c->update, &c->inplace, &c->append,
|
bool* flags[] = {&c->ignore_existing, &c->update, &c->inplace, &c->append,
|
||||||
&c->append_verify, &c->delete_excluded, &c->delete_after};
|
&c->append_verify, &c->delete_excluded, &c->delete_after};
|
||||||
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) {
|
||||||
if (!receive_wire_bool(fd, flags[i]))
|
if (!receive_wire_bool(fd, flags[i]))
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ typedef struct Config {
|
|||||||
bool human_readable;
|
bool human_readable;
|
||||||
|
|
||||||
// Issue #127: Transfer modes
|
// Issue #127: Transfer modes
|
||||||
|
bool ignore_existing;
|
||||||
bool update;
|
bool update;
|
||||||
bool inplace;
|
bool inplace;
|
||||||
bool append;
|
bool append;
|
||||||
|
|||||||
@@ -61,6 +61,21 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --ignore-existing checks the final destination before partial files or
|
||||||
|
overwrite policies can modify it. */
|
||||||
|
if (config && config->ignore_existing) {
|
||||||
|
struct stat destination_stat;
|
||||||
|
char* final_path = path_cat(root_directory, file->path);
|
||||||
|
bool exists = final_path && file_stat_secure(final_path, &destination_stat);
|
||||||
|
free(final_path);
|
||||||
|
if (exists) {
|
||||||
|
free(confined_backup);
|
||||||
|
free(confined_partial);
|
||||||
|
free(disk_path);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* --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) {
|
||||||
struct stat destination_stat;
|
struct stat destination_stat;
|
||||||
|
|||||||
@@ -214,6 +214,29 @@ class TestIncremental:
|
|||||||
assert f.read() == b"hello world\n"
|
assert f.read() == b"hello world\n"
|
||||||
|
|
||||||
|
|
||||||
|
class TestIgnoreExisting:
|
||||||
|
def test_ignore_existing_preserves_existing_and_transfers_new(self, shared_server):
|
||||||
|
clean_dir(DEST_DIR)
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR, port=shared_server.port)
|
||||||
|
assert result.returncode == 0
|
||||||
|
|
||||||
|
received = get_dest_received_dir(DEST_DIR, SOURCE_DIR)
|
||||||
|
existing_file = os.path.join(received, "small.txt")
|
||||||
|
with open(existing_file, "wb") as f:
|
||||||
|
f.write(b"destination content\n")
|
||||||
|
new_source = os.path.join(SOURCE_DIR, "new.txt")
|
||||||
|
with open(new_source, "wb") as f:
|
||||||
|
f.write(b"new file\n")
|
||||||
|
|
||||||
|
result, _ = run_client(SOURCE_DIR, DEST_DIR,
|
||||||
|
flags=["--ignore-existing"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, f"Sync failed: {(result.stderr or result.stdout)[:200]}"
|
||||||
|
with open(existing_file, "rb") as f:
|
||||||
|
assert f.read() == b"destination content\n"
|
||||||
|
with open(os.path.join(received, "new.txt"), "rb") as f:
|
||||||
|
assert f.read() == b"new file\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)
|
||||||
|
|||||||
@@ -181,6 +181,19 @@ static void test_parse_args_valid_port() {
|
|||||||
config_delete(cfg);
|
config_delete(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_parse_args_ignore_existing() {
|
||||||
|
Config* cfg = config_create();
|
||||||
|
char* argv[] = {"fastsync", "--ignore-existing", "/src", "/dst"};
|
||||||
|
int positional_args[2];
|
||||||
|
int positional_count = 0;
|
||||||
|
|
||||||
|
int ret = parse_args(cfg, 4, argv, positional_args, &positional_count);
|
||||||
|
EXPECT_EQ_INT(ret, 0);
|
||||||
|
EXPECT_TRUE(cfg->ignore_existing);
|
||||||
|
|
||||||
|
config_delete(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
/* Test parse_args rejects port > 65535 */
|
/* Test parse_args rejects port > 65535 */
|
||||||
static void test_parse_args_invalid_port() {
|
static void test_parse_args_invalid_port() {
|
||||||
Config* cfg = config_create();
|
Config* cfg = config_create();
|
||||||
@@ -352,6 +365,7 @@ void test_client_cli() {
|
|||||||
test_parse_args_help();
|
test_parse_args_help();
|
||||||
test_parse_args_version();
|
test_parse_args_version();
|
||||||
test_parse_args_valid_port();
|
test_parse_args_valid_port();
|
||||||
|
test_parse_args_ignore_existing();
|
||||||
test_parse_args_invalid_port();
|
test_parse_args_invalid_port();
|
||||||
test_parse_args_non_numeric_port();
|
test_parse_args_non_numeric_port();
|
||||||
test_parse_args_invalid_server_port();
|
test_parse_args_invalid_server_port();
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ static void test_config_send_receive() {
|
|||||||
send_cfg->use_metadata = true;
|
send_cfg->use_metadata = true;
|
||||||
send_cfg->compression_level = 5;
|
send_cfg->compression_level = 5;
|
||||||
send_cfg->chunk_size = 1024;
|
send_cfg->chunk_size = 1024;
|
||||||
|
send_cfg->ignore_existing = true;
|
||||||
|
|
||||||
/* Use socketpair for bidirectional communication */
|
/* Use socketpair for bidirectional communication */
|
||||||
int p[2];
|
int p[2];
|
||||||
@@ -160,6 +161,8 @@ static void test_config_send_receive() {
|
|||||||
ok = false;
|
ok = false;
|
||||||
if (recv_cfg->chunk_size != 1024)
|
if (recv_cfg->chunk_size != 1024)
|
||||||
ok = false;
|
ok = false;
|
||||||
|
if (!recv_cfg->ignore_existing)
|
||||||
|
ok = false;
|
||||||
}
|
}
|
||||||
config_delete(recv_cfg);
|
config_delete(recv_cfg);
|
||||||
close(p[0]);
|
close(p[0]);
|
||||||
|
|||||||
@@ -88,6 +88,38 @@ static void test_file_save_to_disk() {
|
|||||||
rmdir("test_save_tmp");
|
rmdir("test_save_tmp");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_file_save_to_disk_ignore_existing() {
|
||||||
|
const char* path = "test_ignore_existing_tmp/existing.txt";
|
||||||
|
EXPECT_TRUE(file_write_to_disk(path, "old", 3, false, false));
|
||||||
|
|
||||||
|
File* file = file_create("existing.txt");
|
||||||
|
EXPECT_NOT_NULL(file);
|
||||||
|
file->data->data = malloc(3);
|
||||||
|
EXPECT_NOT_NULL(file->data->data);
|
||||||
|
memcpy(file->data->data, "new", 3);
|
||||||
|
file->data->size = 3;
|
||||||
|
|
||||||
|
Config* config = config_create();
|
||||||
|
EXPECT_NOT_NULL(config);
|
||||||
|
config->ignore_existing = true;
|
||||||
|
EXPECT_TRUE(file_save_to_disk("test_ignore_existing_tmp", file, config));
|
||||||
|
|
||||||
|
FILE* stream = fopen(path, "rb");
|
||||||
|
char content[4] = {0};
|
||||||
|
EXPECT_NOT_NULL(stream);
|
||||||
|
// cppcheck-suppress knownConditionTrueFalse
|
||||||
|
if (stream) {
|
||||||
|
EXPECT_EQ_INT((int)fread(content, 1, 3, stream), 3);
|
||||||
|
fclose(stream);
|
||||||
|
}
|
||||||
|
EXPECT_EQ_STR(content, "old");
|
||||||
|
|
||||||
|
file_destroy(file);
|
||||||
|
config_delete(config);
|
||||||
|
unlink(path);
|
||||||
|
rmdir("test_ignore_existing_tmp");
|
||||||
|
}
|
||||||
|
|
||||||
static void test_file_write_to_disk_basic() {
|
static void test_file_write_to_disk_basic() {
|
||||||
const char* content = "Basic file_write_to_disk test";
|
const char* content = "Basic file_write_to_disk test";
|
||||||
EXPECT_TRUE(file_write_to_disk("test_file_write_to_disk_basic.txt", content, strlen(content),
|
EXPECT_TRUE(file_write_to_disk("test_file_write_to_disk_basic.txt", content, strlen(content),
|
||||||
@@ -458,6 +490,7 @@ void test_file() {
|
|||||||
test_file_load_data();
|
test_file_load_data();
|
||||||
test_file_load_data_missing_file();
|
test_file_load_data_missing_file();
|
||||||
test_file_save_to_disk();
|
test_file_save_to_disk();
|
||||||
|
test_file_save_to_disk_ignore_existing();
|
||||||
test_file_write_to_disk_basic();
|
test_file_write_to_disk_basic();
|
||||||
test_file_write_to_disk_creates_dirs();
|
test_file_write_to_disk_creates_dirs();
|
||||||
test_file_write_to_disk_does_not_follow_symlink();
|
test_file_write_to_disk_does_not_follow_symlink();
|
||||||
|
|||||||
Reference in New Issue
Block a user