Merge feat/p3-basis-dest: alternate basis dirs (--compare-dest/--copy-dest/--link-dest)
This commit is contained in:
@@ -114,6 +114,27 @@ static int set_nonneg_int_option(int* dest, const char* value, const char* optio
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Validate and append one --compare-dest/--copy-dest/--link-dest directory.
|
||||
* The path is interpreted on the receiver relative to the destination root,
|
||||
* so it must be a non-empty relative path with no "." / ".." components (an
|
||||
* absolute or escaping path is rejected up front instead of failing on the
|
||||
* server). Returns 0 on success, -1 on error. */
|
||||
static int set_basis_dest_option(Config* config, BasisDestType type, const char* value,
|
||||
const char* option_name) {
|
||||
if (!value || !value[0]) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", option_name);
|
||||
return -1;
|
||||
}
|
||||
if (config_basis_append(config, type, value) != 0) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%s requires a non-empty relative directory name with no '.', '..', or absolute "
|
||||
"path (resolved below the destination root)",
|
||||
option_name);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int set_stderr_mode(const char* value) {
|
||||
if (strcmp(value, "errors") == 0 || strcmp(value, "e") == 0)
|
||||
log_set_stderr_mode(LOG_STDERR_ERRORS);
|
||||
@@ -974,6 +995,36 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
}
|
||||
log_message(LOG_LEVEL_ERROR, "%s is not supported yet (xxHash64 is used)", argv[i]);
|
||||
return -1;
|
||||
} else if (strncmp(argv[i], "--compare-dest=", 15) == 0) {
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COMPARE, argv[i] + 15, "--compare-dest") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--compare-dest", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COMPARE, argv[++i], "--compare-dest") != 0)
|
||||
return -1;
|
||||
} else if (strncmp(argv[i], "--copy-dest=", 12) == 0) {
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COPY, argv[i] + 12, "--copy-dest") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--copy-dest", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_basis_dest_option(config, BASIS_DEST_COPY, argv[++i], "--copy-dest") != 0)
|
||||
return -1;
|
||||
} else if (strncmp(argv[i], "--link-dest=", 12) == 0) {
|
||||
if (set_basis_dest_option(config, BASIS_DEST_LINK, argv[i] + 12, "--link-dest") != 0)
|
||||
return -1;
|
||||
} else if (opt_is(argv[i], "--link-dest", NULL)) {
|
||||
if (i + 1 >= argc) {
|
||||
log_message(LOG_LEVEL_ERROR, "missing argument for %s", argv[i]);
|
||||
return -1;
|
||||
}
|
||||
if (set_basis_dest_option(config, BASIS_DEST_LINK, argv[++i], "--link-dest") != 0)
|
||||
return -1;
|
||||
} else if (argv[i][0] == '-') {
|
||||
char* escaped = output_escape(argv[i], false);
|
||||
fprintf(stderr, "Unknown option: %s\n", escaped ? escaped : "<allocation failed>");
|
||||
@@ -1010,6 +1061,13 @@ int parse_args(Config* config, int argc, char* argv[], int* positional_args,
|
||||
config->files_from_set = set;
|
||||
}
|
||||
|
||||
/* The "unchanged" decision for --compare-dest/--copy-dest/--link-dest must
|
||||
* be made on the receiver against the basis directories, which requires the
|
||||
* per-file STATUS_CHECK handshake: basis-dir options therefore imply
|
||||
* --incremental (and, via the block below, metadata) on the sender. */
|
||||
if (config_has_basis(config))
|
||||
config->use_incremental = true;
|
||||
|
||||
/* Incremental and delta transfers need metadata unless the user disabled it. */
|
||||
if ((config->use_incremental || config->use_delta) && !config->use_metadata &&
|
||||
!config->metadata_explicitly_disabled) {
|
||||
|
||||
@@ -218,6 +218,49 @@ static bool files_from_list_valid(const Config* config) {
|
||||
return no_implied_dirs_files_from_valid(config);
|
||||
}
|
||||
|
||||
/* Basis directories are honored by the receiver's per-file incremental check,
|
||||
which (like every whole-file payload path in FastSync) is bounded by
|
||||
MAX_RECEIVE_WHOLE_FILE_SIZE. rsync would apply basis dirs to files of any
|
||||
size; FastSync cannot, so when basis dirs are requested this preflight scan
|
||||
refuses the run up front with a clear diagnostic instead of letting the
|
||||
receiver abort the whole transfer mid-stream with no client explanation.
|
||||
Returns true when the tree can be transferred. */
|
||||
static bool basis_oversize_preflight(const Config* config) {
|
||||
PreparedScanner prepared;
|
||||
if (!prepare_scanner(config, 0, &prepared))
|
||||
return false;
|
||||
DirectoryScanner* scanner =
|
||||
directory_scanner_create_with_options(config->send_directory, &prepared.options);
|
||||
prepared_scanner_destroy(&prepared);
|
||||
if (!scanner)
|
||||
return false;
|
||||
bool ok = true;
|
||||
Chunk* chunk;
|
||||
while ((chunk = directory_scanner_next(scanner)) != NULL) {
|
||||
for (int i = 0; i < chunk->element_count; i++) {
|
||||
File* f = chunk->items[i];
|
||||
if (f == NULL || f->is_dir || f->data == NULL || f->data->size <= MAX_RECEIVE_WHOLE_FILE_SIZE)
|
||||
continue;
|
||||
char* escaped = output_escape(file_wire_path(f), config->eight_bit_output);
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"%s is %llu bytes, larger than the %llu-byte whole-file transfer limit; "
|
||||
"--compare-dest/--copy-dest/--link-dest cannot sync files above this limit",
|
||||
escaped ? escaped : "<allocation failed>", (unsigned long long)f->data->size,
|
||||
(unsigned long long)MAX_RECEIVE_WHOLE_FILE_SIZE);
|
||||
free(escaped);
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
chunk_destroy(chunk);
|
||||
if (!ok)
|
||||
break;
|
||||
}
|
||||
if (directory_scanner_failed(scanner))
|
||||
ok = false;
|
||||
directory_scanner_destroy(scanner);
|
||||
return ok;
|
||||
}
|
||||
|
||||
/* Select the configured transport for both transfer execution paths. */
|
||||
static Client* connect_transfer_client(const Config* config) {
|
||||
if (config->transport == TRANSPORT_SSH) {
|
||||
@@ -662,7 +705,10 @@ static int incremental_check(Client* client, File* file, const Config* config,
|
||||
return -1;
|
||||
if (!send_n_data(client->file_descriptor, &mtime_nsec, sizeof(mtime_nsec)))
|
||||
return -1;
|
||||
if (config->checksum) {
|
||||
/* With alternate basis directories the receiver must be able to verify the
|
||||
* content of every candidate basis file, so the sender supplies its xxHash64
|
||||
* for every file even when --checksum was not requested. */
|
||||
if (config->checksum || config_has_basis(config)) {
|
||||
uint64_t checksum;
|
||||
if (!file_checksum(file, &checksum) ||
|
||||
!send_n_data(client->file_descriptor, &checksum, sizeof(checksum)))
|
||||
@@ -1216,6 +1262,8 @@ int send_files(Config* config) {
|
||||
return send_dry_run_manifest(config);
|
||||
if (!files_from_list_valid(config))
|
||||
return 1;
|
||||
if (config_has_basis(config) && !basis_oversize_preflight(config))
|
||||
return 1;
|
||||
|
||||
Client* client = connect_transfer_client(config);
|
||||
if (!client) {
|
||||
@@ -1389,6 +1437,8 @@ int send_files_multithreaded(Config** config_ptr) {
|
||||
return send_dry_run_manifest(config);
|
||||
if (!files_from_list_valid(config))
|
||||
return 1;
|
||||
if (config_has_basis(config) && !basis_oversize_preflight(config))
|
||||
return 1;
|
||||
|
||||
long pages = sysconf(_SC_AVPHYS_PAGES);
|
||||
long page_size = sysconf(_SC_PAGE_SIZE);
|
||||
|
||||
@@ -11,6 +11,12 @@ bool validate_config(const Config* config) {
|
||||
print_usage();
|
||||
return false;
|
||||
}
|
||||
if (config_has_basis(config) && config->use_chunk_serialization) {
|
||||
log_message(LOG_LEVEL_ERROR,
|
||||
"--compare-dest/--copy-dest/--link-dest require per-file incremental checks and "
|
||||
"cannot be combined with -s (chunk serialization)");
|
||||
return false;
|
||||
}
|
||||
if (config->use_sendfile && (config->use_chunk_serialization || config->use_compression)) {
|
||||
log_message(LOG_LEVEL_ERROR, "-f/--sendfile cannot be combined with -c (compression) or -s "
|
||||
"(chunk serialization)");
|
||||
|
||||
@@ -70,6 +70,13 @@ void print_usage(void) {
|
||||
printf(" -@, --modify-window <sec> Modification time tolerance\n");
|
||||
printf(" -u, --update Skip files newer than the source on receiver\n");
|
||||
printf(" --existing Skip files not already present at destination\n");
|
||||
printf(" --compare-dest <dir> Treat DIR (relative to destination root) as an extra\n");
|
||||
printf(" comparison basis: unchanged files are not transferred\n");
|
||||
printf(" (requires --incremental, which is implied)\n");
|
||||
printf(" --copy-dest <dir> Like --compare-dest, but copies the unchanged file from DIR\n");
|
||||
printf(" into the destination instead of transferring its data\n");
|
||||
printf(" --link-dest <dir> Like --copy-dest, but hard-links the unchanged file from DIR\n");
|
||||
printf(" into the destination (repeatable; earlier DIRs win)\n");
|
||||
printf(" --checksum-choice, --cc <alg> Checksum algorithm (not supported yet; xxHash64 is "
|
||||
"used)\n");
|
||||
printf(" --delta Delta transfer for changed files (requires --incremental)\n");
|
||||
|
||||
Reference in New Issue
Block a user