feat: add -x/--one-file-system to stay within the source filesystem
Capture the transfer root's device (st_dev) at scanner creation and skip descending into any subdirectory on a different device (a mount point). Implemented sender/client-side only: sequential BFS and parallel (-m) root scan apply the same scanner_same_filesystem decision; no wire/protocol change and default behavior is unchanged. Unit tests cover the pure decision, same device scanning in both modes, and CLI parsing; integration tests prove -x leaves a single-filesystem tree byte-identical and, when root can mount a tmpfs, skips a genuine cross-device subtree.
This commit is contained in:
@@ -416,6 +416,7 @@ static const OptionEntry OPTION_TABLE[] = {
|
||||
|
||||
{"--max-size", NULL, OPT_ULL, offsetof(Config, max_size)},
|
||||
{"--min-size", NULL, OPT_ULL, offsetof(Config, min_size)},
|
||||
{"--one-file-system", "-x", OPT_FLAG, offsetof(Config, one_file_system)},
|
||||
};
|
||||
|
||||
/* Only boolean options with no required argument are safe to negate. */
|
||||
|
||||
@@ -40,12 +40,14 @@ static const char* display_bytes(unsigned long long bytes, bool human_readable,
|
||||
}
|
||||
|
||||
static ScannerOptions scanner_options_from_config(const Config* config, int num_threads) {
|
||||
ScannerOptions options = {
|
||||
config->use_metadata, config->chunk_size, config->exclude_patterns,
|
||||
config->exclude_count, config->include_patterns, config->include_count,
|
||||
config->max_size, config->min_size, config->max_depth,
|
||||
num_threads, config->follow_symlinks, config->copy_links,
|
||||
config->safe_links, config->copy_unsafe_links, config->checksum};
|
||||
ScannerOptions options = {config->use_metadata, config->chunk_size,
|
||||
config->exclude_patterns, config->exclude_count,
|
||||
config->include_patterns, config->include_count,
|
||||
config->max_size, config->min_size,
|
||||
config->max_depth, num_threads,
|
||||
config->follow_symlinks, config->copy_links,
|
||||
config->safe_links, config->copy_unsafe_links,
|
||||
config->checksum, config->one_file_system};
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
+53
-10
@@ -59,6 +59,13 @@ typedef struct {
|
||||
bool is_directory;
|
||||
} ScannerEntry;
|
||||
|
||||
/* --one-file-system (-x) decision. Only directories can carry a different
|
||||
* device than their parent (mount points), so this is checked when a child
|
||||
* directory is about to be descended into. */
|
||||
bool scanner_same_filesystem(bool one_file_system, dev_t root_device, dev_t entry_device) {
|
||||
return !one_file_system || entry_device == root_device;
|
||||
}
|
||||
|
||||
/* Inspect symlinks, resolve the entry type, and apply file filters once for both scanners. */
|
||||
static int scanner_inspect_entry(const ScannerOptions* options, const char* source_root,
|
||||
const char* containing_dir, const char* name,
|
||||
@@ -156,7 +163,18 @@ DirectoryScanner* directory_scanner_create_with_options(const char* root_directo
|
||||
scanner->safe_links = options->safe_links;
|
||||
scanner->copy_unsafe_links = options->copy_unsafe_links;
|
||||
scanner->checksum = options->checksum;
|
||||
scanner->one_file_system = options->one_file_system;
|
||||
scanner->failed = false;
|
||||
if (scanner->one_file_system) {
|
||||
struct stat root_stats;
|
||||
if (stat(root_directory, &root_stats) != 0) {
|
||||
log_perror("Could not stat source directory");
|
||||
queue_destroy(scanner->directories);
|
||||
free(scanner);
|
||||
return NULL;
|
||||
}
|
||||
scanner->root_dev = root_stats.st_dev;
|
||||
}
|
||||
DirEntry* root = dir_entry_create(root_directory, 0);
|
||||
if (!root) {
|
||||
queue_destroy(scanner->directories);
|
||||
@@ -179,10 +197,14 @@ DirectoryScanner* directory_scanner_create(const char* root_directory, bool use_
|
||||
unsigned long long min_size, int max_depth,
|
||||
bool follow_symlinks, bool copy_links, bool safe_links,
|
||||
bool copy_unsafe_links, bool checksum) {
|
||||
ScannerOptions options = {
|
||||
use_metadata, chunk_size, exclude_patterns, exclude_count, include_patterns,
|
||||
include_count, max_size, min_size, max_depth, 0,
|
||||
follow_symlinks, copy_links, safe_links, copy_unsafe_links, checksum};
|
||||
ScannerOptions options = {use_metadata, chunk_size,
|
||||
exclude_patterns, exclude_count,
|
||||
include_patterns, include_count,
|
||||
max_size, min_size,
|
||||
max_depth, 0,
|
||||
follow_symlinks, copy_links,
|
||||
safe_links, copy_unsafe_links,
|
||||
checksum, false};
|
||||
return directory_scanner_create_with_options(root_directory, &options);
|
||||
}
|
||||
|
||||
@@ -272,7 +294,7 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
scanner->max_depth, 0,
|
||||
scanner->follow_symlinks, scanner->copy_links,
|
||||
scanner->safe_links, scanner->copy_unsafe_links,
|
||||
scanner->checksum};
|
||||
scanner->checksum, scanner->one_file_system};
|
||||
ScannerEntry inspected;
|
||||
int inspection = scanner_inspect_entry(&options, scanner->current_path, scanner->current_path,
|
||||
entry->d_name, &inspected);
|
||||
@@ -286,6 +308,10 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner) {
|
||||
struct stat stats = inspected.stats;
|
||||
|
||||
if (inspected.is_directory) {
|
||||
if (!scanner_same_filesystem(scanner->one_file_system, scanner->root_dev, stats.st_dev)) {
|
||||
free(cur_path);
|
||||
continue;
|
||||
}
|
||||
int next_depth = scanner->current_depth + 1;
|
||||
if (scanner->max_depth <= 0 || next_depth < scanner->max_depth) {
|
||||
DirEntry* de = dir_entry_create(cur_path, next_depth);
|
||||
@@ -525,7 +551,7 @@ static Chunk* batch_files(ArrayList* files, unsigned long long chunk_size, Queue
|
||||
/* Scan one root-directory entry into either the subdirs or files list. */
|
||||
static void scan_root_entry(const ScannerOptions* options, const char* root_directory,
|
||||
const struct dirent* entry, ArrayList* root_files, ArrayList* subdirs,
|
||||
ParallelScanner* ps) {
|
||||
dev_t root_dev, ParallelScanner* ps) {
|
||||
ScannerEntry inspected;
|
||||
int inspection =
|
||||
scanner_inspect_entry(options, root_directory, root_directory, entry->d_name, &inspected);
|
||||
@@ -538,6 +564,10 @@ static void scan_root_entry(const ScannerOptions* options, const char* root_dire
|
||||
char* cur_path = inspected.path;
|
||||
struct stat st = inspected.stats;
|
||||
if (inspected.is_directory) {
|
||||
if (!scanner_same_filesystem(options->one_file_system, root_dev, st.st_dev)) {
|
||||
free(cur_path);
|
||||
return;
|
||||
}
|
||||
if (!array_list_add(subdirs, cur_path)) {
|
||||
free(cur_path);
|
||||
ps->failed = true;
|
||||
@@ -567,8 +597,8 @@ static void scan_root_entry(const ScannerOptions* options, const char* root_dire
|
||||
/* Scan the root directory itself, collecting root files and subdirectories.
|
||||
* Returns false if the root directory could not be opened. */
|
||||
static bool scan_root_directory(ParallelScanner* ps, const char* root_directory,
|
||||
const ScannerOptions* options, ArrayList* root_files,
|
||||
ArrayList* subdirs) {
|
||||
const ScannerOptions* options, dev_t root_dev,
|
||||
ArrayList* root_files, ArrayList* subdirs) {
|
||||
DIR* dir = opendir(root_directory);
|
||||
if (!dir) {
|
||||
log_perror("Could not open root directory for parallel scan");
|
||||
@@ -578,7 +608,7 @@ static bool scan_root_directory(ParallelScanner* ps, const char* root_directory,
|
||||
while ((entry = readdir(dir)) != NULL) {
|
||||
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
|
||||
continue;
|
||||
scan_root_entry(options, root_directory, entry, root_files, subdirs, ps);
|
||||
scan_root_entry(options, root_directory, entry, root_files, subdirs, root_dev, ps);
|
||||
}
|
||||
closedir(dir);
|
||||
return true;
|
||||
@@ -677,7 +707,20 @@ ParallelScanner* parallel_scanner_create_with_options(const char* root_directory
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (!scan_root_directory(ps, root_directory, options, root_files, subdirs)) {
|
||||
dev_t root_dev = 0;
|
||||
if (options->one_file_system) {
|
||||
struct stat root_stats;
|
||||
if (stat(root_directory, &root_stats) != 0) {
|
||||
log_perror("Could not stat source directory");
|
||||
array_list_delete(root_files);
|
||||
array_list_delete(subdirs);
|
||||
parallel_scanner_destroy(ps);
|
||||
return NULL;
|
||||
}
|
||||
root_dev = root_stats.st_dev;
|
||||
}
|
||||
|
||||
if (!scan_root_directory(ps, root_directory, options, root_dev, root_files, subdirs)) {
|
||||
array_list_delete(root_files);
|
||||
array_list_delete(subdirs);
|
||||
parallel_scanner_destroy(ps);
|
||||
|
||||
+10
-1
@@ -6,8 +6,9 @@
|
||||
#include "queue.h"
|
||||
#include <dirent.h>
|
||||
#include <stdbool.h>
|
||||
#include <threads.h>
|
||||
#include <stdatomic.h>
|
||||
#include <sys/types.h>
|
||||
#include <threads.h>
|
||||
|
||||
typedef struct {
|
||||
bool use_metadata;
|
||||
@@ -25,6 +26,7 @@ typedef struct {
|
||||
bool safe_links;
|
||||
bool copy_unsafe_links;
|
||||
bool checksum;
|
||||
bool one_file_system;
|
||||
} ScannerOptions;
|
||||
|
||||
typedef struct {
|
||||
@@ -46,6 +48,8 @@ typedef struct {
|
||||
bool safe_links;
|
||||
bool copy_unsafe_links;
|
||||
bool checksum;
|
||||
bool one_file_system;
|
||||
dev_t root_dev;
|
||||
bool failed;
|
||||
} DirectoryScanner;
|
||||
|
||||
@@ -79,6 +83,11 @@ Chunk* directory_scanner_next(DirectoryScanner* scanner);
|
||||
bool directory_scanner_failed(const DirectoryScanner* scanner);
|
||||
void directory_scanner_destroy(DirectoryScanner* scanner);
|
||||
|
||||
/* --one-file-system (-x) decision: a directory entry may be descended into
|
||||
* only when the option is disabled or the entry lives on the same device as
|
||||
* the transfer root. Exposed so tests can exercise the rule directly. */
|
||||
bool scanner_same_filesystem(bool one_file_system, dev_t root_device, dev_t entry_device);
|
||||
|
||||
ParallelScanner* parallel_scanner_create_with_options(const char* root_directory,
|
||||
const ScannerOptions* options,
|
||||
ProtocolSession* allocation_session);
|
||||
|
||||
@@ -83,6 +83,7 @@ void print_usage(void) {
|
||||
printf(" --stats Print transfer statistics at end\n");
|
||||
printf(" -h, --human-readable Print byte sizes in human-readable form\n");
|
||||
printf(" --max-depth <n> Maximum directory depth (0=unlimited)\n");
|
||||
printf(" -x, --one-file-system Do not cross filesystem boundaries\n");
|
||||
printf(" --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");
|
||||
|
||||
@@ -93,6 +93,7 @@ static void config_set_defaults(Config* config) {
|
||||
config->files_from = NULL;
|
||||
config->cvs_exclude = false;
|
||||
config->prune_empty_dirs = false;
|
||||
config->one_file_system = false;
|
||||
config->relative = false;
|
||||
config->rsh_command = NULL;
|
||||
config->rsync_path = NULL;
|
||||
|
||||
@@ -104,6 +104,7 @@ typedef struct Config {
|
||||
char* files_from;
|
||||
bool cvs_exclude;
|
||||
bool prune_empty_dirs;
|
||||
bool one_file_system; /* -x/--one-file-system: do not cross filesystem boundaries */
|
||||
bool relative;
|
||||
|
||||
// Issue #130: Remote shell/connection options
|
||||
|
||||
Reference in New Issue
Block a user