diff --git a/RSYNC_COMPAT.md b/RSYNC_COMPAT.md index 4b236ec..f45dbfb 100644 --- a/RSYNC_COMPAT.md +++ b/RSYNC_COMPAT.md @@ -229,7 +229,7 @@ This document maps rsync's full feature set to FastSync's current implementation |------|-------------------|-----------------|-------| | `--stop-after=MINS` | Stop after N minutes | ❌ Not Implemented | | | `--stop-at=TIME` | Stop at specified time | ❌ Not Implemented | | -| `--fsync` | Fsync every written file | ❌ Not Implemented | | +| `--fsync` | Fsync every written file before publication | ✅ Implemented | | | `--protocol=NUM` | Force older protocol version | ❌ Not Implemented | | | `--iconv=CONVERT_SPEC` | Charset conversion | ❌ Not Implemented | | | `--checksum-seed=NUM` | Set checksum seed | ❌ Not Implemented | | diff --git a/src/client/client_cli.c b/src/client/client_cli.c index 191cc07..f77a808 100644 --- a/src/client/client_cli.c +++ b/src/client/client_cli.c @@ -152,6 +152,7 @@ static const OptionEntry OPTION_TABLE[] = { {"--copy-unsafe-links", NULL, OPT_FLAG, offsetof(Config, copy_unsafe_links)}, {"--sparse", "-S", OPT_FLAG, offsetof(Config, preserve_sparse)}, {"--inplace", NULL, OPT_FLAG, offsetof(Config, inplace)}, + {"--fsync", NULL, OPT_FLAG, offsetof(Config, use_fsync)}, {"--checksum", NULL, OPT_FLAG, offsetof(Config, checksum)}, {"--source-dir", NULL, OPT_STRING, offsetof(Config, send_directory)}, diff --git a/src/client/usage.c b/src/client/usage.c index 2f800af..2fc5c74 100644 --- a/src/client/usage.c +++ b/src/client/usage.c @@ -68,6 +68,7 @@ void print_usage(void) { 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(" --fsync Fsync every written file before publication\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/config.c b/src/shared/config.c index efc2708..419cf45 100644 --- a/src/shared/config.c +++ b/src/shared/config.c @@ -70,6 +70,7 @@ static void config_set_defaults(Config* config) { config->human_readable = false; config->update = false; config->inplace = false; + config->use_fsync = false; config->append = false; config->append_verify = false; config->delete_excluded = false; @@ -125,10 +126,11 @@ static bool validate_received_config(const Config* config) { 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->inplace) && valid_wire_bool(config->append) && - valid_wire_bool(config->append_verify) && valid_wire_bool(config->delete_excluded) && - valid_wire_bool(config->delete_after) && valid_wire_bool(config->relative) && - valid_wire_bool(config->prune_empty_dirs) && valid_wire_bool(config->partial) && - valid_wire_bool(config->delete_before) && valid_wire_bool(config->checksum) && + valid_wire_bool(config->use_fsync) && valid_wire_bool(config->append_verify) && + valid_wire_bool(config->delete_excluded) && valid_wire_bool(config->delete_after) && + valid_wire_bool(config->relative) && valid_wire_bool(config->prune_empty_dirs) && + valid_wire_bool(config->partial) && valid_wire_bool(config->delete_before) && + valid_wire_bool(config->checksum) && (!config->use_compression || (config->compression_level >= 1 && config->compression_level <= 22)) && config->chunk_size > 0 && config->chunk_size <= MAX_CHUNK_SIZE && @@ -244,9 +246,10 @@ static bool send_file_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) && - send_int(fd, c->append_verify) && send_int(fd, c->delete_excluded) && - send_int(fd, c->delete_after) && send_n_data(fd, &c->max_delete, sizeof(c->max_delete)) && - send_int(fd, c->relative) && send_int(fd, c->prune_empty_dirs); + send_int(fd, c->use_fsync) && send_int(fd, c->append_verify) && + send_int(fd, c->delete_excluded) && send_int(fd, c->delete_after) && + 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) { @@ -304,7 +307,7 @@ static bool receive_file_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->update, &c->inplace, &c->append, &c->use_fsync, &c->append_verify, &c->delete_excluded, &c->delete_after}; for (size_t i = 0; i < sizeof(flags) / sizeof(flags[0]); i++) { if (!receive_wire_bool(fd, flags[i])) diff --git a/src/shared/config.h b/src/shared/config.h index 4a218b0..3d3244c 100644 --- a/src/shared/config.h +++ b/src/shared/config.h @@ -78,6 +78,7 @@ typedef struct Config { // Issue #127: Transfer modes bool update; bool inplace; + bool use_fsync; bool append; bool append_verify; @@ -128,7 +129,7 @@ typedef struct Config { char* compress_choice; } Config; -#define PROTOCOL_VERSION "2.2.0" +#define PROTOCOL_VERSION "2.3.0" #define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024) Config* config_create(void); diff --git a/src/shared/file.c b/src/shared/file.c index 9514e87..f96bcab 100644 --- a/src/shared/file.c +++ b/src/shared/file.c @@ -302,8 +302,9 @@ bool file_rename_secure(const char* old_path, const char* new_path) { return ok; } -bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, - bool inplace, bool sparse, const FileMetadata* metadata) { +bool file_to_disk_secure_with_fsync(const char* path, const void* data, + unsigned long long data_size, bool inplace, bool sparse, + const FileMetadata* metadata, bool use_fsync) { char* leaf = NULL; int dirfd = file_open_secure_parent(path, &leaf, true); if (dirfd < 0) @@ -317,6 +318,8 @@ bool file_to_disk_secure(const char* path, const void* data, unsigned long long ok = write_all(fd, data, data_size); if (ok && metadata) ok = file_restore_metadata_fd(fd, metadata); + if (ok && use_fsync) + ok = fsync(fd) == 0; } } else { char tmp[NAME_MAX]; @@ -331,6 +334,8 @@ bool file_to_disk_secure(const char* path, const void* data, unsigned long long ok = write_all(fd, data, data_size); if (ok && metadata) ok = file_restore_metadata_fd(fd, metadata); + if (ok && use_fsync) + ok = fsync(fd) == 0; if (close(fd) != 0) ok = false; fd = -1; @@ -347,6 +352,11 @@ bool file_to_disk_secure(const char* path, const void* data, unsigned long long return ok; } +bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, + bool inplace, bool sparse, const FileMetadata* metadata) { + return file_to_disk_secure_with_fsync(path, data, data_size, inplace, sparse, metadata, false); +} + bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse) { if (!path || (!data && data_size != 0) || has_path_traversal(path)) diff --git a/src/shared/file.h b/src/shared/file.h index 23dac26..7a67203 100644 --- a/src/shared/file.h +++ b/src/shared/file.h @@ -32,5 +32,8 @@ bool file_ensure_directory_secure(const char* path); bool file_rename_secure(const char* old_path, const char* new_path); bool file_to_disk_secure(const char* path, const void* data, unsigned long long data_size, bool inplace, bool sparse, const FileMetadata* metadata); +bool file_to_disk_secure_with_fsync(const char* path, const void* data, + unsigned long long data_size, bool inplace, bool sparse, + const FileMetadata* metadata, bool use_fsync); #endif diff --git a/src/shared/file_receive.c b/src/shared/file_receive.c index a610d67..ad44647 100644 --- a/src/shared/file_receive.c +++ b/src/shared/file_receive.c @@ -103,8 +103,8 @@ bool file_save_to_disk(const char* root_directory, const File* file, const Confi } } - bool ok = file_to_disk_secure(disk_path, file->data->data, file->data->size, inplace, sparse, - file->metadata); + bool ok = file_to_disk_secure_with_fsync(disk_path, file->data->data, file->data->size, inplace, + sparse, file->metadata, config && config->use_fsync); free(parent_copy); free(backup_path); free(confined_backup); diff --git a/tests/test_client_cli.c b/tests/test_client_cli.c index 656fd40..1eba48b 100644 --- a/tests/test_client_cli.c +++ b/tests/test_client_cli.c @@ -339,6 +339,17 @@ static void test_parse_args_archive() { config_delete(cfg); } +static void test_parse_args_fsync() { + Config* cfg = config_create(); + char* argv[] = {"fastsync", "--fsync", "/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->use_fsync); + config_delete(cfg); +} + void test_client_cli() { test_validate_config_required_paths(); test_validate_config_incompatible_options(); @@ -360,4 +371,5 @@ void test_client_cli() { test_parse_args_unknown_option(); test_parse_args_rejects_unimplemented_options(); test_parse_args_archive(); + test_parse_args_fsync(); } diff --git a/tests/test_config.c b/tests/test_config.c index 4f124db..6533706 100644 --- a/tests/test_config.c +++ b/tests/test_config.c @@ -124,6 +124,7 @@ static void test_config_send_receive() { send_cfg->use_chunk_serialization = true; send_cfg->use_compression = true; send_cfg->use_metadata = true; + send_cfg->use_fsync = true; send_cfg->compression_level = 5; send_cfg->chunk_size = 1024; @@ -160,6 +161,8 @@ static void test_config_send_receive() { ok = false; if (recv_cfg->chunk_size != 1024) ok = false; + if (!recv_cfg->use_fsync) + ok = false; } config_delete(recv_cfg); close(p[0]); diff --git a/tests/test_file.c b/tests/test_file.c index 6b8469e..a0fa1f7 100644 --- a/tests/test_file.c +++ b/tests/test_file.c @@ -108,6 +108,17 @@ static void test_file_write_to_disk_basic() { unlink("test_file_write_to_disk_basic.txt"); } +static void test_file_write_to_disk_with_fsync() { + const char* path = "test_file_write_to_disk_fsync.txt"; + const char* content = "fsync file content"; + EXPECT_TRUE( + file_to_disk_secure_with_fsync(path, content, strlen(content), false, false, NULL, true)); + struct stat st; + EXPECT_EQ_INT(stat(path, &st), 0); + EXPECT_EQ_INT((int)st.st_size, (int)strlen(content)); + unlink(path); +} + static void test_file_write_to_disk_creates_dirs() { const char* content = "Nested dir test"; EXPECT_TRUE(file_write_to_disk("test_nested_tmp/nested/file.txt", content, strlen(content), false, @@ -459,6 +470,7 @@ void test_file() { test_file_load_data_missing_file(); test_file_save_to_disk(); test_file_write_to_disk_basic(); + test_file_write_to_disk_with_fsync(); test_file_write_to_disk_creates_dirs(); test_file_write_to_disk_does_not_follow_symlink(); test_file_content_to_buffer();