feat(p7-times): real directory/symlink time preservation; -O/-J meaningful

Wave D of Phase 7. Make -O/--omit-dir-times and -J/--omit-link-times real by
preserving directory and symlink times, and mark --secluded-args as an explicit
Impossible/Divergence no-op.

Wire: PROTOCOL_VERSION 2.16.0 -> 2.17.0. Adds a terminal STATUS_DIR_TIMES frame
(int count + (wire path, metadata) pairs) sent after all file data and the
optional delete manifest. STATUS_MKDIR also carries metadata for --dirs entries.
Config-frame layout is unchanged.

Sender: the recursive scanner captures every traversed source directory (both
DirectoryScanner and the parallel scanner root + workers, appends mutex-guarded)
into a shared list; the single-threaded and -m paths transmit it last.

Receiver: a DirTimeList accumulates received directory metadata and applies it
with fd-relative no-follow utimensat only at the very end -- after all children,
after the commit-style --delete, and after --delay-updates publication -- in the
single-threaded success frame and in server.c after the -m threads join. -O skips
the application. Symlink metadata is applied at link creation with
utimensat/fchownat/fchmodat AT_SYMLINK_NOFOLLOW; -J suppresses only link times.
identity_apply_ownership_link shares the identity resolver with the fd path.

Docs: -O/-J rows -> Implemented; --secluded-args -> Impossible/Divergence;
--protocol accepted/rejected values and Phase-6/7 notes updated.

Tests: unit (scanner dir capture, DirTimeList apply, symlink metadata, protocol
version values) and integration (dir mtime round-trip + -O, symlink mtime
round-trip + -J, independent suppression), parameterized over single/multithread.
This commit is contained in:
2026-09-12 10:31:20 +02:00
parent f34eb34f87
commit f1a447bb4a
23 changed files with 857 additions and 75 deletions
+47
View File
@@ -1,5 +1,6 @@
#include "test_utils.h"
#include "scanner.h"
#include "array_list.h"
#include "file.h"
#include "file_list.h"
#include "filter.h"
@@ -1262,6 +1263,51 @@ static void test_files_from_relative_send_path() {
rmdir(root);
}
/* P7 Wave D: the recursive scan captures every traversed source directory as an
* is_dir File (metadata, no payload) in the shared dir_entries list, including
* the transfer root, so the sender can transmit directory times at the end. */
static void test_scanner_captures_directory_times() {
const char* root = "test_scan_dirtime";
const char* sub = "test_scan_dirtime/sub";
const char* file1 = "test_scan_dirtime/sub/a.txt";
EXPECT_EQ_INT(mkdir(root, 0755), 0);
EXPECT_EQ_INT(mkdir(sub, 0755), 0);
create_test_file(file1, "x");
ArrayList* dirs = array_list_create(file_destroy);
EXPECT_NOT_NULL(dirs);
ScannerOptions options = {0};
options.use_metadata = true;
options.capture_dir_times = true;
options.dir_entries = dirs;
DirectoryScanner* scanner = directory_scanner_create_with_options(root, &options);
EXPECT_NOT_NULL(scanner);
Chunk* chunk;
while ((chunk = directory_scanner_next(scanner)) != NULL)
chunk_destroy(chunk);
EXPECT_FALSE(directory_scanner_failed(scanner));
int found_root = 0;
int found_sub = 0;
for (int i = 0; i < dirs->size; i++) {
const File* file = (const File*)dirs->items[i];
EXPECT_TRUE(file->is_dir);
EXPECT_NOT_NULL(file->metadata);
if (strcmp(file->path, root) == 0)
found_root = 1;
if (strcmp(file->path, sub) == 0)
found_sub = 1;
}
EXPECT_TRUE(found_root);
EXPECT_TRUE(found_sub);
directory_scanner_destroy(scanner);
array_list_delete(dirs);
unlink(file1);
rmdir(sub);
rmdir(root);
}
void test_scanner() {
test_scanner_single_file();
test_scanner_multiple_files();
@@ -1297,4 +1343,5 @@ void test_scanner() {
test_dirs_no_descent();
test_dirs_files_from();
test_files_from_relative_send_path();
test_scanner_captures_directory_times();
}