Release v2.29.0 #312

Merged
TapTap merged 123 commits from dev into main 2026-09-23 02:05:14 +02:00
4 changed files with 96 additions and 1 deletions
Showing only changes of commit 3799200f71 - Show all commits
+9
View File
@@ -2612,6 +2612,15 @@ static int cli_finalize_config(Config* config, bool verbose, bool no_delta, bool
if (config->use_delete && !config->delete_before && !config->delete_during &&
!config->delete_delay && !config->delete_after)
config->delete_during = true;
/* rsync parity: --partial-dir=DIR chooses where an interrupted transfer's
partial file is kept, so it implies --partial. rsync applies the
implication after option parsing, so it wins over an explicit --no-partial
regardless of the order the two options appear in (verified on rsync
3.4.1). --inplace is the exception: the destination file is written in
place with no partial/temp staging, so the partial machinery is bypassed
and the implication is skipped to leave --inplace behavior untouched. */
if (config->partial_dir && !config->inplace)
config->partial = true;
if (config->compress_choice) {
int algo = compression_algo_from_name(config->compress_choice);
if (algo >= 0) {
+1 -1
View File
@@ -288,7 +288,7 @@ void print_usage(void) {
printf(" --log-file <path>, --log-file=<path> Write log messages to file\n");
printf(" --stderr=MODE Route logging to stderr: errors or all\n");
printf(" --partial Keep partial files on interrupted transfer\n");
printf(" --partial-dir <dir> Directory for partial files\n");
printf(" --partial-dir <dir> Directory for partial files (implies --partial)\n");
printf(" -T, --temp-dir <dir> Scratch dir for temp files before atomic install.\n");
printf(" Confined to the receive root: a relative dir resolves below\n");
printf(" it and an absolute/traversal dir is rejected. The dir must\n");
+30
View File
@@ -2275,6 +2275,36 @@ class TestPartialDir:
partial = os.path.join(dest, ".partial", os.path.relpath(source_file, os.path.sep))
assert not os.path.exists(partial)
def test_partial_dir_alone_implies_partial(self, shared_server):
"""--partial-dir=DIR with no --partial implies --partial, like rsync.
rsync 3.4.1 retains the staged partial when --partial-dir is given by
itself; before the implication was added FastSync discarded it. The
transfer is made to fail deterministically by placing a non-empty
directory at the destination path, so the final partial-dir ->
destination rename fails and whatever was staged under the partial dir
stays on disk."""
source = os.path.join(TEST_DATA_DIR, "partial_dir_implied_src")
dest = os.path.join(TEST_DATA_DIR, "partial_dir_implied_dst")
clean_dir(source)
clean_dir(dest)
source_file = os.path.join(source, "f.bin")
with open(source_file, "wb") as f:
f.write(b"partial payload")
received = get_dest_received_dir(dest, source)
os.makedirs(os.path.join(received, "f.bin"))
with open(os.path.join(received, "f.bin", "keep"), "wb") as f:
f.write(b"keep")
result, _ = run_client(source, dest, flags=["--partial-dir=.partial"],
port=shared_server.port)
assert result.returncode != 0, "expected the blocked install to fail"
partial = os.path.join(dest, ".partial", os.path.relpath(source_file, os.path.sep))
assert os.path.exists(partial), \
"--partial-dir alone must imply --partial and retain the partial file"
class TestLargeFile:
def test_transfer_100mb_file(self, shared_server):
+56
View File
@@ -511,6 +511,61 @@ static void test_parse_args_ignore_existing() {
config_delete(cfg);
}
/* --partial-dir=DIR implies --partial, matching rsync 3.4.1. rsync resolves
* this after option parsing, so the implication wins over an explicit
* --no-partial in either order. It is skipped under --inplace, where partial
* staging is bypassed and the destination is written in place. */
static void test_parse_args_partial_dir_implies_partial() {
{
Config* cfg = config_create();
char* argv[] = {"fastsync", "--partial-dir=.partial", "/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->partial);
config_delete(cfg);
}
{
/* Explicit --no-partial before --partial-dir: --partial-dir still wins. */
Config* cfg = config_create();
char* argv[] = {"fastsync", "--no-partial", "--partial-dir=.partial", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->partial);
config_delete(cfg);
}
{
/* Reversed order must not change the precedence. */
Config* cfg = config_create();
char* argv[] = {"fastsync", "--partial-dir=.partial", "--no-partial", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
EXPECT_TRUE(cfg->partial);
config_delete(cfg);
}
{
/* --inplace bypasses partial staging: the implication must not fire. */
Config* cfg = config_create();
char* argv[] = {"fastsync", "--inplace", "--partial-dir=.partial", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 5, argv, positional_args, &positional_count), 0);
EXPECT_FALSE(cfg->partial);
config_delete(cfg);
}
{
Config* cfg = config_create();
char* argv[] = {"fastsync", "--no-partial", "/src", "/dst"};
int positional_args[2];
int positional_count = 0;
EXPECT_EQ_INT(parse_args(cfg, 4, argv, positional_args, &positional_count), 0);
EXPECT_FALSE(cfg->partial);
config_delete(cfg);
}
}
static void test_parse_args_executability() {
Config* cfg = config_create();
char* argv[] = {"fastsync", "-E", "/src", "/dst"};
@@ -4784,6 +4839,7 @@ void test_client_cli() {
test_parse_args_valid_port();
test_parse_args_size_only();
test_parse_args_ignore_existing();
test_parse_args_partial_dir_implies_partial();
test_parse_args_executability();
test_parse_args_chmod();
test_parse_args_numeric_chmod();