Skip to content

Commit e5db10e

Browse files
mosandlconcreativmosandlt
authored andcommitted
fix(cmd,csync): anchor --exclude patterns at the sync root regardless of filename
Summary nextcloudcmd --exclude <path> silently produced zero exclusions for every pattern in the file, unless <path> happened to be literally named "sync-exclude.lst" -- with no warning or error anywhere. Any other filename (a typo'd extension, a descriptive name, a per-tool name like myapp-exclude.lst) caused every single pattern to be matched against the wrong base directory, so none of them ever excluded anything under the sync root. Root cause ExcludedFiles::addExcludeFilePath() in src/csync/csync_exclude.cpp anchors patterns either at the sync root (_localPath) or at the exclude file's own containing directory, based on a filename check: literally "sync-exclude.lst" -> sync root, anything else -> own directory. That heuristic was introduced in 5788f35 (2020-12-09, "Skip sync exclude file from list of exclude files if it doesn't exist") to merge two previously-separate code paths: the GUI's global exclude list (always named sync-exclude.lst, lives outside any sync folder, needs sync-root anchoring) and a per-directory .sync-exclude.lst discovered during traversal (needs anchoring at its own directory). It was a side effect of that merge, not a deliberate choice -- the commit's own description and linked issue are unrelated to anchoring behavior. src/cmd/cmd.cpp passes the CLI's --exclude value into the same function unconditionally, so it inherited this GUI-specific heuristic even though CLI users can name their exclude file anything. Reported independently by multiple users: #2916, #7682, and a comment diagnosing the exact same filename dependency on #2916. Fix Add an explicit anchorToLocalPath parameter to addExcludeFilePath() (default false, preserving current behavior everywhere else) and set it for the CLI's --exclude option in cmd.cpp, so its patterns are always anchored at the sync root regardless of the file's name. Test plan - [x] New unit test: testAddExcludeFilePath_anchorToLocalPath_bypassesFilenameHeuristic - [x] Full ExcludedFilesTest suite: 25 passed, 0 failed - [x] Manual repro against a live Nextcloud account: an --exclude file not named sync-exclude.lst previously had zero effect (excluded directories were downloaded anyway); after this fix they are correctly skipped (FileIgnored) with no behavior change for GUI-managed exclude lists. Signed-off-by: Thomas Mosandl <mosandl@concreativ.de> Assisted-by: ClaudeCode:claude-sonnet-5
1 parent c5223d3 commit e5db10e

4 files changed

Lines changed: 54 additions & 5 deletions

File tree

src/cmd/cmd.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,7 +556,9 @@ int main(int argc, char **argv)
556556
qFatal("Exclude list file supplied via --exclude does not exist: %s", qUtf8Printable(options.exclude));
557557
return EXIT_FAILURE;
558558
}
559-
engine.excludedFiles().addExcludeFilePath(options.exclude);
559+
// Anchor patterns at the sync root regardless of the file's own name,
560+
// see ExcludedFiles::addExcludeFilePath().
561+
engine.excludedFiles().addExcludeFilePath(options.exclude, ExcludedFiles::ExcludeFileAnchor::SyncRoot);
560562
}
561563
// Load the system list if available, or if there's no user-provided list
562564
if (!hasUserExcludeFile || QFile::exists(systemExcludeFile)) {

src/csync/csync_exclude.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,11 @@ ExcludedFiles::ExcludedFiles(const QString &localPath)
242242

243243
ExcludedFiles::~ExcludedFiles() = default;
244244

245-
void ExcludedFiles::addExcludeFilePath(const QString &path)
245+
void ExcludedFiles::addExcludeFilePath(const QString &path, ExcludeFileAnchor anchor)
246246
{
247247
const QFileInfo excludeFileInfo(path);
248248
const auto fileName = excludeFileInfo.fileName();
249-
const auto basePath = fileName.compare(QStringLiteral("sync-exclude.lst"), Qt::CaseInsensitive) == 0
249+
const auto basePath = anchor == ExcludeFileAnchor::SyncRoot || fileName.compare(QStringLiteral("sync-exclude.lst"), Qt::CaseInsensitive) == 0
250250
? _localPath
251251
: leftIncludeLast(path, QLatin1Char('/'));
252252
auto &excludeFilesLocalPath = _excludeFiles[basePath];

src/csync/csync_exclude.h

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,38 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject
6262
public:
6363
using Version = std::tuple<int, int, int>;
6464

65+
/**
66+
* Where a caller-supplied exclude file's patterns are anchored, see
67+
* addExcludeFilePath().
68+
*/
69+
enum class ExcludeFileAnchor {
70+
FileDirectory, // anchor at the exclude file's own containing directory
71+
SyncRoot // anchor at the sync root (_localPath), regardless of the file's name
72+
};
73+
6574
explicit ExcludedFiles(const QString &localPath = QStringLiteral("/"));
6675
~ExcludedFiles() override;
6776

6877
/**
6978
* Adds a new path to a file containing exclude patterns.
7079
*
7180
* Does not load the file. Use reloadExcludeFiles() afterwards.
81+
*
82+
* By default (ExcludeFileAnchor::FileDirectory) the patterns are
83+
* anchored at the directory the exclude file itself lives in, unless
84+
* that file is literally named "sync-exclude.lst" (case-insensitive),
85+
* in which case they're anchored at the sync root (_localPath)
86+
* regardless. That heuristic exists to distinguish the GUI's global
87+
* exclude list (always named sync-exclude.lst, but stored outside any
88+
* sync folder) from a per-directory .sync-exclude.lst discovered
89+
* during traversal.
90+
*
91+
* It does not fit a caller-supplied exclude file of arbitrary name,
92+
* such as nextcloudcmd's --exclude option: pass
93+
* ExcludeFileAnchor::SyncRoot there to force sync-root anchoring
94+
* regardless of the file's name.
7295
*/
73-
void addExcludeFilePath(const QString &path);
96+
void addExcludeFilePath(const QString &path, ExcludeFileAnchor anchor = ExcludeFileAnchor::FileDirectory);
7497

7598
/**
7699
* Whether conflict files shall be excluded.

test/testexcludedfiles.cpp

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -759,7 +759,31 @@ private slots:
759759
QCOMPARE(excludedFiles->_excludeFiles[folder1].first(), folder1ExcludeList);
760760
QCOMPARE(excludedFiles->_excludeFiles[folder2].first(), folder2ExcludeList);
761761
}
762-
762+
763+
// A caller-supplied exclude file not literally named "sync-exclude.lst"
764+
// must still anchor its patterns at the sync root when
765+
// ExcludeFileAnchor::SyncRoot is requested.
766+
void testAddExcludeFilePath_syncRootAnchor_bypassesFilenameHeuristic()
767+
{
768+
const QString basePath("syncFolder/");
769+
excludedFiles.reset(new ExcludedFiles(basePath));
770+
771+
const QString customExcludeList("home/thomas/.config/my-custom-exclude.lst");
772+
773+
// Default (as used for per-directory .sync-exclude.lst discovery):
774+
// anchored at the exclude file's own directory, since its name isn't
775+
// literally "sync-exclude.lst".
776+
excludedFiles->addExcludeFilePath(customExcludeList);
777+
QCOMPARE(excludedFiles->_excludeFiles.contains(basePath), false);
778+
QCOMPARE(excludedFiles->_excludeFiles[QString("home/thomas/.config/")].first(), customExcludeList);
779+
780+
// ExcludeFileAnchor::SyncRoot (as used for nextcloudcmd's --exclude
781+
// option): anchored at the sync root regardless of the file's name.
782+
excludedFiles.reset(new ExcludedFiles(basePath));
783+
excludedFiles->addExcludeFilePath(customExcludeList, ExcludedFiles::ExcludeFileAnchor::SyncRoot);
784+
QCOMPARE(excludedFiles->_excludeFiles[basePath].first(), customExcludeList);
785+
}
786+
763787
void testReloadExcludeFiles_fileDoesNotExist_returnTrue() {
764788
excludedFiles.reset(new ExcludedFiles());
765789
const QString nonExistingFile("directory/.sync-exclude.lst");

0 commit comments

Comments
 (0)