feat(parity): -d/--dirs one-level listing for dir/, dir/. and .
A trailing slash (or trailing '/.', or a bare '.') now lists the source's immediate contents -- files transferred, subdirectories created empty -- without recursing, while a bare directory still sends only its own entry. The -R prefix applies to the generated entries and to the root entry.
This commit is contained in:
+77
-6
@@ -906,18 +906,20 @@ static int open_next_directory(DirectoryScanner* scanner) {
|
|||||||
/* ---- --dirs mode ----
|
/* ---- --dirs mode ----
|
||||||
With -d the scanner transfers directory entries and never recurses into
|
With -d the scanner transfers directory entries and never recurses into
|
||||||
contents. A plain `-d <dir>` sends only the source-root directory mirror
|
contents. A plain `-d <dir>` sends only the source-root directory mirror
|
||||||
(created empty at the destination). With -d + --files-from exactly the
|
(created empty at the destination); `-d dir/`, `-d dir/.` and `-d .` list
|
||||||
listed items are sent: listed directories become empty directory entries and
|
the directory's immediate contents instead (files plus empty directory
|
||||||
listed regular files are transferred as files; nothing else is scanned, so
|
entries), matching rsync. With -d + --files-from exactly the listed items
|
||||||
no descent into a listed directory can happen. */
|
are sent: listed directories become empty directory entries and listed
|
||||||
|
regular files are transferred as files; nothing else is scanned, so no
|
||||||
|
descent into a listed directory can happen. */
|
||||||
|
|
||||||
/* Directory entries carry no payload, so the dirs generator also bounds every
|
/* Directory entries carry no payload, so the dirs generator also bounds every
|
||||||
chunk by element count; chunk_deserialize refuses more than this many files
|
chunk by element count; chunk_deserialize refuses more than this many files
|
||||||
per chunk (see MAX_FILES_PER_CHUNK in chunk.c). */
|
per chunk (see MAX_FILES_PER_CHUNK in chunk.c). */
|
||||||
#define DIRS_CHUNK_MAX_FILES 65536U
|
#define DIRS_CHUNK_MAX_FILES 65536U
|
||||||
|
|
||||||
/* Build the File for the transfer root directory itself (the `-d <dir>` and
|
/* Build the File for the transfer root directory itself (the `-d <dir>`
|
||||||
* "." cases). */
|
* no-trailing-slash case). */
|
||||||
static File* dirs_root_dir_file(DirectoryScanner* scanner) {
|
static File* dirs_root_dir_file(DirectoryScanner* scanner) {
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (stat(scanner->root_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
if (stat(scanner->root_path, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||||
@@ -940,6 +942,14 @@ static File* dirs_root_dir_file(DirectoryScanner* scanner) {
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (scanner->options.relative_prefix && scanner->options.relative_prefix[0] != '\0') {
|
||||||
|
file->send_path = str_dup(scanner->options.relative_prefix);
|
||||||
|
if (!file->send_path) {
|
||||||
|
file_destroy(file);
|
||||||
|
scanner->failed = true;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
scanner_capture_xattrs(scanner, file);
|
scanner_capture_xattrs(scanner, file);
|
||||||
return file;
|
return file;
|
||||||
}
|
}
|
||||||
@@ -1044,6 +1054,13 @@ static File* dirs_file_for_entry(DirectoryScanner* scanner, const char* entry) {
|
|||||||
scanner->failed = true;
|
scanner->failed = true;
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
} else if (scanner->options.relative_prefix) {
|
||||||
|
file->send_path = scanner_prefix_send_path(scanner->options.relative_prefix, entry);
|
||||||
|
if (!file->send_path) {
|
||||||
|
file_destroy(file);
|
||||||
|
scanner->failed = true;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (scanner->options.use_metadata) {
|
if (scanner->options.use_metadata) {
|
||||||
file->metadata = file_metadata_create(file->path, &effective, scanner->options.preserve_atimes,
|
file->metadata = file_metadata_create(file->path, &effective, scanner->options.preserve_atimes,
|
||||||
@@ -1077,9 +1094,63 @@ static bool dirs_source_dir_is_empty(const char* path) {
|
|||||||
return empty;
|
return empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* The next immediate child of the source root for a one-level --dirs listing
|
||||||
|
* (rsync: -d DIR/ lists DIR's immediate contents without recursing). */
|
||||||
|
static File* dirs_next_child(DirectoryScanner* scanner) {
|
||||||
|
if (!scanner->current_dir)
|
||||||
|
return NULL;
|
||||||
|
const struct dirent* entry;
|
||||||
|
while ((entry = readdir(scanner->current_dir)) != NULL) {
|
||||||
|
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||||
|
continue;
|
||||||
|
File* file = dirs_file_for_entry(scanner, entry->d_name);
|
||||||
|
if (scanner->failed)
|
||||||
|
return NULL;
|
||||||
|
if (file && !entry_passes_selection(scanner->options.file_list, scanner->options.base_filters,
|
||||||
|
NULL, entry->d_name, entry->d_name, file->is_dir,
|
||||||
|
scanner->options.per_dir_filters)) {
|
||||||
|
file_destroy(file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file && file->is_dir && scanner->options.prune_empty_dirs &&
|
||||||
|
dirs_source_dir_is_empty(file->path)) {
|
||||||
|
file_destroy(file);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (file)
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
closedir(scanner->current_dir);
|
||||||
|
scanner->current_dir = NULL;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
/* The next File from the --dirs generator, or NULL when exhausted. */
|
/* The next File from the --dirs generator, or NULL when exhausted. */
|
||||||
static File* dirs_next_file(DirectoryScanner* scanner) {
|
static File* dirs_next_file(DirectoryScanner* scanner) {
|
||||||
if (!scanner->options.file_list) {
|
if (!scanner->options.file_list) {
|
||||||
|
const char* spec = scanner->root_path ? scanner->root_path : "";
|
||||||
|
size_t n = strlen(spec);
|
||||||
|
/* rsync: a trailing slash or "/." on the source argument lists the
|
||||||
|
directory's immediate contents (files and empty directory entries)
|
||||||
|
without recursing. A bare directory sends only its own entry. */
|
||||||
|
bool list_children =
|
||||||
|
(n == 1 && spec[0] == '.') ||
|
||||||
|
(n > 0 && (spec[n - 1] == '/' || (n >= 2 && spec[n - 1] == '.' && spec[n - 2] == '/')));
|
||||||
|
if (list_children) {
|
||||||
|
if (!scanner->dirs_root_emitted) {
|
||||||
|
scanner->dirs_root_emitted = true;
|
||||||
|
if (scanner->options.prune_empty_dirs && dirs_source_dir_is_empty(scanner->root_path))
|
||||||
|
return NULL;
|
||||||
|
scanner->current_dir = opendir(scanner->root_path);
|
||||||
|
if (!scanner->current_dir) {
|
||||||
|
scanner->io_error = true;
|
||||||
|
log_perror("Could not open directory");
|
||||||
|
scanner->failed = true;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dirs_next_child(scanner);
|
||||||
|
}
|
||||||
if (scanner->dirs_root_emitted)
|
if (scanner->dirs_root_emitted)
|
||||||
return NULL;
|
return NULL;
|
||||||
scanner->dirs_root_emitted = true;
|
scanner->dirs_root_emitted = true;
|
||||||
|
|||||||
@@ -105,6 +105,43 @@ class TestRelativeGeneral:
|
|||||||
f"mtime mismatch for {rel} with {extra}"
|
f"mtime mismatch for {rel} with {extra}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestDirsOneLevel:
|
||||||
|
"""#13: -d with a trailing slash (or '.') lists the source's immediate
|
||||||
|
contents; FastSync mirrors them below the source-root mirror, so compare
|
||||||
|
rsync's destination tree against that mirror."""
|
||||||
|
|
||||||
|
@requires_rsync
|
||||||
|
@pytest.mark.ci
|
||||||
|
def test_dirs_trailing_slash_matches_rsync(self, shared_server):
|
||||||
|
source = _make_tree(os.path.join(TEST_DATA_DIR, "sel_dirs_src"))
|
||||||
|
os.makedirs(os.path.join(source, "empty"), exist_ok=True)
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, "sel_dirs_dst")
|
||||||
|
rdst = os.path.join(TEST_DATA_DIR, "sel_dirs_rdst")
|
||||||
|
clean_dir(dest)
|
||||||
|
clean_dir(rdst)
|
||||||
|
r = _rsync(["-d", source + "/", rdst + "/"])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
result, _ = run_client(source + "/", dest, flags=["-d"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, result.stderr[:300]
|
||||||
|
mirror = get_dest_received_dir(dest, source)
|
||||||
|
assert _tree(rdst) == _tree(mirror)
|
||||||
|
|
||||||
|
@requires_rsync
|
||||||
|
@pytest.mark.ci
|
||||||
|
def test_dirs_relative_matches_rsync(self, shared_server):
|
||||||
|
source = _make_tree(os.path.join(TEST_DATA_DIR, "sel_dirsr_src"))
|
||||||
|
dest = os.path.join(TEST_DATA_DIR, "sel_dirsr_dst")
|
||||||
|
rdst = os.path.join(TEST_DATA_DIR, "sel_dirsr_rdst")
|
||||||
|
clean_dir(dest)
|
||||||
|
clean_dir(rdst)
|
||||||
|
spec = source + "/./foo"
|
||||||
|
r = _rsync(["-d", "-R", spec, rdst + "/"])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
result, _ = run_client(spec, dest, flags=["-d", "-R"], port=shared_server.port)
|
||||||
|
assert result.returncode == 0, result.stderr[:300]
|
||||||
|
assert _tree(rdst) == _tree(dest)
|
||||||
|
|
||||||
|
|
||||||
class TestClientAliases:
|
class TestClientAliases:
|
||||||
"""#5: safe rsync option aliases accepted client-side."""
|
"""#5: safe rsync option aliases accepted client-side."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user