diff --git a/flist.c b/flist.c index 17832533e..993df0489 100644 --- a/flist.c +++ b/flist.c @@ -21,6 +21,7 @@ */ #include "rsync.h" +#include #include "ifuncs.h" #include "rounding.h" #include "inums.h" @@ -277,6 +278,18 @@ static inline int path_is_daemon_excluded(char *path, int ignore_filename) static inline int is_excluded(const char *fname, int is_dir, int filter_level) { + /* Check for marker file in directories */ + if (skip_dir_with_file && is_dir) { + char marker_path[MAXPATHLEN]; + int len = snprintf(marker_path, MAXPATHLEN, "%s/%s", fname, skip_dir_with_file); + + if (len > 0 && len < MAXPATHLEN && access(marker_path, F_OK) == 0) { + if (INFO_GTE(FLIST, 2)) + rprintf(FINFO, "skipping directory %s containing %s marker\n", + full_fname(fname), skip_dir_with_file); + return 1; + } + } return name_is_excluded(fname, is_dir ? NAME_IS_DIR : NAME_IS_FILE, filter_level); } diff --git a/options.c b/options.c index 578507c6e..098d432f0 100644 --- a/options.c +++ b/options.c @@ -139,6 +139,7 @@ int32 block_size = 0; time_t stop_at_utime = 0; char *skip_compress = NULL; char *copy_as = NULL; +const char *skip_dir_with_file = NULL; item_list dparam_list = EMPTY_ITEM_LIST; /** Network address family. **/ @@ -584,7 +585,7 @@ enum {OPT_SERVER = 1000, OPT_DAEMON, OPT_SENDER, OPT_EXCLUDE, OPT_EXCLUDE_FROM, OPT_NO_D, OPT_APPEND, OPT_NO_ICONV, OPT_INFO, OPT_DEBUG, OPT_BLOCK_SIZE, OPT_USERMAP, OPT_GROUPMAP, OPT_CHOWN, OPT_BWLIMIT, OPT_STDERR, OPT_OLD_COMPRESS, OPT_NEW_COMPRESS, OPT_NO_COMPRESS, OPT_OLD_ARGS, - OPT_STOP_AFTER, OPT_STOP_AT, + OPT_STOP_AFTER, OPT_STOP_AT, OPT_SKIP_DIR_WITH, OPT_REFUSED_BASE = 9000}; static struct poptOption long_options[] = { @@ -841,6 +842,8 @@ static struct poptOption long_options[] = { {"dparam", 0, POPT_ARG_STRING, 0, OPT_DAEMON, 0, 0 }, {"detach", 0, POPT_ARG_NONE, 0, OPT_DAEMON, 0, 0 }, {"no-detach", 0, POPT_ARG_NONE, 0, OPT_DAEMON, 0, 0 }, + {"skip-compress", 0, POPT_ARG_STRING, &skip_compress, 0, 0, 0 }, + {"skip-dir-with", 0, POPT_ARG_STRING, &skip_dir_with_file, OPT_SKIP_DIR_WITH, 0, 0 }, {0,0,0,0, 0, 0, 0} }; @@ -1893,6 +1896,16 @@ int parse_arguments(int *argc_p, const char ***argv_p) break; #endif + case OPT_SKIP_DIR_WITH: + arg = poptGetOptArg(pc); + if (arg && *arg) + skip_dir_with_file = arg; + else { + snprintf(err_buf, sizeof err_buf, "skip-dir-with requires a FILE argument\n"); + goto cleanup; + } + break; + case OPT_STDERR: { int len; arg = poptGetOptArg(pc); diff --git a/rsync.1.md b/rsync.1.md index 7e40e3617..b66351092 100644 --- a/rsync.1.md +++ b/rsync.1.md @@ -514,6 +514,7 @@ has its own detailed description later in this manpage. --compress-choice=STR choose the compression algorithm (aka --zc) --compress-level=NUM explicitly set compression level (aka --zl) --skip-compress=LIST skip compressing files with suffix in LIST +--skip-dir-with=FILE skip directories containing specified FILE --cvs-exclude, -C auto-ignore files in the same way CVS does --filter=RULE, -f add a file-filtering RULE -F same as --filter='dir-merge /.rsync-filter' @@ -2951,6 +2952,19 @@ expand it. list of non-compressing files (and its list may be configured to a different default). +0. `--skip-dir-with=FILE` + + Skip directories that contain the specified FILE. + + This option is useful for skipping transfer of directories that contain + a marker file indicating the directory should not be transferred. For + example, you could use it to skip directories containing temporary files or + cached data. + + The FILE argument is required and specifies the name of the marker file to + look for. rsync will check each directory for the presence of a file with + this name, and if found, will skip the directory entirely. + 0. `--numeric-ids` With this option rsync will transfer numeric group and user IDs rather than diff --git a/rsync.h b/rsync.h index 479ac4848..c79ded961 100644 --- a/rsync.h +++ b/rsync.h @@ -1487,3 +1487,5 @@ const char *get_panic_action(void); #elif defined HAVE_MALLINFO #define MEM_ALLOC_INFO mallinfo #endif + +extern const char *skip_dir_with_file; diff --git a/testsuite/skip-dir-with.test b/testsuite/skip-dir-with.test new file mode 100644 index 000000000..9d328b9de --- /dev/null +++ b/testsuite/skip-dir-with.test @@ -0,0 +1,69 @@ +#!/bin/sh + +# Copyright (C) 2025 Allan Clark + +# This program is distributable under the terms of the GNU GPL (see +# COPYING). + +# Test rsync handling of --skip-dir-with option, which skips directories +# containing a specified marker file. + +. "$suitedir/rsync.fns" + +# Build some files/dirs/links to copy +makepath "$fromdir/dir1" +makepath "$fromdir/dir2" +makepath "$fromdir/dir3" +makepath "$fromdir/dir4/subdir" + +# Add content to the directories +echo "file1" > "$fromdir/dir1/file1.txt" +echo "file2" > "$fromdir/dir2/file2.txt" +echo "file3" > "$fromdir/dir3/file3.txt" +echo "file4" > "$fromdir/dir4/file4.txt" +echo "subfile" > "$fromdir/dir4/subdir/subfile.txt" + +# Create marker files +touch "$fromdir/dir2/.marker" +touch "$fromdir/dir4/subdir/.different_marker" + +# Test 1: Using .marker to skip dir2 +$RSYNC -a --skip-dir-with=.marker "$fromdir/" "$todir/" + +# Verify that dir2 was skipped +test -d "$todir/dir2" && test_fail "dir2 with .marker was not skipped" +test -d "$todir/dir1" || test_fail "dir1 should have been copied" +test -d "$todir/dir3" || test_fail "dir3 should have been copied" +test -d "$todir/dir4" || test_fail "dir4 should have been copied" + +# Cleanup +rm -rf "$todir"/* + +# Test 2: Using .different_marker to skip subdir +$RSYNC -a --skip-dir-with=.different_marker "$fromdir/" "$todir/" + +# Verify that dir4/subdir was skipped +test -d "$todir/dir4/subdir" && test_fail "dir4/subdir with .different_marker was not skipped" +test -d "$todir/dir1" || test_fail "dir1 should have been copied" +test -d "$todir/dir2" || test_fail "dir2 should have been copied" +test -d "$todir/dir3" || test_fail "dir3 should have been copied" +test -f "$todir/dir4/file4.txt" || test_fail "dir4/file4.txt should have been copied" + +# Cleanup +rm -rf "$todir"/* + +# Add more marker files for third test +touch "$fromdir/dir1/.common_marker" +touch "$fromdir/dir3/.common_marker" + +# Test 3: Using .common_marker to skip multiple directories +$RSYNC -a --skip-dir-with=.common_marker "$fromdir/" "$todir/" + +# Verify that both dir1 and dir3 were skipped +test -d "$todir/dir1" && test_fail "dir1 with .common_marker was not skipped" +test -d "$todir/dir3" && test_fail "dir3 with .common_marker was not skipped" +test -d "$todir/dir2" || test_fail "dir2 should have been copied" +test -d "$todir/dir4" || test_fail "dir4 should have been copied" + +# Success! +exit 0 \ No newline at end of file