Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/cmd/cmd.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ struct CmdOptions
bool interactive = false;
bool ignoreHiddenFiles = false;
QString exclude;
QString excludeAnchored;
QString unsyncedfolders;
int restartTimes = 0;
int downlimit = 0;
Expand Down Expand Up @@ -171,6 +172,10 @@ void help()
std::cout << " Proxy is http://server:port" << std::endl;
std::cout << " --trust Trust the SSL certification." << std::endl;
std::cout << " --exclude [file] Exclude list file" << std::endl;
std::cout << " --exclude-anchored [file] Exclude list file, always anchored at the" << std::endl;
std::cout << " sync root regardless of the file's own name" << std::endl;
std::cout << " (use this if --exclude patterns aren't matching," << std::endl;
std::cout << " see nextcloud/desktop#2916, #7682)" << std::endl;
std::cout << " --unsyncedfolders [file] File containing the list of unsynced remote folders (selective sync)" << std::endl;
std::cout << " --user, -u [name] Use [name] as the login name" << std::endl;
std::cout << " --password, -p [pass] Use [pass] as password" << std::endl;
Expand Down Expand Up @@ -248,6 +253,8 @@ void parseOptions(const QStringList &app_args, CmdOptions *options)
options->password = it.next();
} else if (option == "--exclude" && !it.peekNext().startsWith("-")) {
options->exclude = it.next();
} else if (option == "--exclude-anchored" && !it.peekNext().startsWith("-")) {
options->excludeAnchored = it.next();
} else if (option == "--unsyncedfolders" && !it.peekNext().startsWith("-")) {
options->unsyncedfolders = it.next();
} else if (option == "--max-sync-retries" && !it.peekNext().startsWith("-")) {
Expand Down Expand Up @@ -542,11 +549,11 @@ int main(int argc, char **argv)

// Exclude lists

bool hasUserExcludeFile = !options.exclude.isEmpty();
bool hasUserExcludeFile = !options.exclude.isEmpty() || !options.excludeAnchored.isEmpty();
QString systemExcludeFile = ConfigFile::excludeFileFromSystem();

// Always try to load the user-provided exclude list if one is specified
if (hasUserExcludeFile) {
// Always try to load the user-provided exclude list(s) if specified
if (!options.exclude.isEmpty()) {
if (!QFile::exists(options.exclude)) {
// A user-supplied --exclude path that can't be found is a
// configuration error, not something to silently ignore:
Expand All @@ -556,15 +563,29 @@ int main(int argc, char **argv)
qFatal("Exclude list file supplied via --exclude does not exist: %s", qUtf8Printable(options.exclude));
return EXIT_FAILURE;
}
// Keeps addExcludeFilePath()'s historic filename-based anchoring
// heuristic for --exclude, so existing setups that rely on it
// (however unintentionally) don't change behavior. Use
// --exclude-anchored instead if patterns aren't matching because
// the file isn't literally named "sync-exclude.lst".
engine.excludedFiles().addExcludeFilePath(options.exclude);
}
if (!options.excludeAnchored.isEmpty()) {
if (!QFile::exists(options.excludeAnchored)) {
qFatal("Exclude list file supplied via --exclude-anchored does not exist: %s", qUtf8Printable(options.excludeAnchored));
return EXIT_FAILURE;
}
// Always anchor patterns at the sync root regardless of the file's
// own name, see ExcludedFiles::addExcludeFilePath().
engine.excludedFiles().addExcludeFilePath(options.excludeAnchored, ExcludedFiles::ExcludeFileAnchor::SyncRoot);
}
// Load the system list if available, or if there's no user-provided list
if (!hasUserExcludeFile || QFile::exists(systemExcludeFile)) {
engine.excludedFiles().addExcludeFilePath(systemExcludeFile);
}

if (!engine.excludedFiles().reloadExcludeFiles()) {
qFatal("Cannot load system exclude list or list supplied via --exclude");
qFatal("Cannot load system exclude list or list supplied via --exclude/--exclude-anchored");
return EXIT_FAILURE;
}

Expand Down
4 changes: 2 additions & 2 deletions src/csync/csync_exclude.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -242,11 +242,11 @@ ExcludedFiles::ExcludedFiles(const QString &localPath)

ExcludedFiles::~ExcludedFiles() = default;

void ExcludedFiles::addExcludeFilePath(const QString &path)
void ExcludedFiles::addExcludeFilePath(const QString &path, ExcludeFileAnchor anchor)
{
const QFileInfo excludeFileInfo(path);
const auto fileName = excludeFileInfo.fileName();
const auto basePath = fileName.compare(QStringLiteral("sync-exclude.lst"), Qt::CaseInsensitive) == 0
const auto basePath = anchor == ExcludeFileAnchor::SyncRoot || fileName.compare(QStringLiteral("sync-exclude.lst"), Qt::CaseInsensitive) == 0
? _localPath
: leftIncludeLast(path, QLatin1Char('/'));
auto &excludeFilesLocalPath = _excludeFiles[basePath];
Expand Down
25 changes: 24 additions & 1 deletion src/csync/csync_exclude.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,38 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject
public:
using Version = std::tuple<int, int, int>;

/**
* Where a caller-supplied exclude file's patterns are anchored, see
* addExcludeFilePath().
*/
enum class ExcludeFileAnchor {
FileDirectory, // anchor at the exclude file's own containing directory
SyncRoot // anchor at the sync root (_localPath), regardless of the file's name
};

explicit ExcludedFiles(const QString &localPath = QStringLiteral("/"));
~ExcludedFiles() override;

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

/**
* Whether conflict files shall be excluded.
Expand Down
26 changes: 25 additions & 1 deletion test/testexcludedfiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -759,7 +759,31 @@ private slots:
QCOMPARE(excludedFiles->_excludeFiles[folder1].first(), folder1ExcludeList);
QCOMPARE(excludedFiles->_excludeFiles[folder2].first(), folder2ExcludeList);
}


// A caller-supplied exclude file not literally named "sync-exclude.lst"
// must still anchor its patterns at the sync root when
// ExcludeFileAnchor::SyncRoot is requested.
void testAddExcludeFilePath_syncRootAnchor_bypassesFilenameHeuristic()
{
const QString basePath("syncFolder/");
excludedFiles.reset(new ExcludedFiles(basePath));

const QString customExcludeList("home/thomas/.config/my-custom-exclude.lst");

// Default (as used for per-directory .sync-exclude.lst discovery):
// anchored at the exclude file's own directory, since its name isn't
// literally "sync-exclude.lst".
excludedFiles->addExcludeFilePath(customExcludeList);
QCOMPARE(excludedFiles->_excludeFiles.contains(basePath), false);
QCOMPARE(excludedFiles->_excludeFiles[QString("home/thomas/.config/")].first(), customExcludeList);

// ExcludeFileAnchor::SyncRoot (as used for nextcloudcmd's --exclude
// option): anchored at the sync root regardless of the file's name.
excludedFiles.reset(new ExcludedFiles(basePath));
excludedFiles->addExcludeFilePath(customExcludeList, ExcludedFiles::ExcludeFileAnchor::SyncRoot);
QCOMPARE(excludedFiles->_excludeFiles[basePath].first(), customExcludeList);
}

void testReloadExcludeFiles_fileDoesNotExist_returnTrue() {
excludedFiles.reset(new ExcludedFiles());
const QString nonExistingFile("directory/.sync-exclude.lst");
Expand Down