feat(delete): default --delete to rsync delete-during; add --delete-commit
- plain --delete with no timing flag now selects delete-during (progressive deletion, matching rsync and avoiding the full old+new tree peak) - new long-only FastSync --delete-commit restores the old atomic behavior (delete only after the whole transfer succeeds); timing-identical to --delete-after, implemented via the same wire bool - timing flags are mutually exclusive; --delete-commit conflicts with other timings; --delete-before/--delete-during rows reworded per Phase-0 probes - CHANGELOG migration note; tally unchanged 116/14/27
This commit is contained in:
+21
-1
@@ -997,6 +997,12 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
{"--delete-during", "--del", OPT_FLAG, offsetof(Config, delete_during)},
|
||||
{"--delete-delay", NULL, OPT_FLAG, offsetof(Config, delete_delay)},
|
||||
{"--delete-after", NULL, OPT_FLAG, offsetof(Config, delete_after)},
|
||||
/* FastSync-only long spelling of the late whole-tree commit, which selects
|
||||
the same timing as rsync's --delete-after in FastSync (the whole-tree
|
||||
keep-set manifest is committed only after the entire transfer succeeded).
|
||||
Plain --delete now defaults to delete-during, so this restores the old
|
||||
FastSync behavior; it maps onto the same delete_after wire field. */
|
||||
{"--delete-commit", NULL, OPT_FLAG, offsetof(Config, delete_after)},
|
||||
{"--delete-excluded", NULL, OPT_FLAG, offsetof(Config, delete_excluded)},
|
||||
{"--max-delete", NULL, OPT_SIGNED_INT, offsetof(Config, max_delete)},
|
||||
{"--ignore-errors", NULL, OPT_FLAG, offsetof(Config, ignore_errors)},
|
||||
@@ -1513,7 +1519,9 @@ static bool cli_handle_table_option(CliParseCtx* ctx) {
|
||||
if (entry->offset == offsetof(Config, per_dir_filter) && config->per_dir_filter_count < INT_MAX)
|
||||
config->per_dir_filter_count++;
|
||||
/* A delete-timing flag selects when --delete removes extras, so it
|
||||
implies --delete exactly like the rsync options do. */
|
||||
implies --delete exactly like the rsync options do. --delete-commit (the
|
||||
FastSync-only late-commit spelling) is mapped onto delete_after and so is
|
||||
covered here too. */
|
||||
if (entry->offset == offsetof(Config, delete_before) ||
|
||||
entry->offset == offsetof(Config, delete_during) ||
|
||||
entry->offset == offsetof(Config, delete_delay) ||
|
||||
@@ -2567,6 +2575,18 @@ static bool cli_handle_outbuf_option(CliParseCtx* ctx) {
|
||||
* -1 on error. */
|
||||
static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool no_incremental) {
|
||||
set_log_level(config->quiet ? LOG_LEVEL_ERROR : (verbose ? LOG_LEVEL_DEBUG : LOG_LEVEL_WARNING));
|
||||
/* rsync's plain --delete defaults to delete-during (--del): each directory's
|
||||
extras are removed as that directory is processed, so space is freed
|
||||
progressively and a tight destination never has to hold the whole old+new
|
||||
tree at once. The late whole-tree commit FastSync historically used is
|
||||
still selected explicitly by --delete-after or by the FastSync-only long
|
||||
spelling --delete-commit (an exact alias for --delete-after). Resolve the
|
||||
default on the client, before validation and before the config crosses the
|
||||
wire, so exactly one timing flag is ever set; an explicit timing (including
|
||||
--delete-commit) always wins. */
|
||||
if (config->use_delete && !config->delete_before && !config->delete_during &&
|
||||
!config->delete_delay && !config->delete_after)
|
||||
config->delete_during = true;
|
||||
if (config->compress_choice) {
|
||||
int algo = compression_algo_from_name(config->compress_choice);
|
||||
if (algo >= 0) {
|
||||
|
||||
@@ -3287,7 +3287,8 @@ int send_files(Config* config) {
|
||||
prepared.options.synced_dirs = synced_dirs;
|
||||
}
|
||||
}
|
||||
/* The late-timing modes (plain --delete / --delete-after) build the manifest
|
||||
/* The late-timing modes (--delete-after/--delete-commit and a plain --delete
|
||||
that fell back from per-dir mode because of -d/--dirs) build the manifest
|
||||
while streaming and send it after the last data frame. --delete-before
|
||||
sends a whole-tree keep-set up front; --delete-during/--delete-delay build a
|
||||
per-directory plan set up front (paths only) and stream the plans alongside
|
||||
|
||||
+6
-4
@@ -62,8 +62,7 @@ void print_usage(void) {
|
||||
printf(" NOTE: the FastSync batch format is NOT interoperable with rsync's batch\n");
|
||||
printf(" files (different container format); do not mix the two tools.\n");
|
||||
printf(" --delete Delete files on receiver not in source\n");
|
||||
printf(" (default timing: delete only after the whole\n");
|
||||
printf(" transfer has succeeded)\n");
|
||||
printf(" (default timing: delete-during, like rsync --del)\n");
|
||||
printf(" --delete-before Delete extras before the transfer starts\n");
|
||||
printf(" (implies --delete)\n");
|
||||
printf(" --delete-during Delete a directory's extras as that directory is\n");
|
||||
@@ -72,7 +71,9 @@ void print_usage(void) {
|
||||
printf(" --delete-delay Record the extras during the scan but remove them\n");
|
||||
printf(" only after a successful transfer (implies --delete)\n");
|
||||
printf(" --delete-after Delete only after the whole transfer succeeded\n");
|
||||
printf(" (the default --delete timing; implies --delete)\n");
|
||||
printf(" (implies --delete)\n");
|
||||
printf(" --delete-commit FastSync-only: restore the late whole-tree commit\n");
|
||||
printf(" (identical to --delete-after; implies --delete)\n");
|
||||
printf(" --delete-excluded Also delete destination files that were excluded on\n");
|
||||
printf(" the source (default protects them, matching rsync)\n");
|
||||
printf(" --max-delete=NUM Delete at most NUM destination entries per run; if the\n");
|
||||
@@ -93,7 +94,8 @@ void print_usage(void) {
|
||||
printf(" -m, --prune-empty-dirs Do not create empty directories (a recursive transfer\n");
|
||||
printf(" otherwise recreates them, like rsync)\n");
|
||||
printf(" Note: each timing flag implies --delete. Combining a timing flag with\n");
|
||||
printf(" --no-delete (in either order) is rejected as a config error.\n");
|
||||
printf(" --no-delete (in either order) is rejected as a config error, as is more\n");
|
||||
printf(" than one timing flag.\n");
|
||||
printf(" --ignore-existing Skip files that already exist on receiver\n");
|
||||
printf(" --delay-updates Put updated files into place only at the end of transfer\n");
|
||||
printf(" --dirs, -d, --old-dirs, --old-d Transfer the named directory entries without\n");
|
||||
|
||||
@@ -305,14 +305,16 @@ int receiver_process(Config* config, int file_descriptor, const ReceiverSink* si
|
||||
|
||||
/* Runs the whole receive loop. The delete manifest may legitimately arrive
|
||||
either FIRST (--delete-before / --delete-during: the sender transmits the
|
||||
validated keep-set before any file data) or LAST (plain --delete /
|
||||
--delete-after / --delete-delay: the manifest closes the data stream). In
|
||||
validated keep-set before any file data) or LAST (--delete-after /
|
||||
--delete-commit / --delete-delay: the manifest closes the data stream). In
|
||||
the early modes the receiver deletes as soon as the manifest has been read
|
||||
and acknowledges with STATUS_OK so the sender only starts streaming once the
|
||||
deletion has committed (or failed); in the late modes the manifest is held
|
||||
and the deletion is committed only after the terminal STATUS_FINISHED proves
|
||||
the whole transfer succeeded. See receiver_process_pending() for how the -m
|
||||
receiver defers that commit until its disk writer has drained. */
|
||||
the whole transfer succeeded. A plain --delete defaults to the per-directory
|
||||
delete-during plan mode (no manifest at all). See
|
||||
receiver_process_pending() for how the -m receiver defers that commit until
|
||||
its disk writer has drained. */
|
||||
int receiver_process_pending(Config* config, int file_descriptor, const ReceiverSink* sink,
|
||||
DeleteManifest** pending_manifest, DeletePlanSession** pending_plans) {
|
||||
Status status;
|
||||
|
||||
+13
-6
@@ -642,10 +642,14 @@ typedef struct Config {
|
||||
source directory is streamed in directory order, and the receiver removes
|
||||
each directory's extras when its plan arrives (during) or snapshots them
|
||||
and removes them only after a successful transfer (delay). delete_after
|
||||
(and plain --delete) keep the whole-tree commit mode: extras are removed
|
||||
from a fresh end-of-transfer destination scan only after the whole transfer
|
||||
succeeded. See config_delete_timing_early()/config_delete_timing_per_dir()
|
||||
below. */
|
||||
keeps the whole-tree commit mode: extras are removed from a fresh
|
||||
end-of-transfer destination scan only after the whole transfer succeeded.
|
||||
A plain --delete with no explicit timing flag defaults to delete_during on
|
||||
the client (cli_finalize_config), matching rsync's --del default; the old
|
||||
late-commit behavior is selected explicitly by --delete-after or the
|
||||
FastSync-only long spelling --delete-commit (an exact alias for
|
||||
--delete-after, mapped onto the same wire field). See
|
||||
config_delete_timing_early()/config_delete_timing_per_dir() below. */
|
||||
/* partial_dir */
|
||||
// PR #174: Partial transfer resumption
|
||||
/* suffix */
|
||||
@@ -1168,8 +1172,11 @@ bool config_delete_timing_early(const Config* config);
|
||||
* commits them only after a fully-successful transfer (delay). */
|
||||
bool config_delete_timing_per_dir(const Config* config);
|
||||
/* Delete-timing sanity: with deletion enabled at most one timing flag may be
|
||||
* set (none = the default delete-after commit timing); without deletion no
|
||||
* timing flag may be set (each timing flag implies --delete). */
|
||||
* set; without deletion no timing flag may be set (each timing flag implies
|
||||
* --delete). A plain --delete is normalized to delete_during by
|
||||
* cli_finalize_config on the client, so a transmitted use_delete config always
|
||||
* carries exactly one timing; the zero-timing case remains valid only for a
|
||||
* config that has not been through the CLI. */
|
||||
bool config_has_valid_delete_timing(const Config* config);
|
||||
|
||||
/* Single source of truth for the cross-field ("combination") invariants a
|
||||
|
||||
@@ -331,6 +331,8 @@ static int send_plan_node(int fd, DeletePlanSender* sender, PlanNode* node) {
|
||||
return -1;
|
||||
sender->config_sent = true;
|
||||
}
|
||||
if (!send_int(fd, 1)) /* apply = true */
|
||||
return -1;
|
||||
if (!send_wire_str(fd, node->dir))
|
||||
return -1;
|
||||
if (send_str_section(fd, node->dirs) != 0 || send_str_section(fd, node->files) != 0)
|
||||
@@ -339,6 +341,29 @@ static int send_plan_node(int fd, DeletePlanSender* sender, PlanNode* node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Transmit the one-shot per-run config block (protected prefixes, size-pruned
|
||||
* mirrors, --delete-missing-args exact paths) on its own carrier frame, with
|
||||
* apply=false so the receiver consumes the config but walks nothing. This is
|
||||
* how the config still reaches the receiver when the scope allows no directory
|
||||
* plan at all (a --files-from list of bare files synchronizes no directory):
|
||||
* without it, the missing-args exact deletions would be lost. Idempotent. */
|
||||
static int send_config_only(int fd, DeletePlanSender* sender) {
|
||||
if (!sender || sender->config_sent)
|
||||
return 0;
|
||||
if (!send_status(fd, STATUS_DELETE_PLAN) || !send_int(fd, 1))
|
||||
return -1;
|
||||
if (send_str_section(fd, sender->protected_prefixes) != 0 ||
|
||||
send_str_section(fd, sender->size_skipped) != 0 ||
|
||||
send_str_section(fd, sender->missing_args) != 0)
|
||||
return -1;
|
||||
sender->config_sent = true;
|
||||
if (!send_int(fd, 0)) /* apply = false */
|
||||
return -1;
|
||||
if (!send_wire_str(fd, ".") || !send_int(fd, 0) || !send_int(fd, 0))
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int send_prefix_plan(int fd, DeletePlanSender* sender, const char* dir) {
|
||||
PlanNode* node = plan_find(sender, dir);
|
||||
if (!node || node->sent)
|
||||
@@ -354,6 +379,10 @@ int delete_plan_send_root(int fd, DeletePlanSender* sender) {
|
||||
const char* root = sender->walk_root ? sender->walk_root : ".";
|
||||
if (!plan_ensure(sender, root))
|
||||
return -1;
|
||||
/* Put the config block on the wire first, on its own carrier frame, so the
|
||||
receiver always sees it even when the scope permits no directory plan. */
|
||||
if (send_config_only(fd, sender) != 0)
|
||||
return -1;
|
||||
return send_prefix_plan(fd, sender, root);
|
||||
}
|
||||
|
||||
@@ -850,6 +879,14 @@ int delete_plan_session_receive(DeletePlanSession* session, const Config* config
|
||||
}
|
||||
session->config_seen = true;
|
||||
}
|
||||
/* apply=false is the config-only carrier frame: the receiver consumes the
|
||||
config (and the missing-args exact deletions) but must not walk any
|
||||
directory. Every real plan carries apply=true. */
|
||||
int apply;
|
||||
if (!receive_int(fd, &apply) || (apply != 0 && apply != 1)) {
|
||||
send_status(fd, STATUS_ERROR);
|
||||
return -1;
|
||||
}
|
||||
char* dir = receive_wire_str(fd);
|
||||
ArrayList* dirs = array_list_create(free);
|
||||
ArrayList* files = array_list_create(free);
|
||||
@@ -867,7 +904,7 @@ int delete_plan_session_receive(DeletePlanSession* session, const Config* config
|
||||
if (!session->dry_run && enabled) {
|
||||
if (!session->defer && !apply_missing(session, config))
|
||||
ok = false;
|
||||
if (ok && !apply_plan_dir(session, config, dir, dirs, files))
|
||||
if (ok && apply && !apply_plan_dir(session, config, dir, dirs, files))
|
||||
ok = false;
|
||||
}
|
||||
free(dir);
|
||||
|
||||
@@ -48,11 +48,14 @@ void delete_plan_sender_finalize(DeletePlanSender* sender, const ArrayList* sync
|
||||
Directory keep entries do not count, so an I/O error that hid every file
|
||||
still refuses to delete. */
|
||||
bool delete_plan_sender_empty(const DeletePlanSender* sender);
|
||||
/* Attach the global config sections advertised on the first plan frame. */
|
||||
/* Attach the global config sections advertised on the first plan frame. The
|
||||
* block is always transmitted by delete_plan_send_root(), on a config-only
|
||||
* carrier frame when the scope allows no directory plan. */
|
||||
void delete_plan_sender_set_config(DeletePlanSender* sender, const ArrayList* protected_prefixes,
|
||||
const ArrayList* size_skipped, const ArrayList* missing_args);
|
||||
/* Send the root plan (even before any data, so root extras are handled like
|
||||
* rsync's first generator directory). Returns -1 on I/O error. */
|
||||
* rsync's first generator directory), after transmitting the per-run config
|
||||
* block on its own carrier frame. Returns -1 on I/O error. */
|
||||
int delete_plan_send_root(int fd, DeletePlanSender* sender);
|
||||
/* Send the plans for every ancestor of `path` (root-first) and, when is_dir,
|
||||
* for `path` itself; already-sent plans are skipped. */
|
||||
|
||||
@@ -191,7 +191,9 @@ enum NET_STATUS {
|
||||
* (--delete-delay). Payload: an int32 has_config flag (1 on the first plan
|
||||
* of the run, 0 afterwards); when set, the three global config sections
|
||||
* (protected-prefix count+paths, size-skipped count+paths, missing-args
|
||||
* count+paths); then the destination-relative directory path wire string
|
||||
* count+paths); then an int32 apply flag (1 for a real plan, 0 for a
|
||||
* config-only carrier frame that must not walk a directory); then the
|
||||
* destination-relative directory path wire string
|
||||
* ("." for the receive root); then the child-directory count + names and the
|
||||
* child-file count + names that must be kept. Appended after
|
||||
* STATUS_DEST_INFO so no existing status is renumbered. */
|
||||
|
||||
Reference in New Issue
Block a user