feat: per-attribute preservation flags -p/-t/-o/-g with --no-* negations (protocol 2.22.0)
Split FastSync's single use_metadata bundle into four independent rsync-parity attributes: preserve_perms, preserve_times, preserve_owner, preserve_group. use_metadata is now a derived transport bit (config_derived_use_metadata). CLI: real -p/--perms, -t/--times, -o/--owner, -g/--group plus --no-perms/--no-times/--no-owner/--no-group (short and long) and --no-preserve; -a is now rsync -rlptgoD; --preserve = -pt; -A implies -p; -X does not; --chmod implies -p; --usermap/--groupmap/--chown imply owner/group per side; --incremental/--delta still auto-preserve unless negated. Receiver: per-attribute FileAttrPolicy gating for files, dirs (modes applied at end of transfer), symlinks and specials; rsync -E read-bit rule; new files get source_mode & ~umask sanitized (no group/other write); per-side identity resolution; deferred directory metadata; batch dir-metadata replay; daemon modules without 'client owner = yes' no longer refuse plain -a but force super off (no ownership) with a warning. Wire: PROTOCOL_VERSION 2.21.0 -> 2.22.0 (four appended config bools, golden 653 / 95530566005420798). FileMetadata/chunk/batch framing unchanged. Docs/CHANGELOG/CMake updated to 2.22.0.
This commit is contained in:
+104
-54
@@ -619,6 +619,11 @@ typedef struct {
|
||||
size_t offset; /* offsetof of the boolean target field in Config */
|
||||
} NegatableOption;
|
||||
|
||||
/* Sentinel offset for --no-preserve, the rsync drop-in negation of the whole
|
||||
* preservation bundle: it clears all four per-attribute flags and records the
|
||||
* explicit metadata opt-out instead of clearing a single Config field. */
|
||||
#define NEGATABLE_PRESERVE_BUNDLE ((size_t) - 1)
|
||||
|
||||
/* Options that map directly onto a Config field with no side effects.
|
||||
*
|
||||
* NOTE: these CLI tables are intentionally NOT generated from the wire-field
|
||||
@@ -796,7 +801,11 @@ static const NegatableOption NEGATABLE_OPTIONS[] = {
|
||||
{"compress", NULL, offsetof(Config, use_compression)},
|
||||
{"compress", "z", offsetof(Config, use_compression)},
|
||||
{"multithreading", "j", offsetof(Config, use_multithreading)},
|
||||
{"preserve", NULL, offsetof(Config, use_metadata)},
|
||||
{"preserve", NULL, NEGATABLE_PRESERVE_BUNDLE},
|
||||
{"perms", "p", offsetof(Config, preserve_perms)},
|
||||
{"times", "t", offsetof(Config, preserve_times)},
|
||||
{"owner", "o", offsetof(Config, preserve_owner)},
|
||||
{"group", "g", offsetof(Config, preserve_group)},
|
||||
{"sendfile", NULL, offsetof(Config, use_sendfile)},
|
||||
{"chunk-serialization", NULL, offsetof(Config, use_chunk_serialization)},
|
||||
{"xattrs", "X", offsetof(Config, preserve_xattrs)},
|
||||
@@ -856,9 +865,27 @@ static int apply_negation(Config* config, const char* arg) {
|
||||
fprintf(stderr, "Cannot negate unsupported or unsafe option: %s\n", arg);
|
||||
return -1;
|
||||
}
|
||||
*(bool*)((char*)config + entry->offset) = false;
|
||||
if (entry->offset == offsetof(Config, use_metadata))
|
||||
if (entry->offset == NEGATABLE_PRESERVE_BUNDLE) {
|
||||
config->preserve_perms = false;
|
||||
config->preserve_times = false;
|
||||
config->preserve_owner = false;
|
||||
config->preserve_group = false;
|
||||
config->metadata_explicitly_disabled = true;
|
||||
/* --no-preserve is an explicit opt-out of the whole bundle: record it so
|
||||
* the --incremental/--delta auto-preserve in cli_finalize_config does not
|
||||
* silently re-enable perms/times. */
|
||||
config->preserve_perms_explicit_off = true;
|
||||
config->preserve_times_explicit_off = true;
|
||||
return 0;
|
||||
}
|
||||
*(bool*)((char*)config + entry->offset) = false;
|
||||
/* Track an explicit per-attribute negation so --incremental/--delta can
|
||||
* auto-preserve the OTHER attribute without undoing this one. A later
|
||||
* -p/-t sets the attribute directly; this flag only gates the implication. */
|
||||
if (entry->offset == offsetof(Config, preserve_perms))
|
||||
config->preserve_perms_explicit_off = true;
|
||||
else if (entry->offset == offsetof(Config, preserve_times))
|
||||
config->preserve_times_explicit_off = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -869,8 +896,6 @@ 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_NOOP:
|
||||
return 0;
|
||||
@@ -1115,7 +1140,7 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
}
|
||||
/* Remember that --server-host was explicitly given (the field itself
|
||||
defaults to 127.0.0.1, so a value check cannot distinguish it). Used
|
||||
@@ -1142,21 +1167,15 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
||||
config. */
|
||||
if (entry->offset == offsetof(Config, delete_missing_args))
|
||||
config->ignore_missing_args = true;
|
||||
/* -U/--atimes and -N/--crtimes carry their times inside the metadata
|
||||
payload, which is only transmitted when use_metadata is set, so either
|
||||
one implies metadata transmission. This is FastSync's broad -M bundle
|
||||
(mode/mtime travel too); it does NOT enable ownership application,
|
||||
which stays opt-in via the identity flags. */
|
||||
if (entry->offset == offsetof(Config, preserve_atimes) ||
|
||||
entry->offset == offsetof(Config, preserve_crtimes))
|
||||
config->use_metadata = true;
|
||||
/* -A/--acls implies permission preservation as well as the xattr channel;
|
||||
-X/--xattrs preserves only the extended attributes. Either sets the
|
||||
derived xattr transport bit so the sender emits the per-file xattr block. */
|
||||
if (entry->offset == offsetof(Config, preserve_xattrs) ||
|
||||
entry->offset == offsetof(Config, preserve_acls)) {
|
||||
config->use_metadata = true;
|
||||
config->use_xattrs = config->preserve_acls || config->preserve_xattrs;
|
||||
if (entry->offset == offsetof(Config, preserve_acls))
|
||||
config->preserve_perms = true;
|
||||
}
|
||||
if (entry->offset == offsetof(Config, fake_super))
|
||||
config->use_metadata = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1178,7 +1197,7 @@ static bool cli_handle_inline_chmod(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1206,29 +1225,44 @@ static bool cli_handle_meta_flags(CliParseCtx* ctx) {
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-a", "--archive")) {
|
||||
/* FastSync archive mode (-rlptD). FastSync is always recursive and always
|
||||
* preserves hard-link/other transfer semantics per its own flags, so -a
|
||||
* implies links, metadata (perms/times), devices and specials. Owner/group
|
||||
* are NOT implied; they require an explicit identity flag
|
||||
* (--numeric-ids/--usermap/--groupmap/--chown/--copy-as). Compression and
|
||||
* multithreading are NOT implied either (they are no longer part of
|
||||
* archive mode). */
|
||||
/* FastSync archive mode (-rlptgoD). FastSync is always recursive and
|
||||
* always preserves hard-link/other transfer semantics per its own flags, so
|
||||
* -a implies links plus the four per-attribute preservation flags
|
||||
* (perms/times/owner/group), devices and specials. Compression and
|
||||
* multithreading are NOT implied (they are no longer part of archive
|
||||
* mode). */
|
||||
config->follow_symlinks = true;
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
config->preserve_times = true;
|
||||
config->preserve_owner = true;
|
||||
config->preserve_group = true;
|
||||
config->preserve_devices = true;
|
||||
config->preserve_specials = true;
|
||||
log_info_message(LOG_INFO_MISC,
|
||||
"Enabled archive mode (-rlptD: links, metadata, devices, specials; "
|
||||
"owner/group opt-in)");
|
||||
"Enabled archive mode (-rlptgoD: links, perms, times, owner, group, "
|
||||
"devices, specials)");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-p", "--perms")) {
|
||||
/* rsync -p/--perms: preserve permission bits. Folded into FastSync's
|
||||
* broad metadata bundle (mode/mtime travel together). */
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled permission preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-t", "--times")) {
|
||||
config->preserve_times = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled time preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-o", "--owner")) {
|
||||
config->preserve_owner = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled owner preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-g", "--group")) {
|
||||
config->preserve_group = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled group preservation");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1357,12 +1391,12 @@ static bool cli_handle_transfer_flags(CliParseCtx* ctx) {
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--preserve", NULL)) {
|
||||
config->use_metadata = true;
|
||||
config->preserve_perms = true;
|
||||
config->preserve_times = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled metadata preservation");
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "-E", "--executability")) {
|
||||
config->use_metadata = true;
|
||||
config->use_executability = true;
|
||||
log_info_message(LOG_INFO_MISC, "Enabled executable permission preservation");
|
||||
return true;
|
||||
@@ -1806,7 +1840,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_owner = true;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--usermap", NULL)) {
|
||||
@@ -1819,7 +1853,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_owner = true;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--groupmap=", 11) == 0) {
|
||||
@@ -1827,7 +1861,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--groupmap", NULL)) {
|
||||
@@ -1840,7 +1874,7 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--chown=", 8) == 0) {
|
||||
@@ -1848,7 +1882,10 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
if (config->chown_uid_set)
|
||||
config->preserve_owner = true;
|
||||
if (config->chown_gid_set)
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (opt_is(arg, "--chown", NULL)) {
|
||||
@@ -1861,7 +1898,10 @@ static bool cli_handle_remote_basis_options(CliParseCtx* ctx) {
|
||||
ctx->exit_code = -1;
|
||||
return true;
|
||||
}
|
||||
config->use_metadata = true;
|
||||
if (config->chown_uid_set)
|
||||
config->preserve_owner = true;
|
||||
if (config->chown_gid_set)
|
||||
config->preserve_group = true;
|
||||
return true;
|
||||
}
|
||||
if (strncmp(arg, "--copy-as=", 10) == 0) {
|
||||
@@ -1921,19 +1961,10 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
||||
config->files_from_set = set;
|
||||
}
|
||||
|
||||
/* Device/special preservation recreates a node from its metadata mode (whose
|
||||
S_IFMT bits carry the node kind), so --devices/--specials/-D imply metadata
|
||||
transmission. --copy-devices/--write-devices treat the entry as data but a
|
||||
mtime/mode-preserving transfer still benefits from metadata, so all four
|
||||
imply it (FastSync's broad -M bundle; ownership stays opt-in). */
|
||||
if (config->preserve_devices || config->preserve_specials || config->copy_devices ||
|
||||
config->write_devices)
|
||||
config->use_metadata = true;
|
||||
|
||||
/* 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. */
|
||||
* --incremental (and, via the derived bit below, metadata) on the sender. */
|
||||
if (config_has_basis(config))
|
||||
config->use_incremental = true;
|
||||
|
||||
@@ -1966,12 +1997,27 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
|
||||
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) {
|
||||
log_message(LOG_LEVEL_INFO, "Enabling metadata preservation for incremental/delta transfer");
|
||||
config->use_metadata = true;
|
||||
/* --incremental/--delta historically auto-enabled the metadata path, which
|
||||
* applied mode+mtime (README: "--incremental Auto-enables --preserve").
|
||||
* Restore that behavior by turning on the two attributes unless the user
|
||||
* explicitly negated them (--no-perms/--no-times/--no-preserve). This runs
|
||||
* BEFORE the derived use_metadata bit so the transport frame is still sent
|
||||
* for the incremental/delta handshake even when both attributes were negated
|
||||
* via --no-preserve (metadata_explicitly_disabled handles that opt-out). */
|
||||
if ((config->use_incremental || config->use_delta) && !config->metadata_explicitly_disabled) {
|
||||
if (!config->preserve_perms_explicit_off)
|
||||
config->preserve_perms = true;
|
||||
if (!config->preserve_times_explicit_off)
|
||||
config->preserve_times = true;
|
||||
}
|
||||
|
||||
/* Derive the transport bit from the FINAL parsed flags. Every
|
||||
* preservation/ownership option that needs the metadata frame (per-attribute
|
||||
* perms/times/owner/group, atimes/crtimes, executability, xattrs/acls,
|
||||
* fake-super, devices/specials, chmod, identity maps/chown/copy-as, and the
|
||||
* incremental/delta handshake unless --no-preserve explicitly disabled it) is
|
||||
* centralized in config_derived_use_metadata(). */
|
||||
config->use_metadata = config_derived_use_metadata(config);
|
||||
/* Recompute the derived xattr flag from the FINAL preserve flags (after any
|
||||
* --no-xattrs/--no-acls negation) so the sender's wire gate always matches
|
||||
* the flags the receiver will recompute from the received config. */
|
||||
@@ -2121,6 +2167,10 @@ static int load_daemon_credentials(Config* config) {
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
/* Capture the process umask now, while still single-threaded: the cached
|
||||
* value is what file_mode_base() uses, and reading it later would race with
|
||||
* receiver threads creating files. */
|
||||
file_umask_capture();
|
||||
/* The server may close a connection mid-stream (e.g. when it rejects an
|
||||
oversized delta). Ignore SIGPIPE so that a broken TCP connection
|
||||
surfaces as a clean write error instead of killing the client. */
|
||||
|
||||
@@ -176,9 +176,10 @@ static bool prepare_scanner(const Config* config, int num_threads, PreparedScann
|
||||
options->excluded_paths = NULL;
|
||||
options->excluded_mutex = NULL;
|
||||
options->hardlinks = NULL;
|
||||
/* P7 Wave D: capture source directory times whenever metadata rides the
|
||||
wire. Whether they are APPLIED is decided receiver-side (-O skips). */
|
||||
options->capture_dir_times = config->use_metadata;
|
||||
/* P7 Wave D: capture source directory metadata when a directory attribute is
|
||||
requested (-p for modes, -t for times unless -O omits them). Whether they
|
||||
are APPLIED is decided receiver-side. */
|
||||
options->capture_dir_times = dir_metadata_should_capture(config);
|
||||
options->dir_entries = NULL;
|
||||
options->dir_entries_mutex = NULL;
|
||||
if (config->preserve_hard_links) {
|
||||
@@ -1444,7 +1445,8 @@ static bool send_directory_entry(const Client* client, File* file, const Config*
|
||||
stays within the receiver's bound, and a frame that would exceed it is never
|
||||
emitted. */
|
||||
static bool send_dir_times(const Client* client, const Config* config, ArrayList* dir_entries) {
|
||||
if (!client || !config || !config->use_metadata || !dir_entries || dir_entries->size == 0)
|
||||
if (!client || !config || !dir_metadata_should_capture(config) || !dir_entries ||
|
||||
dir_entries->size == 0)
|
||||
return true;
|
||||
int fd = client->file_descriptor;
|
||||
int index = 0;
|
||||
@@ -2250,7 +2252,7 @@ int send_files(Config* config) {
|
||||
receive_daemon_motd(client, config);
|
||||
if (!prepare_scanner(config, 0, &prepared))
|
||||
goto send_fail;
|
||||
if (config->use_metadata) {
|
||||
if (dir_metadata_should_capture(config)) {
|
||||
dir_entries = array_list_create(file_destroy);
|
||||
if (!dir_entries)
|
||||
goto send_fail;
|
||||
|
||||
+19
-10
@@ -20,12 +20,15 @@ void print_usage(void) {
|
||||
printf("Options:\n");
|
||||
printf(" -c, --checksum Verify content by checksum instead of size+mtime\n");
|
||||
printf(" -z, --compress [level] Enable compression (level 1-22, default 5)\n");
|
||||
printf(" -a, --archive rsync archive mode (-rlptD): links, perms, times,\n");
|
||||
printf(" devices and specials; owner/group are not implied;\n");
|
||||
printf(" not compression/multithreading\n");
|
||||
printf(" -a, --archive rsync archive mode (-rlptgoD): links, perms, times,\n");
|
||||
printf(" owner, group, devices and specials; not\n");
|
||||
printf(" compression/multithreading\n");
|
||||
printf(" -n, --dry-run Show what would be transferred\n");
|
||||
printf(" --remove-source-files Remove regular source files after successful transfer\n");
|
||||
printf(" -p, --perms Preserve permission bits (part of the metadata bundle)\n");
|
||||
printf(" -p, --perms Preserve permission bits\n");
|
||||
printf(" -t, --times Preserve modification times\n");
|
||||
printf(" -o, --owner Preserve owner (uid)\n");
|
||||
printf(" -g, --group Preserve group (gid)\n");
|
||||
printf(" --ssh-port <port> SSH port (default: 22)\n");
|
||||
printf(" -e, --rsh <command> Remote shell to launch on the client for the SSH\n");
|
||||
printf(" transport (default: ssh). The command may include\n");
|
||||
@@ -161,7 +164,12 @@ void print_usage(void) {
|
||||
printf(" --debug=FLAGS Fine-grained debug logging (use --debug=help for flags)\n");
|
||||
printf(" --info=FLAGS Fine-grained info: copy,misc,skip,stats,all,none\n");
|
||||
printf(" none suppresses info even with --verbose\n");
|
||||
printf(" --preserve Preserve file metadata (long form only)\n");
|
||||
printf(" --preserve Preserve permissions and times (= -pt; long form only)\n");
|
||||
printf(" --no-perms Negate -p/--perms\n");
|
||||
printf(" --no-times Negate -t/--times\n");
|
||||
printf(" --no-owner Negate -o/--owner\n");
|
||||
printf(" --no-group Negate -g/--group\n");
|
||||
printf(" --no-preserve Disable metadata preservation (negates --preserve)\n");
|
||||
printf(" -E, --executability Preserve executable permission bits\n");
|
||||
printf(" -U, --atimes Preserve access times\n");
|
||||
printf(" -N, --crtimes Capture birth time; cannot be applied (documented\n");
|
||||
@@ -180,8 +188,9 @@ void print_usage(void) {
|
||||
printf(" (char/block device-node creation, --write-devices)\n");
|
||||
printf(" within the confined receive root. Never elevates\n");
|
||||
printf(" privileges and never bypasses confinement; ownership\n");
|
||||
printf(" is still applied only with an explicit identity flag\n");
|
||||
printf(" (--numeric-ids/--chown/--usermap/--groupmap/--copy-as)\n");
|
||||
printf(" is still applied only with -o/--owner, -g/--group, or an\n");
|
||||
printf(" explicit identity flag (--numeric-ids/--chown/--usermap/\n");
|
||||
printf(" --groupmap/--copy-as)\n");
|
||||
printf(" --no-super Forbid those super-user activities even when the\n");
|
||||
printf(" receiver is running as root\n");
|
||||
printf(" --chmod <changes> Modify transferred permissions (rsync syntax)\n");
|
||||
@@ -196,12 +205,12 @@ void print_usage(void) {
|
||||
printf(" USER:GROUP, USER (owner only), :GROUP (group only); a\n");
|
||||
printf(" value of * means the current/root user as appropriate.\n");
|
||||
printf(" Names resolve on the source machine; @N for numerics.\n");
|
||||
printf(" (Metadata is enabled with --preserve; -M now means\n");
|
||||
printf(" rsync's --remote-option.)\n");
|
||||
printf(" (Implies owner/group metadata; -M now means rsync's\n");
|
||||
printf(" --remote-option.)\n");
|
||||
printf(" --copy-as=USER[:GROUP] Force every written entry (files, dirs, symlinks\n");
|
||||
printf(" and special nodes) to USER[:GROUP], resolved on the\n");
|
||||
printf(" source machine like --chown. Requires a privileged\n");
|
||||
printf(" (root) receiver and implies --preserve; an\n");
|
||||
printf(" (root) receiver and implies owner/group metadata; an\n");
|
||||
printf(" unprivileged receiver refuses the transfer. Never\n");
|
||||
printf(" switches process credentials (safe-subset; see\n");
|
||||
printf(" RSYNC_COMPAT.md). A daemon refuses it.\n");
|
||||
|
||||
Reference in New Issue
Block a user