From 7decc78c97a592b37024b4f72bcdfe88722892fe Mon Sep 17 00:00:00 2001 From: Andrea Mazzoleni Date: Mon, 28 Sep 2026 11:24:59 +0200 Subject: [PATCH] Apply filters to empty directories during scan and match subdirectories in directory rules --- cmdline/elem.c | 13 ++++++----- cmdline/scan.c | 15 ++++++++---- cmdline/selftest.c | 58 +++++++++++++++++++++++++++++++++++----------- 3 files changed, 61 insertions(+), 25 deletions(-) diff --git a/cmdline/elem.c b/cmdline/elem.c index cdeeab4d..60c63a9e 100644 --- a/cmdline/elem.c +++ b/cmdline/elem.c @@ -240,25 +240,26 @@ static int filter_apply(struct snapraid_filter* filter, struct snapraid_filter** * * This is done allowing a partial matching as far it ends at a directory separator */ - int match_sub = 0; - if (filter->is_dir && !is_dir) - match_sub = 1; + int match_sub = filter->is_dir; int ret = 0; if (filter->is_abs) { /* preserve the skipped slash because it gives an initial ** its recursive meaning */ - if (wnmatch_sub_prev(filter->pattern + 1, path, match_sub, '/') == 0) + if (wnmatch_sub_prev(filter->pattern + 1, path, match_sub, '/') == 0 + || (is_dir && wnmatch_sub_prev(filter->pattern + 1, path, 0, '/') == 0)) ret = filter->direction; } else { /* the path is relative, first try to match from the root */ - if (wnmatch_sub(filter->pattern, path, match_sub) == 0) { + if (wnmatch_sub(filter->pattern, path, match_sub) == 0 + || (is_dir && wnmatch_sub(filter->pattern, path, 0) == 0)) { ret = filter->direction; } else { /* then try to match after all the / presents */ const char* slash = strchr(path, '/'); while (slash) { - if (wnmatch_sub(filter->pattern, slash + 1, match_sub) == 0) { + if (wnmatch_sub(filter->pattern, slash + 1, match_sub) == 0 + || (is_dir && wnmatch_sub(filter->pattern, slash + 1, 0) == 0)) { ret = filter->direction; break; } diff --git a/cmdline/scan.c b/cmdline/scan.c index 8a54633b..4eb0e4e3 100644 --- a/cmdline/scan.c +++ b/cmdline/scan.c @@ -1757,14 +1757,19 @@ static int scan_sub(struct snapraid_scan* scan, int level, int is_diff, char* pa /* recurse */ pathslash(path_next, PATH_MAX); pathslash(sub_next, PATH_MAX); - if (scan_sub(scan, level + 1, is_diff, path_next, sub_next, tmp) == 0) { + if (scan_sub(scan, level + 1, is_diff, path_next, sub_next, tmp) != 0) { + processed = 1; + } else { /* restore removing additions */ pathcatl(sub_next, sub_len, PATH_MAX, name); - /* scan the directory as empty dir */ - scan_emptydir(scan, sub_next); + + /* scan the directory as empty dir only if selected */ + if (filter_emptydir(&state->filterlist, &reason, disk->name, sub_next) == 0 + && filter_emptydir(&scan->local_filter_list, &reason, disk->name, sub_next) == 0) { + scan_emptydir(scan, sub_next); + processed = 1; + } } - /* or we processed something internally, or we have added the empty dir */ - processed = 1; } } else { msg_verbose("Excluding directory '%s' for rule '%s'\n", path_next, filter_type(reason, tmp, PATH_MAX)); diff --git a/cmdline/selftest.c b/cmdline/selftest.c index 8bc6c628..a88f61e5 100644 --- a/cmdline/selftest.c +++ b/cmdline/selftest.c @@ -1466,8 +1466,8 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { { "d1", "file.tmp", -1, 0, 0 }, { "d1", "sub/file.tmp", -1, 0, 0 }, { "d1", "cache", 0, -1, -1 }, - { "d1", "cache/data.bin", -1, 0, 0 }, - { "d1", "sub/tmp/data.bin", -1, 0, 0 }, + { "d1", "cache/data.bin", -1, -1, -1 }, + { "d1", "sub/tmp/data.bin", -1, -1, -1 }, { "d1", "sub/tmp", 0, -1, -1 }, { "d1", "doc.pdf", 0, 0, 0 }, { "d1", "other/doc.pdf", 0, 0, 0 }, @@ -1485,7 +1485,7 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { { { "d1", "song.mp3", 0, 0, -1 }, { "d1", "audio/song.mp3", 0, 0, -1 }, - { "d1", "music/track.flac", 0, 0, -1 }, + { "d1", "music/track.flac", 0, 0, 0 }, { "d1", "music", -1, 0, 0 }, { "d1", "photo.jpg", -1, 0, -1 }, { "d1", "photos/photo.jpg", -1, 0, -1 }, @@ -1493,6 +1493,36 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { { 0 } } }, + { + "directory rules include descendant directories", + { + { 1, 0, "", "/music/" }, + { 0 } + }, + { + { "d1", "music", -1, 0, 0 }, + { "d1", "music/live", 0, 0, 0 }, + { "d1", "music/live/empty", 0, 0, 0 }, + { "d1", "other", -1, 0, -1 }, + { "d1", "other/empty", -1, 0, -1 }, + { 0 } + } + }, + { + "directory rules exclude descendant directories", + { + { -1, 0, "", "/cache/" }, + { 0 } + }, + { + { "d1", "cache", 0, -1, -1 }, + { "d1", "cache/sub", -1, -1, -1 }, + { "d1", "cache/sub/empty", -1, -1, -1 }, + { "d1", "other", 0, 0, 0 }, + { "d1", "other/empty", 0, 0, 0 }, + { 0 } + } + }, { "directory exclude prunes scanner before file include can match", { @@ -1502,8 +1532,8 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { }, { { "d1", "keep", 0, -1, -1 }, - { "d1", "keep/file.dat", 0, 0, 0 }, - { "d1", "keep/other.dat", -1, 0, 0 }, + { "d1", "keep/file.dat", 0, -1, -1 }, + { "d1", "keep/other.dat", -1, -1, -1 }, { "d1", "other/file.txt", 0, 0, 0 }, { 0 } } @@ -1561,7 +1591,7 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { { { "d1", "cache.tmp", -1, 0, -1 }, { "d1", "media", -1, 0, 0 }, - { "d1", "media/movie.mkv", 0, 0, -1 }, + { "d1", "media/movie.mkv", 0, 0, 0 }, { "d1", "docs/readme.txt", -1, 0, -1 }, { 0 } } @@ -1575,7 +1605,7 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { }, { { "d1", "logs", 0, -1, -1 }, - { "d1", "logs/a.txt", -1, 0, 0 }, + { "d1", "logs/a.txt", -1, -1, -1 }, { "d1", "report", -1, 0, 0 }, { "d1", "report/a.txt", 0, 0, 0 }, { 0 } @@ -1590,12 +1620,12 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { }, { { "d1", "abs_tmp", 0, -1, -1 }, - { "d1", "abs_tmp/file.txt", -1, 0, 0 }, + { "d1", "abs_tmp/file.txt", -1, -1, -1 }, { "d1", "sub/abs_tmp/file.txt", 0, 0, 0 }, { "d1", "rel_tmp", 0, -1, -1 }, - { "d1", "rel_tmp/file.txt", -1, 0, 0 }, + { "d1", "rel_tmp/file.txt", -1, -1, -1 }, { "d1", "sub/rel_tmp", 0, -1, -1 }, - { "d1", "sub/rel_tmp/file.txt", -1, 0, 0 }, + { "d1", "sub/rel_tmp/file.txt", -1, -1, -1 }, { 0 } } }, @@ -1634,7 +1664,7 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { }, { { "d1", "projects/sub/build", 0, -1, -1 }, - { "d1", "projects/sub/build/output.o", -1, 0, 0 }, + { "d1", "projects/sub/build/output.o", -1, -1, -1 }, { "d1", "projects/sub/nested/build", 0, 0, 0 }, { "d1", "projects/sub/nested/build/output.o", 0, 0, 0 }, { "d1", "projects/submarine/build", 0, 0, 0 }, @@ -1654,8 +1684,8 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { { "d1", "src/components/ui/button.js", 0, 0, -1 }, { "d1", "src/test", -1, -1, -1 }, { "d1", "src/components/test", -1, -1, -1 }, - { "d1", "src/test/app.js", -1, 0, -1 }, - { "d1", "src/components/test/app.js", -1, 0, -1 }, + { "d1", "src/test/app.js", -1, -1, -1 }, + { "d1", "src/components/test/app.js", -1, -1, -1 }, { "d1", "src/app.css", -1, 0, -1 }, { 0 } } @@ -1741,7 +1771,7 @@ static const struct filter_scenario FILTER_SCENARIOS[] = { { { "d1", "foo", 0, -1, -1 }, { "d1", "a/b/foo", 0, -1, -1 }, - { "d1", "a/b/foo/file", -1, 0, 0 }, + { "d1", "a/b/foo/file", -1, -1, -1 }, { "d1", "a/b/bar", 0, 0, 0 }, { 0 } }