feat(stats): receiver-observed created/literal counters (protocol 2.28.0)
- PROTOCOL_VERSION 2.27.0 -> 2.28.0; STATUS_STATS gains literal_bytes and the created reg/dir/link/special counters (golden wire updated) - receiver reports which destination entries it newly created, including implicitly-created parent directories below the logical transfer root, so Number of created files carries rsync's per-type breakdown - Literal data is now exact for a delta transfer (receiver counts the literal fragments it stored) - differential-tested vs rsync 3.4.1 for fresh-create, update and delta
This commit is contained in:
+15
-2
@@ -82,7 +82,7 @@ typedef struct {
|
||||
typedef enum SuperMode { SUPER_MODE_AUTO = 0, SUPER_MODE_ON = 1, SUPER_MODE_OFF = 2 } SuperMode;
|
||||
|
||||
/* ===========================================================================
|
||||
* Config wire-field table (single source of truth for protocol 2.27.0).
|
||||
* Config wire-field table (single source of truth for protocol 2.28.0).
|
||||
*
|
||||
* Every field below crosses the wire. The table is the ONLY place a
|
||||
* serialized field is named: config.h expands CONFIG_WIRE_FIELDS() to declare
|
||||
@@ -1006,7 +1006,20 @@ typedef struct Config {
|
||||
* the sender can print rsync's `deleting PATH` lines for a real deletion. No
|
||||
* change to the fixed STATUS_STATS record itself; only a new trailing config
|
||||
* bool, which still requires the version bump for the strict lockstep. */
|
||||
#define PROTOCOL_VERSION "2.27.0"
|
||||
/* (8) --stats receiver-observed counters (protocol 2.28.0): the fixed
|
||||
* STATUS_STATS record grows from three counters to eight. The receiver now
|
||||
* reports the bytes it literally stored (`literal_data`) and the count of
|
||||
* destination entries it newly CREATED, split by type
|
||||
* (reg/dir/link/special), so the sender can print rsync's exact
|
||||
* `Number of created files: N (reg: X, dir: Y, link: Z, special: W)` line and
|
||||
* an exact `Literal data` total even for delta transfers. The config-frame
|
||||
* LAYOUT is unchanged (no new config field), but the STATUS_STATS body grows,
|
||||
* so a 2.27 peer that does not consume the five new fixed-width counters would
|
||||
* desynchronize on the trailing would-delete path list; the strict
|
||||
* same-version handshake (config_receive rejects a mismatched version before
|
||||
* parsing anything else) keeps mixed deployments from ever reaching that
|
||||
* state. */
|
||||
#define PROTOCOL_VERSION "2.28.0"
|
||||
#define DEFAULT_CHUNK_SIZE (10 * 1024 * 1024)
|
||||
/* Upper bound on total basis-dir entries (rsync caps --link-dest at 20). */
|
||||
#define MAX_BASIS_DIRS 64
|
||||
|
||||
+156
-21
@@ -187,6 +187,7 @@ File* file_create(const char* path) {
|
||||
file->xattrs = NULL;
|
||||
file->dest_state = (OutputDestState){0};
|
||||
file->matched_bytes = 0;
|
||||
file->literal_bytes = 0;
|
||||
return file;
|
||||
}
|
||||
|
||||
@@ -614,6 +615,103 @@ static int open_dir_beneath_root(const char* resolved, const char* root) {
|
||||
}
|
||||
|
||||
int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs) {
|
||||
return file_open_secure_parent_counted(path, leaf_out, create_dirs, NULL, NULL);
|
||||
}
|
||||
|
||||
/* The logical transfer root expressed in the same coordinate as the secure
|
||||
* parent walk's `rel_buf` (relative to the authorized root, with a leading
|
||||
* '/'), used as the floor at or below which a created directory is a real
|
||||
* file-list entry. The on-disk transfer root is the receive root joined to the
|
||||
* wire path; the mirror scaffolding above it (the absolute source path below
|
||||
* the destination root) is not an rsync entry. Returns an allocated string or
|
||||
* NULL (count every created component). */
|
||||
static char* transfer_root_floor(const Config* config) {
|
||||
if (!config || !config->send_directory || config->send_directory[0] == '\0')
|
||||
return NULL;
|
||||
const char* spec = config->send_directory;
|
||||
const char* after = spec;
|
||||
if (spec[0] == '.' && spec[1] == '/') {
|
||||
after = spec + 2;
|
||||
} else {
|
||||
const char* cut = strstr(spec, "/./");
|
||||
if (cut)
|
||||
after = cut + 3;
|
||||
}
|
||||
while (*after == '/')
|
||||
after++;
|
||||
char* wire_root = str_dup(after);
|
||||
if (!wire_root)
|
||||
return NULL;
|
||||
size_t wlen = strlen(wire_root);
|
||||
while (wlen > 0 && wire_root[wlen - 1] == '/')
|
||||
wire_root[--wlen] = '\0';
|
||||
if (wlen == 0) {
|
||||
free(wire_root);
|
||||
return NULL;
|
||||
}
|
||||
char* disk_root = config->receive_root_directory
|
||||
? path_cat(config->receive_root_directory, wire_root)
|
||||
: str_dup(wire_root);
|
||||
free(wire_root);
|
||||
if (!disk_root)
|
||||
return NULL;
|
||||
const char* root_path = utils_get_authorized_root_path();
|
||||
const char* floor = disk_root;
|
||||
if (root_path && root_path[0] == '/') {
|
||||
size_t rl = strlen(root_path);
|
||||
while (rl > 0 && root_path[rl - 1] == '/')
|
||||
rl--;
|
||||
if (strncmp(disk_root, root_path, rl) == 0 && (disk_root[rl] == '/' || disk_root[rl] == '\0'))
|
||||
floor = disk_root + rl;
|
||||
}
|
||||
while (*floor == '/')
|
||||
floor++;
|
||||
char* out = str_dup(floor);
|
||||
free(disk_root);
|
||||
if (!out)
|
||||
return NULL;
|
||||
if (out[0] == '\0') {
|
||||
free(out);
|
||||
return NULL;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/* A created parent component counts toward `Number of created files` only when
|
||||
* its receive-root-relative path is at or below the logical transfer root
|
||||
* (`count_floor`). The transfer root itself corresponds to rsync's `.` entry
|
||||
* (created on a fresh destination, pre-existing otherwise); the mirror
|
||||
* scaffolding above it is FastSync's absolute-path layout, not an rsync entry. */
|
||||
static bool created_dir_counts(const char* count_floor, const char* rel_buf,
|
||||
const char* component) {
|
||||
if (!count_floor)
|
||||
return true;
|
||||
char candidate[PATH_MAX];
|
||||
int n = snprintf(candidate, sizeof(candidate), "%s/%s", rel_buf, component);
|
||||
if (n < 0 || (size_t)n >= sizeof(candidate))
|
||||
return false;
|
||||
const char* cand = candidate;
|
||||
while (*cand == '/')
|
||||
cand++;
|
||||
size_t fl = strlen(count_floor);
|
||||
if (strncmp(cand, count_floor, fl) != 0)
|
||||
return false;
|
||||
return cand[fl] == '\0' || cand[fl] == '/';
|
||||
}
|
||||
|
||||
/* Public wrapper for the receiver's created-directory accounting: the logical
|
||||
* transfer root expressed receive-root-relative, or NULL when the wire paths
|
||||
* carry no mirror scaffolding above it (--relative and --files-from, whose
|
||||
* paths are already relative to the transfer root). The caller frees a
|
||||
* non-NULL result. */
|
||||
char* file_transfer_root_floor(const Config* config) {
|
||||
if (!config || config->relative || config->files_from_set != NULL)
|
||||
return NULL;
|
||||
return transfer_root_floor(config);
|
||||
}
|
||||
|
||||
int file_open_secure_parent_counted(const char* path, char** leaf_out, bool create_dirs,
|
||||
unsigned* dirs_created, const char* count_floor) {
|
||||
char* copy = str_dup(path);
|
||||
if (!copy)
|
||||
return -1;
|
||||
@@ -674,6 +772,14 @@ int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs)
|
||||
if (next < 0 && create_dirs && errno == ENOENT) {
|
||||
bool created = mkdirat(fd, component, (mode_t)(0777 & ~(mode_t)file_process_umask())) == 0;
|
||||
if (created || errno == EEXIST) {
|
||||
/* Protocol 2.28.0: only directories the logical file list would
|
||||
create count toward `Number of created files`; the mirror
|
||||
scaffolding above the transfer root (e.g. the absolute source path
|
||||
under the destination root) is not an rsync entry. `count_floor`
|
||||
is a receive-root-relative prefix that must be reached before a
|
||||
created component is counted. */
|
||||
if (created && dirs_created && created_dir_counts(count_floor, rel_buf, component))
|
||||
(*dirs_created)++;
|
||||
/* P7 Wave E: --copy-as owns EVERY entry, including the intermediate
|
||||
directories this walk creates implicitly. Its target ids are a
|
||||
global policy, so they are available here without per-entry source
|
||||
@@ -1026,15 +1132,14 @@ static void restore_extra_fd(int fd, const FileMetadata* metadata, const FileXat
|
||||
}
|
||||
}
|
||||
|
||||
static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
unsigned long long data_size, bool inplace, bool sparse,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool update, bool no_replace,
|
||||
bool use_fsync, const char* temp_dir,
|
||||
const FileXattrList* xattrs, bool fake_super,
|
||||
bool keep_partial) {
|
||||
static bool
|
||||
file_to_disk_secure_impl(const char* path, const void* data, unsigned long long data_size,
|
||||
bool inplace, bool sparse, bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool update, bool no_replace, bool use_fsync,
|
||||
const char* temp_dir, const FileXattrList* xattrs, bool fake_super,
|
||||
bool keep_partial, unsigned* dirs_created, const char* count_floor) {
|
||||
char* leaf = NULL;
|
||||
int dirfd = file_open_secure_parent(path, &leaf, true);
|
||||
int dirfd = file_open_secure_parent_counted(path, &leaf, true, dirs_created, count_floor);
|
||||
if (dirfd < 0)
|
||||
return false;
|
||||
int fd = -1;
|
||||
@@ -1325,7 +1430,7 @@ static bool file_to_disk_secure_impl(const char* path, const void* data,
|
||||
"non-atomic copy into the destination directory");
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
policy, update, no_replace, use_fsync, NULL, xattrs, fake_super,
|
||||
keep_partial);
|
||||
keep_partial, dirs_created, count_floor);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
@@ -1334,7 +1439,8 @@ bool file_to_disk_secure(const char* path, const void* data, unsigned long long
|
||||
bool inplace, bool sparse, bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
policy, false, false, false, temp_dir, NULL, false, false);
|
||||
policy, false, false, false, temp_dir, NULL, false, false, NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_update(const char* path, const void* data, unsigned long long data_size,
|
||||
@@ -1342,7 +1448,8 @@ bool file_to_disk_secure_update(const char* path, const void* data, unsigned lon
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
policy, true, false, false, temp_dir, NULL, false, false);
|
||||
policy, true, false, false, temp_dir, NULL, false, false, NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_with_fsync(const char* path, const void* data,
|
||||
@@ -1350,7 +1457,8 @@ bool file_to_disk_secure_with_fsync(const char* path, const void* data,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool use_fsync, const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
policy, false, false, use_fsync, temp_dir, NULL, false, false);
|
||||
policy, false, false, use_fsync, temp_dir, NULL, false, false,
|
||||
NULL, NULL);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
||||
@@ -1358,7 +1466,8 @@ bool file_to_disk_secure_no_replace(const char* path, const void* data,
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, false, sparse, preallocate, metadata,
|
||||
policy, false, true, false, temp_dir, NULL, false, false);
|
||||
policy, false, true, false, temp_dir, NULL, false, false, NULL,
|
||||
NULL);
|
||||
}
|
||||
|
||||
/* Receiver write-path variant that also applies the per-file xattrs (-X/-A)
|
||||
@@ -1371,9 +1480,21 @@ bool file_to_disk_secure_attrs(const char* path, const void* data, unsigned long
|
||||
const FileMetadata* metadata, FileAttrPolicy policy, bool update,
|
||||
bool no_replace, bool use_fsync, const FileXattrList* xattrs,
|
||||
bool fake_super, bool keep_partial, const char* temp_dir) {
|
||||
return file_to_disk_secure_attrs_counted(path, data, data_size, inplace, sparse, preallocate,
|
||||
metadata, policy, update, no_replace, use_fsync, xattrs,
|
||||
fake_super, keep_partial, temp_dir, NULL, NULL);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_attrs_counted(const char* path, const void* data,
|
||||
unsigned long long data_size, bool inplace, bool sparse,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool update, bool no_replace,
|
||||
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
||||
bool keep_partial, const char* temp_dir,
|
||||
unsigned* dirs_created, const char* count_floor) {
|
||||
return file_to_disk_secure_impl(path, data, data_size, inplace, sparse, preallocate, metadata,
|
||||
policy, update, no_replace, use_fsync, temp_dir, xattrs,
|
||||
fake_super, keep_partial);
|
||||
fake_super, keep_partial, dirs_created, count_floor);
|
||||
}
|
||||
|
||||
/* Atomic --link-dest install. The destination is replaced (via a temporary
|
||||
@@ -1396,11 +1517,12 @@ static bool file_to_disk_secure_link_impl(const char* path, const char* basis_pa
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool use_fsync,
|
||||
const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir) {
|
||||
const char* temp_dir, unsigned* dirs_created,
|
||||
const char* count_floor) {
|
||||
if (!path || !basis_path)
|
||||
return false;
|
||||
char* leaf = NULL;
|
||||
int dirfd = file_open_secure_parent(path, &leaf, true);
|
||||
int dirfd = file_open_secure_parent_counted(path, &leaf, true, dirs_created, count_floor);
|
||||
if (dirfd < 0)
|
||||
return false;
|
||||
|
||||
@@ -1483,9 +1605,9 @@ static bool file_to_disk_secure_link_impl(const char* path, const char* basis_pa
|
||||
free(leaf);
|
||||
/* The basis file could not be linked in (missing, cross-device, refused
|
||||
by the filesystem). Write a byte-identical local copy instead. */
|
||||
return file_to_disk_secure_attrs(path, data, data_size, false, false, preallocate, metadata,
|
||||
policy, false, false, use_fsync, xattrs, fake_super, false,
|
||||
temp_dir);
|
||||
return file_to_disk_secure_attrs_counted(
|
||||
path, data, data_size, false, false, preallocate, metadata, policy, false, false, use_fsync,
|
||||
xattrs, fake_super, false, temp_dir, dirs_created, count_floor);
|
||||
}
|
||||
|
||||
if (scratch_dirfd >= 0)
|
||||
@@ -1500,7 +1622,7 @@ bool file_to_disk_secure_link(const char* path, const char* basis_path, const vo
|
||||
const FileMetadata* metadata, FileAttrPolicy policy, bool use_fsync,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_link_impl(path, basis_path, data, data_size, preallocate, metadata,
|
||||
policy, use_fsync, NULL, false, temp_dir);
|
||||
policy, use_fsync, NULL, false, temp_dir, NULL, NULL);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_link_attrs(const char* path, const char* basis_path, const void* data,
|
||||
@@ -1508,8 +1630,21 @@ bool file_to_disk_secure_link_attrs(const char* path, const char* basis_path, co
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir) {
|
||||
return file_to_disk_secure_link_attrs_counted(path, basis_path, data, data_size, preallocate,
|
||||
metadata, policy, use_fsync, xattrs, fake_super,
|
||||
temp_dir, NULL, NULL);
|
||||
}
|
||||
|
||||
bool file_to_disk_secure_link_attrs_counted(const char* path, const char* basis_path,
|
||||
const void* data, unsigned long long data_size,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool use_fsync,
|
||||
const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir, unsigned* dirs_created,
|
||||
const char* count_floor) {
|
||||
return file_to_disk_secure_link_impl(path, basis_path, data, data_size, preallocate, metadata,
|
||||
policy, use_fsync, xattrs, fake_super, temp_dir);
|
||||
policy, use_fsync, xattrs, fake_super, temp_dir,
|
||||
dirs_created, count_floor);
|
||||
}
|
||||
|
||||
bool file_write_to_disk(const char* path, const void* data, unsigned long long data_size,
|
||||
|
||||
@@ -87,6 +87,11 @@ bool file_path_exists_secure(const char* path);
|
||||
bool file_stat_secure(const char* path, struct stat* st);
|
||||
bool file_destination_is_newer_secure(const char* path, const FileMetadata* metadata);
|
||||
int file_open_secure_parent(const char* path, char** leaf_out, bool create_dirs);
|
||||
/* Protocol 2.28.0 variant: also increments *dirs_created for every missing
|
||||
* parent directory this walk creates that lies strictly below `count_floor`
|
||||
* (a receive-root-relative path, or NULL to count all of them). */
|
||||
int file_open_secure_parent_counted(const char* path, char** leaf_out, bool create_dirs,
|
||||
unsigned* dirs_created, const char* count_floor);
|
||||
bool file_ensure_directory_secure(const char* path);
|
||||
bool file_directory_exists_secure(const char* path);
|
||||
bool file_rename_secure(const char* old_path, const char* new_path);
|
||||
@@ -158,5 +163,27 @@ bool file_to_disk_secure_link_attrs(const char* path, const char* basis_path, co
|
||||
const FileMetadata* metadata, FileAttrPolicy policy,
|
||||
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir);
|
||||
/* Protocol 2.28.0 receiver-stat variants: like the two above but additionally
|
||||
* report through `dirs_created` (when non-NULL) how many parent directories the
|
||||
* confined secure walk had to create that lie strictly below `count_floor` (a
|
||||
* receive-root-relative prefix, or NULL for all). Used to reproduce rsync's
|
||||
* `Number of created files` directory count on a fresh destination. */
|
||||
bool file_to_disk_secure_attrs_counted(const char* path, const void* data,
|
||||
unsigned long long data_size, bool inplace, bool sparse,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool update, bool no_replace,
|
||||
bool use_fsync, const FileXattrList* xattrs, bool fake_super,
|
||||
bool keep_partial, const char* temp_dir,
|
||||
unsigned* dirs_created, const char* count_floor);
|
||||
bool file_to_disk_secure_link_attrs_counted(const char* path, const char* basis_path,
|
||||
const void* data, unsigned long long data_size,
|
||||
bool preallocate, const FileMetadata* metadata,
|
||||
FileAttrPolicy policy, bool use_fsync,
|
||||
const FileXattrList* xattrs, bool fake_super,
|
||||
const char* temp_dir, unsigned* dirs_created,
|
||||
const char* count_floor);
|
||||
/* The logical transfer root expressed receive-root-relative, or NULL when the
|
||||
* wire paths carry no mirror scaffolding above it. Caller frees non-NULL. */
|
||||
char* file_transfer_root_floor(const Config* config);
|
||||
|
||||
#endif
|
||||
|
||||
+77
-12
@@ -37,7 +37,12 @@
|
||||
#define MANIFEST_ENTRY_OVERHEAD (sizeof(char*) + 16)
|
||||
|
||||
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config) {
|
||||
return file_save_to_disk_full(root_directory, file, config) != FILE_SAVE_ERROR;
|
||||
return file_save_to_disk_full_ex(root_directory, file, config, NULL, NULL) != FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
||||
const Config* config) {
|
||||
return file_save_to_disk_full_ex(root_directory, file, config, NULL, NULL);
|
||||
}
|
||||
|
||||
/* --delay-updates receiver path: write the file into a private staging tree
|
||||
@@ -208,13 +213,14 @@ static FileSaveResult hardlink_sibling_absent_first(const char* destination_path
|
||||
--existing/--ignore-existing/--update policies are decided against the final
|
||||
destination like every normal write. */
|
||||
static FileSaveResult file_save_hardlink_sibling(const char* root_directory, const File* file,
|
||||
const Config* config) {
|
||||
const Config* config, bool* created) {
|
||||
Config* cfg = (Config*)config;
|
||||
if (!root_directory || !file || !file->path || !file->hardlink_target)
|
||||
return FILE_SAVE_ERROR;
|
||||
char* destination_path = path_cat(root_directory, file->path);
|
||||
if (!destination_path)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool existed = file_path_exists_secure(destination_path);
|
||||
|
||||
if (cfg->existing && !file_path_exists_secure(destination_path)) {
|
||||
free(destination_path);
|
||||
@@ -278,6 +284,8 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
free(staged_first);
|
||||
free(staged_sibling);
|
||||
free(destination_path);
|
||||
if (ok && created && !existed)
|
||||
*created = true;
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
@@ -324,6 +332,8 @@ static FileSaveResult file_save_hardlink_sibling(const char* root_directory, con
|
||||
free(content);
|
||||
free(first_disk);
|
||||
free(destination_path);
|
||||
if (ok && created && !existed)
|
||||
*created = true;
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
@@ -360,7 +370,7 @@ bool file_special_rdev_valid(int32_t major, int32_t minor, mode_t mode) {
|
||||
* non-device entry must carry an empty rdev.
|
||||
*/
|
||||
static FileSaveResult file_save_special_to_disk(const char* root_directory, const File* file,
|
||||
const Config* config) {
|
||||
const Config* config, bool* created) {
|
||||
/* The empty-path and structural checks stay unconditional; the redundant
|
||||
".." list-path re-check is skipped under --trust-sender exactly like the
|
||||
receive layer (confinement is deferred to the secure parent walk below,
|
||||
@@ -412,6 +422,7 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
char* destination = path_cat(root_directory, file->path);
|
||||
if (!destination)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool existed = file_path_exists_secure(destination);
|
||||
char* leaf = NULL;
|
||||
int parent_fd = file_open_secure_parent(destination, &leaf, true);
|
||||
if (parent_fd < 0) {
|
||||
@@ -532,6 +543,8 @@ static FileSaveResult file_save_special_to_disk(const char* root_directory, cons
|
||||
free(destination);
|
||||
/* A failed required --copy-as ownership marks the node as failed; every other
|
||||
* identity policy stays best-effort. */
|
||||
if (owner_ok && created && !existed)
|
||||
*created = true;
|
||||
return owner_ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
@@ -610,8 +623,13 @@ static FileSaveResult file_save_write_device(const char* root_directory, const F
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_SKIPPED;
|
||||
}
|
||||
|
||||
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
||||
const Config* config) {
|
||||
FileSaveResult file_save_to_disk_full_ex(const char* root_directory, const File* file,
|
||||
const Config* config, bool* created,
|
||||
unsigned* created_dirs) {
|
||||
if (created)
|
||||
*created = false;
|
||||
if (created_dirs)
|
||||
*created_dirs = 0;
|
||||
/* Central no-mutation guard: a server-contacting --dry-run (or a local batch
|
||||
apply that somehow carries dry_run) must never touch the destination, no
|
||||
matter which caller reached this primitive. The per-caller guards remain,
|
||||
@@ -658,7 +676,7 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
/* Device/special node (--devices/--specials): recreate the node instead of
|
||||
writing content (privilege-gated, confined, rdev-validated). */
|
||||
if (file->is_special)
|
||||
return file_save_special_to_disk(root_directory, file, config);
|
||||
return file_save_special_to_disk(root_directory, file, config, created);
|
||||
/* --write-devices: write straight into an existing device node. Writing
|
||||
into a device is a super-user activity, so --no-super must suppress it just
|
||||
like device-node creation; the default AUTO/--super attempt it (the wide
|
||||
@@ -689,6 +707,7 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
char* dir_path = path_cat(root_directory, file->path);
|
||||
if (!dir_path)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool dir_existed = file_path_exists_secure(dir_path);
|
||||
bool ok = file_ensure_directory_secure(dir_path);
|
||||
/* P7 Wave E: apply the negotiated ownership to the directory ITSELF (not
|
||||
just the files inside it). --copy-as and every explicit identity policy
|
||||
@@ -712,6 +731,8 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
free(leaf);
|
||||
}
|
||||
free(dir_path);
|
||||
if (ok && created && !dir_existed)
|
||||
*created = true;
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
@@ -728,6 +749,7 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
char* link_path = path_cat(root_directory, file->path);
|
||||
if (!link_path)
|
||||
return FILE_SAVE_ERROR;
|
||||
bool link_existed = file_path_exists_secure(link_path);
|
||||
/* The link value is stored verbatim (rsync -l parity: absolute and
|
||||
".."-bearing targets are preserved; the scanner's --safe-links /
|
||||
--copy-unsafe-links decide which links are sent at all). --munge-links
|
||||
@@ -768,6 +790,8 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
ok = file_restore_symlink_metadata(link_path, file->metadata, link_policy,
|
||||
config->omit_link_times);
|
||||
}
|
||||
if (ok && created && !link_existed)
|
||||
*created = true;
|
||||
free(link_path);
|
||||
return ok ? FILE_SAVE_WRITTEN : FILE_SAVE_ERROR;
|
||||
}
|
||||
@@ -777,7 +801,7 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
byte-identical copy of) the group's first member. Handled entirely here,
|
||||
before the normal data-write paths (which would create an empty file). */
|
||||
if (file->link_group != 0 && !file->link_first && file->hardlink_target != NULL) {
|
||||
return file_save_hardlink_sibling(root_directory, file, config);
|
||||
return file_save_hardlink_sibling(root_directory, file, config, created);
|
||||
}
|
||||
|
||||
/* These options arrive from the client. --backup-dir, --partial-dir and
|
||||
@@ -808,12 +832,18 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
free(disk_path);
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
/* Snapshot the final destination's existence BEFORE any backup/force/partial
|
||||
step can move or remove it, so the receiver can report rsync's
|
||||
`Number of created files` (protocol 2.28.0). */
|
||||
bool dest_existed = file_path_exists_secure(destination_path);
|
||||
|
||||
/* --delay-updates diverts the whole write into the staging tree; the rest of
|
||||
this function is the immediate-install path. */
|
||||
if (config && config->delay_updates) {
|
||||
FileSaveResult result =
|
||||
file_stage_delayed_update(root_directory, destination_path, file, (Config*)config);
|
||||
if (result == FILE_SAVE_WRITTEN && created && !dest_existed)
|
||||
*created = true;
|
||||
free(confined_backup);
|
||||
free(confined_partial);
|
||||
free(destination_path);
|
||||
@@ -939,19 +969,23 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
existing/ignore-existing/update/backup preamble above has already made the
|
||||
policy decision. */
|
||||
bool ok;
|
||||
char* count_floor = file_transfer_root_floor(config);
|
||||
if (config && file->basis_link) {
|
||||
ok = file_to_disk_secure_link_attrs(
|
||||
ok = file_to_disk_secure_link_attrs_counted(
|
||||
disk_path, file->basis_link, file->data->data, file->data->size, config->preallocate,
|
||||
metadata, policy, config->use_fsync, file->xattrs, config->fake_super, confined_temp);
|
||||
metadata, policy, config->use_fsync, file->xattrs, config->fake_super, confined_temp,
|
||||
created_dirs, count_floor);
|
||||
} else {
|
||||
/* The plain no-replace / update / with-fsync engines, plus per-file xattr
|
||||
(-X/-A) and --fake-super application on the written fd. */
|
||||
ok = file_to_disk_secure_attrs(
|
||||
ok = file_to_disk_secure_attrs_counted(
|
||||
disk_path, file->data->data, file->data->size, inplace, sparse,
|
||||
config && config->preallocate, metadata, policy, config && config->update,
|
||||
config && config->ignore_existing, config && config->use_fsync, file->xattrs,
|
||||
config ? config->fake_super : false, config ? config->partial : false, confined_temp);
|
||||
config ? config->fake_super : false, config ? config->partial : false, confined_temp,
|
||||
created_dirs, count_floor);
|
||||
}
|
||||
free(count_floor);
|
||||
free(confined_temp);
|
||||
confined_temp = NULL;
|
||||
if (!ok)
|
||||
@@ -972,6 +1006,8 @@ FileSaveResult file_save_to_disk_full(const char* root_directory, const File* fi
|
||||
free(confined_partial);
|
||||
free(destination_path);
|
||||
free(disk_path);
|
||||
if (created && !dest_existed)
|
||||
*created = true;
|
||||
return FILE_SAVE_WRITTEN;
|
||||
|
||||
fail:
|
||||
@@ -984,6 +1020,30 @@ fail:
|
||||
return FILE_SAVE_ERROR;
|
||||
}
|
||||
|
||||
void receiver_stats_note_saved(ReceiverStats* stats, const File* file, bool created,
|
||||
unsigned created_dirs) {
|
||||
if (!stats || !file)
|
||||
return;
|
||||
bool is_sibling = file->link_group != 0 && !file->link_first;
|
||||
if (!file->is_dir && !file->is_symlink && !file->is_special && !is_sibling) {
|
||||
unsigned long long literal = file->literal_bytes;
|
||||
if (literal == 0 && file->matched_bytes == 0)
|
||||
literal = file->data ? file->data->size : 0;
|
||||
stats->literal_bytes += literal;
|
||||
}
|
||||
stats->created_dir += created_dirs;
|
||||
if (!created)
|
||||
return;
|
||||
if (file->is_dir)
|
||||
stats->created_dir++;
|
||||
else if (file->is_symlink)
|
||||
stats->created_link++;
|
||||
else if (file->is_special)
|
||||
stats->created_special++;
|
||||
else
|
||||
stats->created_reg++;
|
||||
}
|
||||
|
||||
/* Receive a file's xattr block (when the config enables xattr transport) and
|
||||
* attach it to `file`. Returns false on a malformed/oversized frame. */
|
||||
static bool receive_file_xattrs(File* file, int fd, const Config* config) {
|
||||
@@ -1094,11 +1154,15 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
return NULL;
|
||||
}
|
||||
/* Wire-stats tally: bytes taken straight from the basis file (matched
|
||||
delta blocks). Computed before the delta is destroyed. */
|
||||
delta blocks) and bytes shipped literally (protocol 2.28.0). Computed
|
||||
before the delta is destroyed. */
|
||||
unsigned long long matched = 0;
|
||||
unsigned long long literal = 0;
|
||||
for (uint32_t k = 0; k < delta->instruction_count; k++) {
|
||||
if (delta->instructions[k].type == DELTA_INSTR_BLOCK_MATCH)
|
||||
matched += delta->instructions[k].match.length;
|
||||
else if (delta->instructions[k].type == DELTA_INSTR_LITERAL)
|
||||
literal += delta->instructions[k].literal.length;
|
||||
}
|
||||
void* new_data = delta_apply(old_data, old_size, delta, config->delta_block_size);
|
||||
delta_destroy(delta);
|
||||
@@ -1119,6 +1183,7 @@ static File* receive_delta_file(int fd, const Config* config, const char* check_
|
||||
return NULL;
|
||||
}
|
||||
file->matched_bytes = matched;
|
||||
file->literal_bytes = literal;
|
||||
|
||||
if (config->use_metadata) {
|
||||
int meta_ok = 1;
|
||||
|
||||
@@ -179,6 +179,22 @@ typedef enum { FILE_SAVE_ERROR = 0, FILE_SAVE_WRITTEN = 1, FILE_SAVE_SKIPPED = 2
|
||||
|
||||
FileSaveResult file_save_to_disk_full(const char* root_directory, const File* file,
|
||||
const Config* config);
|
||||
/* Protocol 2.28.0 variant: also reports through `created` (when non-NULL)
|
||||
* whether the destination entry did not exist before this save, and through
|
||||
* `created_dirs` how many parent directories the confined walk created, so the
|
||||
* receiver can build rsync's `Number of created files` breakdown. The plain
|
||||
* file_save_to_disk_full() is this with both out-params NULL. */
|
||||
FileSaveResult file_save_to_disk_full_ex(const char* root_directory, const File* file,
|
||||
const Config* config, bool* created,
|
||||
unsigned* created_dirs);
|
||||
bool file_save_to_disk(const char* root_directory, const File* file, const Config* config);
|
||||
|
||||
/* Protocol 2.28.0 receiver counter accumulator: fold one successfully saved
|
||||
* entry into `stats`, adding its receiver-observed literal bytes and, when
|
||||
* `created`, the matching created-by-type counter (regular file / symlink /
|
||||
* special) plus `created_dirs` implicitly-created parent directories.
|
||||
* Non-first hardlink siblings contribute no literal bytes. */
|
||||
void receiver_stats_note_saved(ReceiverStats* stats, const File* file, bool created,
|
||||
unsigned created_dirs);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -97,6 +97,11 @@ typedef struct {
|
||||
* basis file (matched delta blocks) for this entry. 0 when the file was sent
|
||||
* whole. Accumulated into ReceiverStats.matched_data by the receiver sink. */
|
||||
unsigned long long matched_bytes;
|
||||
/* Receiver-only (protocol 2.28.0) wire-stats tally: the literal delta fragment
|
||||
* bytes this entry carried (DELTA_INSTR_LITERAL). 0 when the file was sent
|
||||
* whole; the sink then falls back to the whole payload size. Accumulated
|
||||
* into ReceiverStats.literal_bytes. */
|
||||
unsigned long long literal_bytes;
|
||||
} File;
|
||||
|
||||
/* The path that should be sent on the wire and used for the receiver-side
|
||||
|
||||
+15
-13
@@ -105,25 +105,27 @@ bool format_dest_state_receive(int fd, OutputDestState* state) {
|
||||
bool format_stats_send(int fd, const ReceiverStats* stats) {
|
||||
if (!stats)
|
||||
return false;
|
||||
unsigned long long matched = stats->matched_data;
|
||||
unsigned long long deleted = stats->deleted_files;
|
||||
unsigned long long would = stats->would_delete_count;
|
||||
return send_n_data(fd, &matched, sizeof(matched)) && send_n_data(fd, &deleted, sizeof(deleted)) &&
|
||||
send_n_data(fd, &would, sizeof(would));
|
||||
unsigned long long fields[8] = {
|
||||
stats->matched_data, stats->deleted_files, stats->would_delete_count, stats->literal_bytes,
|
||||
stats->created_reg, stats->created_dir, stats->created_link, stats->created_special,
|
||||
};
|
||||
return send_n_data(fd, fields, sizeof(fields));
|
||||
}
|
||||
|
||||
bool format_stats_receive(int fd, ReceiverStats* stats) {
|
||||
if (!stats)
|
||||
return false;
|
||||
unsigned long long matched = 0;
|
||||
unsigned long long deleted = 0;
|
||||
unsigned long long would = 0;
|
||||
if (!receive_n_data(fd, &matched, sizeof(matched)) ||
|
||||
!receive_n_data(fd, &deleted, sizeof(deleted)) || !receive_n_data(fd, &would, sizeof(would)))
|
||||
unsigned long long fields[8] = {0};
|
||||
if (!receive_n_data(fd, fields, sizeof(fields)))
|
||||
return false;
|
||||
memset(stats, 0, sizeof(*stats));
|
||||
stats->matched_data = matched;
|
||||
stats->deleted_files = deleted;
|
||||
stats->would_delete_count = would;
|
||||
stats->matched_data = fields[0];
|
||||
stats->deleted_files = fields[1];
|
||||
stats->would_delete_count = fields[2];
|
||||
stats->literal_bytes = fields[3];
|
||||
stats->created_reg = fields[4];
|
||||
stats->created_dir = fields[5];
|
||||
stats->created_link = fields[6];
|
||||
stats->created_special = fields[7];
|
||||
return true;
|
||||
}
|
||||
|
||||
+16
-4
@@ -57,14 +57,26 @@ bool format_dest_state_send(int fd, const OutputDestState* state);
|
||||
bool format_dest_state_receive(int fd, OutputDestState* state);
|
||||
|
||||
/* End-of-transfer receiver counters reported through STATUS_STATS (protocol
|
||||
* 2.25.0) when the wire config carries report_stats. `would_delete_count` is
|
||||
* the number of destination-relative paths the receiver would have deleted in a
|
||||
* -n/--dry-run --delete run; that many wire strings immediately follow the
|
||||
* fixed record (sent/read by the caller). */
|
||||
* 2.25.0, extended in 2.28.0) when the wire config carries report_stats.
|
||||
* `would_delete_count` is the number of destination-relative paths the receiver
|
||||
* would have deleted in a -n/--dry-run --delete run; that many wire strings
|
||||
* immediately follow the fixed record (sent/read by the caller).
|
||||
*
|
||||
* Protocol 2.28.0 adds the receiver-observed counters the sender cannot see:
|
||||
* `literal_bytes` is the file data the receiver actually stored literally
|
||||
* (whole files plus the literal fragments of a delta) and the four `created_*`
|
||||
* counters split the destination entries the receiver newly created by type,
|
||||
* reproducing rsync's `Number of created files` breakdown and an exact
|
||||
* `Literal data` for a delta run. */
|
||||
typedef struct {
|
||||
unsigned long long matched_data;
|
||||
unsigned long long deleted_files;
|
||||
unsigned long long would_delete_count;
|
||||
unsigned long long literal_bytes;
|
||||
unsigned long long created_reg;
|
||||
unsigned long long created_dir;
|
||||
unsigned long long created_link;
|
||||
unsigned long long created_special;
|
||||
} ReceiverStats;
|
||||
|
||||
/* Fixed-width STATUS_STATS counter record. The status frame and the optional
|
||||
|
||||
Reference in New Issue
Block a user