From 14279104ae12c46eaa209e1c9c7da4b85395bfc3 Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Thu, 8 Aug 2019 23:30:49 +0100 Subject: Read .sync_exclude.lst in each subdirectory Signed-off-by: Samir Benmendil --- src/csync/csync_exclude.cpp | 237 +++++++++++++++++-------- src/csync/csync_exclude.h | 57 ++++-- src/libsync/syncengine.cpp | 2 +- test/csync/csync_tests/check_csync_exclude.cpp | 63 ++++++- 4 files changed, 264 insertions(+), 95 deletions(-) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index e3146a099..030783c6b 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -236,13 +236,29 @@ static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const char *path, bool excludeC return match; } +static QByteArray leftIncludeLast(const QByteArray & arr, char c) +{ + // left up to and including `c` + return arr.left(arr.lastIndexOf(c, arr.size() - 2) + 1); +} using namespace OCC; -ExcludedFiles::ExcludedFiles() +ExcludedFiles::ExcludedFiles(QString localPath) + : _localPath(std::move(localPath)) { + Q_ASSERT(_localPath.endsWith("/")); // Windows used to use PathMatchSpec which allows *foo to match abc/deffoo. _wildcardsMatchSlash = Utility::isWindows(); + + // We're in a detached exclude probably coming from a partial sync or test + if (_localPath.isEmpty()) + return; + + // Load exclude file from base dir + QFileInfo fi(_localPath + ".sync-exclude.lst"); + if (fi.isReadable()) + addInTreeExcludeFilePath(fi.absoluteFilePath()); } ExcludedFiles::~ExcludedFiles() @@ -251,7 +267,13 @@ ExcludedFiles::~ExcludedFiles() void ExcludedFiles::addExcludeFilePath(const QString &path) { - _excludeFiles.insert(path); + _excludeFiles[_localPath.toUtf8()].append(path); +} + +void ExcludedFiles::addInTreeExcludeFilePath(const QString &path) +{ + BasePathByteArray basePath = leftIncludeLast(path.toUtf8(), '/'); + _excludeFiles[basePath].append(path); } void ExcludedFiles::setExcludeConflictFiles(bool onoff) @@ -259,11 +281,15 @@ void ExcludedFiles::setExcludeConflictFiles(bool onoff) _excludeConflictFiles = onoff; } -void ExcludedFiles::addManualExclude(const QByteArray &expr) +void ExcludedFiles::addManualExclude(const QByteArray &expr, const QByteArray &basePath) { - _manualExcludes.append(expr); - _allExcludes.append(expr); - prepare(); + Q_ASSERT(basePath.startsWith('/')); + Q_ASSERT(basePath.endsWith('/')); + + auto key = basePath; + _manualExcludes[key].append(expr); + _allExcludes[key].append(expr); + prepare(key); } void ExcludedFiles::clearManualExcludes() @@ -282,21 +308,27 @@ bool ExcludedFiles::reloadExcludeFiles() { _allExcludes.clear(); bool success = true; - foreach (const QString &file, _excludeFiles) { - QFile f(file); - if (!f.open(QIODevice::ReadOnly)) { - success = false; - continue; - } - while (!f.atEnd()) { - QByteArray line = f.readLine().trimmed(); - if (line.isEmpty() || line.startsWith('#')) + for (auto basePath : _excludeFiles.keys()) { + for (auto file : _excludeFiles.value(basePath)) { + QFile f(file); + if (!f.open(QIODevice::ReadOnly)) { + success = false; continue; - csync_exclude_expand_escapes(line); - _allExcludes.append(line); + } + while (!f.atEnd()) { + QByteArray line = f.readLine().trimmed(); + if (line.isEmpty() || line.startsWith('#')) + continue; + csync_exclude_expand_escapes(line); + _allExcludes[basePath].append(line); + } } } - _allExcludes.append(_manualExcludes); + + auto endManual = _manualExcludes.cend(); + for (auto kv = _manualExcludes.cbegin(); kv != endManual; ++kv) + _allExcludes[kv.key()].append(kv.value()); + prepare(); return success; } @@ -317,6 +349,8 @@ bool ExcludedFiles::isExcluded( // We do want to be able to sync with a hidden folder as the target. while (path.size() > basePath.size()) { QFileInfo fi(path); + //TODO probably not ignore `.sync-exclude.lst` files as it makes sense for them to be + //synced if (fi.isHidden() || fi.fileName().startsWith(QLatin1Char('.'))) { return true; } @@ -340,7 +374,7 @@ bool ExcludedFiles::isExcluded( return fullPatternMatch(relativePath.toUtf8(), type) != CSYNC_NOT_EXCLUDED; } -CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const char *path, ItemType filetype) const +CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const char *path, ItemType filetype) { auto match = _csync_excluded_common(path, _excludeConflictFiles); if (match != CSYNC_NOT_EXCLUDED) @@ -348,6 +382,16 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const char *path, ItemTy if (_allExcludes.isEmpty()) return CSYNC_NOT_EXCLUDED; + // Directories are guaranteed to be visited before their files + if (filetype == ItemTypeDirectory) { + QFileInfo fi = QFileInfo(_localPath + path + "/.sync-exclude.lst"); + if (fi.isReadable()) { + addInTreeExcludeFilePath(fi.absoluteFilePath()); + //really we only need to load this file and prepare(this basePath) + reloadExcludeFiles(); + } + } + // Check the bname part of the path to see whether the full // regex should be run. @@ -359,35 +403,53 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const char *path, ItemTy } QString bnameStr = QString::fromUtf8(bname); - QRegularExpressionMatch m; - if (filetype == ItemTypeDirectory) { - m = _bnameTraversalRegexDir.match(bnameStr); - } else { - m = _bnameTraversalRegexFile.match(bnameStr); - } - if (!m.hasMatch()) - return CSYNC_NOT_EXCLUDED; - if (m.capturedStart(QStringLiteral("exclude")) != -1) { - return CSYNC_FILE_EXCLUDE_LIST; - } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { - return CSYNC_FILE_EXCLUDE_AND_REMOVE; - } - - // third capture: full path matching is triggered - QString pathStr = QString::fromUtf8(path); + QByteArray basePath(_localPath.toUtf8() + path); + while (basePath.size() > _localPath.size()) { + basePath = leftIncludeLast(basePath, '/'); + QRegularExpressionMatch m; + if (filetype == ItemTypeDirectory + && _bnameTraversalRegexDir.contains(basePath)) { + m = _bnameTraversalRegexDir[basePath].match(bnameStr); + } else if (filetype == ItemTypeFile + && _bnameTraversalRegexFile.contains(basePath)) { + m = _bnameTraversalRegexFile[basePath].match(bnameStr); + } else { + continue; + } - if (filetype == ItemTypeDirectory) { - m = _fullTraversalRegexDir.match(pathStr); - } else { - m = _fullTraversalRegexFile.match(pathStr); - } - if (m.hasMatch()) { + if (!m.hasMatch()) + return CSYNC_NOT_EXCLUDED; if (m.capturedStart(QStringLiteral("exclude")) != -1) { return CSYNC_FILE_EXCLUDE_LIST; } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { return CSYNC_FILE_EXCLUDE_AND_REMOVE; } } + + // third capture: full path matching is triggered + QString pathStr = QString::fromUtf8(path); + basePath = _localPath.toUtf8() + path; + while (basePath.size() > _localPath.size()) { + basePath = leftIncludeLast(basePath, '/'); + QRegularExpressionMatch m; + if (filetype == ItemTypeDirectory + && _fullTraversalRegexDir.contains(basePath)) { + m = _fullTraversalRegexDir[basePath].match(pathStr); + } else if (filetype == ItemTypeFile + && _fullTraversalRegexFile.contains(basePath)) { + m = _fullTraversalRegexFile[basePath].match(pathStr); + } else { + continue; + } + + if (m.hasMatch()) { + if (m.capturedStart(QStringLiteral("exclude")) != -1) { + return CSYNC_FILE_EXCLUDE_LIST; + } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { + return CSYNC_FILE_EXCLUDE_AND_REMOVE; + } + } + } return CSYNC_NOT_EXCLUDED; } @@ -400,23 +462,38 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::fullPatternMatch(const char *path, ItemType fi return CSYNC_NOT_EXCLUDED; QString p = QString::fromUtf8(path); - QRegularExpressionMatch m; - if (filetype == ItemTypeDirectory) { - m = _fullRegexDir.match(p); - } else { - m = _fullRegexFile.match(p); - } - if (m.hasMatch()) { - if (m.capturedStart(QStringLiteral("exclude")) != -1) { - return CSYNC_FILE_EXCLUDE_LIST; - } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { - return CSYNC_FILE_EXCLUDE_AND_REMOVE; + // `path` seems to always be relative to `_localPath`, the tests however have not been + // written that way... this makes the tests happy for now. TODO Fix the tests at some point + if (path[0] == '/') + ++path; + + QByteArray basePath(_localPath.toUtf8() + path); + while (basePath.size() > _localPath.size()) { + basePath = leftIncludeLast(basePath, '/'); + QRegularExpressionMatch m; + if (filetype == ItemTypeDirectory + && _fullRegexDir.contains(basePath)) { + m = _fullRegexDir[basePath].match(p); + } else if (filetype == ItemTypeFile + && _fullRegexFile.contains(basePath)) { + m = _fullRegexFile[basePath].match(p); + } else { + continue; + } + + if (m.hasMatch()) { + if (m.capturedStart(QStringLiteral("exclude")) != -1) { + return CSYNC_FILE_EXCLUDE_LIST; + } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { + return CSYNC_FILE_EXCLUDE_AND_REMOVE; + } } } + return CSYNC_NOT_EXCLUDED; } -auto ExcludedFiles::csyncTraversalMatchFun() const +auto ExcludedFiles::csyncTraversalMatchFun() -> std::function { return [this](const char *path, ItemType filetype) { return this->traversalPatternMatch(path, filetype); }; @@ -555,6 +632,22 @@ static QString extractBnameTrigger(const QString &exclude, bool wildcardsMatchSl void ExcludedFiles::prepare() { + // clear all regex + _bnameTraversalRegexFile.clear(); + _bnameTraversalRegexDir.clear(); + _fullTraversalRegexFile.clear(); + _fullTraversalRegexDir.clear(); + _fullRegexFile.clear(); + _fullRegexDir.clear(); + + for (auto const & basePath : _allExcludes.keys()) + prepare(basePath); +} + +void ExcludedFiles::prepare(const BasePathByteArray & basePath) +{ + Q_ASSERT(_allExcludes.contains(basePath)); + // Build regular expressions for the different cases. // // To compose the _bnameTraversalRegex, _fullTraversalRegex and _fullRegex @@ -596,7 +689,7 @@ void ExcludedFiles::prepare() pattern.append(appendMe); }; - for (auto exclude : _allExcludes) { + for (auto exclude : _allExcludes.value(basePath)) { if (exclude[0] == '\n') continue; // empty line if (exclude[0] == '\r') @@ -654,11 +747,11 @@ void ExcludedFiles::prepare() // (exclude)|(excluderemove)|(bname triggers). // If the third group matches, the fullActivatedRegex needs to be applied // to the full path. - _bnameTraversalRegexFile.setPattern( + _bnameTraversalRegexFile[basePath].setPattern( "^(?P" + bnameFileDirKeep + ")$|" + "^(?P" + bnameFileDirRemove + ")$|" + "^(?P" + bnameTriggerFileDir + ")$"); - _bnameTraversalRegexDir.setPattern( + _bnameTraversalRegexDir[basePath].setPattern( "^(?P" + bnameFileDirKeep + "|" + bnameDirKeep + ")$|" + "^(?P" + bnameFileDirRemove + "|" + bnameDirRemove + ")$|" + "^(?P" + bnameTriggerFileDir + "|" + bnameTriggerDir + ")$"); @@ -667,13 +760,13 @@ void ExcludedFiles::prepare() // the bname regex matches. Its basic form is (exclude)|(excluderemove)". // This pattern can be much simpler than fullRegex since we can assume a traversal // situation and doesn't need to look for bname patterns in parent paths. - _fullTraversalRegexFile.setPattern( + _fullTraversalRegexFile[basePath].setPattern( QLatin1String("") // Full patterns are anchored to the beginning + "^(?P" + fullFileDirKeep + ")(?:$|/)" + "|" + "^(?P" + fullFileDirRemove + ")(?:$|/)"); - _fullTraversalRegexDir.setPattern( + _fullTraversalRegexDir[basePath].setPattern( QLatin1String("") + "^(?P" + fullFileDirKeep + "|" + fullDirKeep + ")(?:$|/)" + "|" @@ -681,7 +774,7 @@ void ExcludedFiles::prepare() // The full regex is applied to the full path and incorporates both bname and // full-path patterns. It has the form "(exclude)|(excluderemove)". - _fullRegexFile.setPattern( + _fullRegexFile[basePath].setPattern( QLatin1String("(?P") // Full patterns are anchored to the beginning + "^(?:" + fullFileDirKeep + ")(?:$|/)" + "|" @@ -697,7 +790,7 @@ void ExcludedFiles::prepare() + "(?:^|/)(?:" + bnameFileDirRemove + ")(?:$|/)" + "|" + "(?:^|/)(?:" + bnameDirRemove + ")/" + ")"); - _fullRegexDir.setPattern( + _fullRegexDir[basePath].setPattern( QLatin1String("(?P") + "^(?:" + fullFileDirKeep + "|" + fullDirKeep + ")(?:$|/)" + "|" + "(?:^|/)(?:" + bnameFileDirKeep + "|" + bnameDirKeep + ")(?:$|/)" @@ -711,16 +804,16 @@ void ExcludedFiles::prepare() QRegularExpression::PatternOptions patternOptions = QRegularExpression::NoPatternOption; if (OCC::Utility::fsCasePreserving()) patternOptions |= QRegularExpression::CaseInsensitiveOption; - _bnameTraversalRegexFile.setPatternOptions(patternOptions); - _bnameTraversalRegexFile.optimize(); - _bnameTraversalRegexDir.setPatternOptions(patternOptions); - _bnameTraversalRegexDir.optimize(); - _fullTraversalRegexFile.setPatternOptions(patternOptions); - _fullTraversalRegexFile.optimize(); - _fullTraversalRegexDir.setPatternOptions(patternOptions); - _fullTraversalRegexDir.optimize(); - _fullRegexFile.setPatternOptions(patternOptions); - _fullRegexFile.optimize(); - _fullRegexDir.setPatternOptions(patternOptions); - _fullRegexDir.optimize(); + _bnameTraversalRegexFile[basePath].setPatternOptions(patternOptions); + _bnameTraversalRegexFile[basePath].optimize(); + _bnameTraversalRegexDir[basePath].setPatternOptions(patternOptions); + _bnameTraversalRegexDir[basePath].optimize(); + _fullTraversalRegexFile[basePath].setPatternOptions(patternOptions); + _fullTraversalRegexFile[basePath].optimize(); + _fullTraversalRegexDir[basePath].setPatternOptions(patternOptions); + _fullTraversalRegexDir[basePath].optimize(); + _fullRegexFile[basePath].setPatternOptions(patternOptions); + _fullRegexFile[basePath].optimize(); + _fullRegexDir[basePath].setPatternOptions(patternOptions); + _fullRegexDir[basePath].optimize(); } diff --git a/src/csync/csync_exclude.h b/src/csync/csync_exclude.h index 249ec7bff..5e2d3109f 100644 --- a/src/csync/csync_exclude.h +++ b/src/csync/csync_exclude.h @@ -66,7 +66,7 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject { Q_OBJECT public: - ExcludedFiles(); + ExcludedFiles(QString localPath = "/"); ~ExcludedFiles(); /** @@ -75,6 +75,7 @@ public: * Does not load the file. Use reloadExcludeFiles() afterwards. */ void addExcludeFilePath(const QString &path); + void addInTreeExcludeFilePath(const QString &path); /** * Whether conflict files shall be excluded. @@ -95,12 +96,12 @@ public: bool excludeHidden) const; /** - * Adds an exclude pattern. + * Adds an exclude pattern anchored to base path * * Primarily used in tests. Patterns added this way are preserved when * reloadExcludeFiles() is called. */ - void addManualExclude(const QByteArray &expr); + void addManualExclude(const QByteArray &expr, const QByteArray &basePath = "/"); /** * Removes all manually added exclude patterns. @@ -121,7 +122,7 @@ public: * Careful: The function will only be valid for as long as this * ExcludedFiles instance stays alive. */ - auto csyncTraversalMatchFun() const + auto csyncTraversalMatchFun() -> std::function; public slots: @@ -156,10 +157,32 @@ private: * Note that this only matches patterns. It does not check whether the file * or directory pointed to is hidden (or whether it even exists). */ - CSYNC_EXCLUDE_TYPE traversalPatternMatch(const char *path, ItemType filetype) const; + CSYNC_EXCLUDE_TYPE traversalPatternMatch(const char *path, ItemType filetype); + + // Our BasePath need to end with '/' + class BasePathByteArray : public QByteArray + { + public: + BasePathByteArray(QByteArray && other) + : QByteArray(std::move(other)) + { + Q_ASSERT(this->endsWith('/')); + } + + BasePathByteArray(const QByteArray & other) + : QByteArray(other) + { + Q_ASSERT(this->endsWith('/')); + } + + BasePathByteArray(const char * data, int size = -1) + : BasePathByteArray(QByteArray(data, size)) + { + } + }; /** - * Generate optimized regular expressions for the exclude patterns. + * Generate optimized regular expressions for the exclude patterns anchored to basePath. * * The optimization works in two steps: First, all supported patterns are put * into _fullRegexFile/_fullRegexDir. These regexes can be applied to the full @@ -187,24 +210,28 @@ private: * full matcher would exclude. Example: "b" is excluded. traversal("b/c") * returns not-excluded because "c" isn't a bname activation pattern. */ + void prepare(const BasePathByteArray & basePath); + void prepare(); + + QString _localPath; /// Files to load excludes from - QSet _excludeFiles; + QMap> _excludeFiles; /// Exclude patterns added with addManualExclude() - QList _manualExcludes; + QMap> _manualExcludes; /// List of all active exclude patterns - QList _allExcludes; + QMap> _allExcludes; /// see prepare() - QRegularExpression _bnameTraversalRegexFile; - QRegularExpression _bnameTraversalRegexDir; - QRegularExpression _fullTraversalRegexFile; - QRegularExpression _fullTraversalRegexDir; - QRegularExpression _fullRegexFile; - QRegularExpression _fullRegexDir; + QMap _bnameTraversalRegexFile; + QMap _bnameTraversalRegexDir; + QMap _fullTraversalRegexFile; + QMap _fullTraversalRegexDir; + QMap _fullRegexFile; + QMap _fullRegexDir; bool _excludeConflictFiles = true; diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index d9c240a93..3e2624a21 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -91,7 +91,7 @@ SyncEngine::SyncEngine(AccountPtr account, const QString &localPath, _csync_ctx.reset(new CSYNC(localPath.toUtf8().data(), journal)); - _excludedFiles.reset(new ExcludedFiles); + _excludedFiles.reset(new ExcludedFiles(localPath)); _csync_ctx->exclude_traversal_fn = _excludedFiles->csyncTraversalMatchFun(); _syncFileStatusTracker.reset(new SyncFileStatusTracker(this)); diff --git a/test/csync/csync_tests/check_csync_exclude.cpp b/test/csync/csync_tests/check_csync_exclude.cpp index 95b6e9a31..bfbf36095 100644 --- a/test/csync/csync_tests/check_csync_exclude.cpp +++ b/test/csync/csync_tests/check_csync_exclude.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #define CSYNC_TEST 1 #include "csync_exclude.cpp" @@ -115,16 +116,27 @@ static void check_csync_exclude_add(void **) excludedFiles->addManualExclude("/tmp/check_csync1/*"); assert_int_equal(check_file_full("/tmp/check_csync1/foo"), CSYNC_FILE_EXCLUDE_LIST); assert_int_equal(check_file_full("/tmp/check_csync2/foo"), CSYNC_NOT_EXCLUDED); - assert_true(excludedFiles->_allExcludes.contains("/tmp/check_csync1/*")); + assert_true(excludedFiles->_allExcludes["/"].contains("/tmp/check_csync1/*")); - assert_true(excludedFiles->_fullRegexFile.pattern().contains("csync1")); - assert_true(excludedFiles->_fullTraversalRegexFile.pattern().contains("csync1")); - assert_false(excludedFiles->_bnameTraversalRegexFile.pattern().contains("csync1")); + assert_true(excludedFiles->_fullRegexFile["/"].pattern().contains("csync1")); + assert_true(excludedFiles->_fullTraversalRegexFile["/"].pattern().contains("csync1")); + assert_false(excludedFiles->_bnameTraversalRegexFile["/"].pattern().contains("csync1")); excludedFiles->addManualExclude("foo"); - assert_true(excludedFiles->_bnameTraversalRegexFile.pattern().contains("foo")); - assert_true(excludedFiles->_fullRegexFile.pattern().contains("foo")); - assert_false(excludedFiles->_fullTraversalRegexFile.pattern().contains("foo")); + assert_true(excludedFiles->_bnameTraversalRegexFile["/"].pattern().contains("foo")); + assert_true(excludedFiles->_fullRegexFile["/"].pattern().contains("foo")); + assert_false(excludedFiles->_fullTraversalRegexFile["/"].pattern().contains("foo")); +} + +static void check_csync_exclude_add_per_dir(void **) +{ + excludedFiles->addManualExclude("*", "/tmp/check_csync1/"); + assert_int_equal(check_file_full("/tmp/check_csync1/foo"), CSYNC_FILE_EXCLUDE_LIST); + assert_int_equal(check_file_full("/tmp/check_csync2/foo"), CSYNC_NOT_EXCLUDED); + assert_true(excludedFiles->_allExcludes["/tmp/check_csync1/"].contains("*")); + + excludedFiles->addManualExclude("foo"); + assert_true(excludedFiles->_fullRegexFile["/"].pattern().contains("foo")); } static void check_csync_excluded(void **) @@ -232,6 +244,41 @@ static void check_csync_excluded(void **) assert_int_equal(check_file_full("c [d]"), CSYNC_FILE_EXCLUDE_LIST); } +static void check_csync_excluded_per_dir(void **) +{ + excludedFiles->addManualExclude("A"); + excludedFiles->reloadExcludeFiles(); + + assert_int_equal(check_file_full("A"), CSYNC_FILE_EXCLUDE_LIST); + + excludedFiles->clearManualExcludes(); + excludedFiles->addManualExclude("A", "/B/"); + excludedFiles->reloadExcludeFiles(); + + assert_int_equal(check_file_full("A"), CSYNC_NOT_EXCLUDED); + assert_int_equal(check_file_full("B/A"), CSYNC_FILE_EXCLUDE_LIST); + +#define FOO_DIR "/tmp/check_csync1/foo" +#define FOO_EXCLUDE_LIST FOO_DIR "/.sync-exclude.lst" + int rc; + rc = system("mkdir -p " FOO_DIR); + assert_int_equal(rc, 0); + FILE *fh = fopen(FOO_EXCLUDE_LIST, "w"); + assert_non_null(fh); + rc = fprintf(fh, "bar"); + assert_int_not_equal(rc, 0); + rc = fclose(fh); + assert_int_equal(rc, 0); + + excludedFiles->addInTreeExcludeFilePath(FOO_EXCLUDE_LIST); + excludedFiles->reloadExcludeFiles(); + assert_int_equal(check_file_full(FOO_DIR), CSYNC_NOT_EXCLUDED); + assert_int_equal(check_file_full(FOO_DIR "/bar"), CSYNC_FILE_EXCLUDE_LIST); + assert_int_equal(check_file_full(FOO_DIR "/baz"), CSYNC_NOT_EXCLUDED); +#undef FOO_DIR +#undef FOO_EXCLUDE_LIST +} + static void check_csync_excluded_traversal(void **) { assert_int_equal(check_file_traversal(""), CSYNC_NOT_EXCLUDED); @@ -633,7 +680,9 @@ int torture_run_tests(void) const struct CMUnitTest tests[] = { cmocka_unit_test_setup_teardown(T::check_csync_exclude_add, T::setup, T::teardown), + cmocka_unit_test_setup_teardown(T::check_csync_exclude_add_per_dir, T::setup, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_excluded, T::setup_init, T::teardown), + cmocka_unit_test_setup_teardown(T::check_csync_excluded_per_dir, T::setup, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_excluded_traversal, T::setup_init, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_dir_only, T::setup, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_pathes, T::setup_init, T::teardown), -- cgit v1.2.3 From 4a2b91a043625d72f7aff1d82ac51df36ae572ee Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Tue, 13 Aug 2019 21:50:48 +0100 Subject: IgnoreListEditor can work on any path Signed-off-by: Samir Benmendil --- src/gui/generalsettings.cpp | 3 ++- src/gui/ignorelisteditor.cpp | 28 +++++++++++++++++----------- src/gui/ignorelisteditor.h | 3 ++- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index b8c10d592..07cf2dc61 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -184,7 +184,8 @@ void GeneralSettings::slotShowInExplorerNavigationPane(bool checked) void GeneralSettings::slotIgnoreFilesEditor() { if (_ignoreEditor.isNull()) { - _ignoreEditor = new IgnoreListEditor(this); + ConfigFile cfgFile; + _ignoreEditor = new IgnoreListEditor(cfgFile.excludeFile(ConfigFile::UserScope), this); _ignoreEditor->setAttribute(Qt::WA_DeleteOnClose, true); _ignoreEditor->open(); } else { diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index 9b44c25a0..6f8fa5e37 100644 --- a/src/gui/ignorelisteditor.cpp +++ b/src/gui/ignorelisteditor.cpp @@ -14,8 +14,9 @@ #include "configfile.h" -#include "ignorelisteditor.h" #include "folderman.h" +#include "generalsettings.h" +#include "ignorelisteditor.h" #include "ui_ignorelisteditor.h" #include @@ -31,9 +32,10 @@ static int patternCol = 0; static int deletableCol = 1; static int readOnlyRows = 3; -IgnoreListEditor::IgnoreListEditor(QWidget *parent) +IgnoreListEditor::IgnoreListEditor(QString ignoreFile, QWidget *parent) : QDialog(parent) , ui(new Ui::IgnoreListEditor) + , m_ignoreFile(std::move(ignoreFile)) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->setupUi(this); @@ -43,12 +45,16 @@ IgnoreListEditor::IgnoreListEditor(QWidget *parent) "Items where deletion is allowed will be deleted if they prevent a " "directory from being removed. " "This is useful for meta data.")); + //FIXME This is not true. The entries are hardcoded below in setupTableReadOnlyItems readOnlyTooltip = tr("This entry is provided by the system at '%1' " "and cannot be modified in this view.") .arg(QDir::toNativeSeparators(cfgFile.excludeFile(ConfigFile::SystemScope))); - setupTableReadOnlyItems(); - readIgnoreFile(cfgFile.excludeFile(ConfigFile::UserScope), false); + //TODO this is a bit hacky but is an easy way to figure out if this is created from the + //GeneralSettings or not. Until it gets refactored into a separate widged. + if (qobject_cast(parent)) + setupTableReadOnlyItems(); + readIgnoreFile(m_ignoreFile, false); connect(this, &QDialog::accepted, this, &IgnoreListEditor::slotUpdateLocalIgnoreList); ui->removePushButton->setEnabled(false); @@ -105,17 +111,16 @@ void IgnoreListEditor::slotRemoveCurrentItem() void IgnoreListEditor::slotRemoveAllItems() { ui->tableWidget->clearContents(); - setupTableReadOnlyItems(); + if (qobject_cast(parent())) + setupTableReadOnlyItems(); } void IgnoreListEditor::slotUpdateLocalIgnoreList() { - ConfigFile cfgFile; - QString ignoreFile = cfgFile.excludeFile(ConfigFile::UserScope); - QFile ignores(ignoreFile); + QFile ignores(m_ignoreFile); if (ignores.open(QIODevice::WriteOnly)) { // rewrites the whole file since now the user can also remove system patterns - QFile::resize(ignoreFile, 0); + QFile::resize(m_ignoreFile, 0); for (int row = 0; row < ui->tableWidget->rowCount(); ++row) { QTableWidgetItem *patternItem = ui->tableWidget->item(row, patternCol); QTableWidgetItem *deletableItem = ui->tableWidget->item(row, deletableCol); @@ -131,7 +136,7 @@ void IgnoreListEditor::slotUpdateLocalIgnoreList() } } else { QMessageBox::warning(this, tr("Could not open file"), - tr("Cannot write changes to '%1'.").arg(ignoreFile)); + tr("Cannot write changes to '%1'.").arg(m_ignoreFile)); } ignores.close(); //close the file before reloading stuff. @@ -170,7 +175,8 @@ void IgnoreListEditor::slotAddPattern() void IgnoreListEditor::slotRestoreDefaults(QAbstractButton *button){ if(ui->buttonBox->buttonRole(button) == QDialogButtonBox::ResetRole){ ConfigFile cfgFile; - setupTableReadOnlyItems(); + if (qobject_cast(parent())) + setupTableReadOnlyItems(); readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); } } diff --git a/src/gui/ignorelisteditor.h b/src/gui/ignorelisteditor.h index 74137a736..f18709e6d 100644 --- a/src/gui/ignorelisteditor.h +++ b/src/gui/ignorelisteditor.h @@ -35,7 +35,7 @@ class IgnoreListEditor : public QDialog Q_OBJECT public: - explicit IgnoreListEditor(QWidget *parent = nullptr); + IgnoreListEditor(QString ignoreFile, QWidget *parent = nullptr); ~IgnoreListEditor(); bool ignoreHiddenFiles(); @@ -54,6 +54,7 @@ private: int addPattern(const QString &pattern, bool deletable, bool readOnly); QString readOnlyTooltip; Ui::IgnoreListEditor *ui; + QString m_ignoreFile; }; } // namespace OCC -- cgit v1.2.3 From e44a2302de26ce29d36633864295efb917f9ad99 Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Tue, 13 Aug 2019 23:50:18 +0100 Subject: Remove all rows in the table widget `clearContents()` will leave the number of rows as is. This was causing a segfault when trying to loop over the items of the widget. Signed-off-by: Samir Benmendil --- src/gui/ignorelisteditor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index 6f8fa5e37..df8726a70 100644 --- a/src/gui/ignorelisteditor.cpp +++ b/src/gui/ignorelisteditor.cpp @@ -110,7 +110,7 @@ void IgnoreListEditor::slotRemoveCurrentItem() void IgnoreListEditor::slotRemoveAllItems() { - ui->tableWidget->clearContents(); + ui->tableWidget->setRowCount(0); if (qobject_cast(parent())) setupTableReadOnlyItems(); } -- cgit v1.2.3 From d4816442efa0dacb6a5cec00a9bac6f685a239c3 Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Wed, 14 Aug 2019 22:09:19 +0100 Subject: Extract IgnoreListTableWidget to be reused Signed-off-by: Samir Benmendil --- src/gui/CMakeLists.txt | 2 + src/gui/generalsettings.cpp | 2 +- src/gui/ignorelisteditor.cpp | 182 +++++--------------------------------- src/gui/ignorelisteditor.h | 10 +-- src/gui/ignorelisteditor.ui | 100 +++------------------ src/gui/ignorelisttablewidget.cpp | 167 ++++++++++++++++++++++++++++++++++ src/gui/ignorelisttablewidget.h | 38 ++++++++ src/gui/ignorelisttablewidget.ui | 112 +++++++++++++++++++++++ 8 files changed, 351 insertions(+), 262 deletions(-) create mode 100644 src/gui/ignorelisttablewidget.cpp create mode 100644 src/gui/ignorelisttablewidget.h create mode 100644 src/gui/ignorelisttablewidget.ui diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 167d1727f..ff78ca1bd 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -22,6 +22,7 @@ set(client_UI_SRCS generalsettings.ui legalnotice.ui ignorelisteditor.ui + ignorelisttablewidget.ui networksettings.ui activitywidget.ui synclogdialog.ui @@ -59,6 +60,7 @@ set(client_SRCS generalsettings.cpp legalnotice.cpp ignorelisteditor.cpp + ignorelisttablewidget.cpp lockwatcher.cpp logbrowser.cpp navigationpanehelper.cpp diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index 07cf2dc61..dd4235834 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -185,7 +185,7 @@ void GeneralSettings::slotIgnoreFilesEditor() { if (_ignoreEditor.isNull()) { ConfigFile cfgFile; - _ignoreEditor = new IgnoreListEditor(cfgFile.excludeFile(ConfigFile::UserScope), this); + _ignoreEditor = new IgnoreListEditor(this); _ignoreEditor->setAttribute(Qt::WA_DeleteOnClose, true); _ignoreEditor->open(); } else { diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index df8726a70..22182b5a0 100644 --- a/src/gui/ignorelisteditor.cpp +++ b/src/gui/ignorelisteditor.cpp @@ -28,45 +28,28 @@ namespace OCC { -static int patternCol = 0; -static int deletableCol = 1; -static int readOnlyRows = 3; - -IgnoreListEditor::IgnoreListEditor(QString ignoreFile, QWidget *parent) +IgnoreListEditor::IgnoreListEditor(QWidget *parent) : QDialog(parent) , ui(new Ui::IgnoreListEditor) - , m_ignoreFile(std::move(ignoreFile)) { setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->setupUi(this); ConfigFile cfgFile; - ui->descriptionLabel->setText(tr("Files or folders matching a pattern will not be synchronized.\n\n" - "Items where deletion is allowed will be deleted if they prevent a " - "directory from being removed. " - "This is useful for meta data.")); //FIXME This is not true. The entries are hardcoded below in setupTableReadOnlyItems readOnlyTooltip = tr("This entry is provided by the system at '%1' " "and cannot be modified in this view.") .arg(QDir::toNativeSeparators(cfgFile.excludeFile(ConfigFile::SystemScope))); - //TODO this is a bit hacky but is an easy way to figure out if this is created from the - //GeneralSettings or not. Until it gets refactored into a separate widged. - if (qobject_cast(parent)) - setupTableReadOnlyItems(); - readIgnoreFile(m_ignoreFile, false); - - connect(this, &QDialog::accepted, this, &IgnoreListEditor::slotUpdateLocalIgnoreList); - ui->removePushButton->setEnabled(false); - connect(ui->tableWidget, &QTableWidget::itemSelectionChanged, this, &IgnoreListEditor::slotItemSelectionChanged); - connect(ui->removePushButton, &QAbstractButton::clicked, this, &IgnoreListEditor::slotRemoveCurrentItem); - connect(ui->addPushButton, &QAbstractButton::clicked, this, &IgnoreListEditor::slotAddPattern); - connect(ui->removeAllPushButton, &QAbstractButton::clicked, this, &IgnoreListEditor::slotRemoveAllItems); - connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &IgnoreListEditor::slotRestoreDefaults); + setupTableReadOnlyItems(); + const auto userConfig = cfgFile.excludeFile(ConfigFile::Scope::UserScope); + ui->ignoreTableWidget->readIgnoreFile(userConfig); - ui->tableWidget->resizeColumnsToContents(); - ui->tableWidget->horizontalHeader()->setSectionResizeMode(patternCol, QHeaderView::Stretch); - ui->tableWidget->verticalHeader()->setVisible(false); + connect(this, &QDialog::accepted, [=]() { + ui->ignoreTableWidget->slotWriteIgnoreFile(userConfig); + }); + connect(ui->buttonBox, &QDialogButtonBox::clicked, + this, &IgnoreListEditor::slotRestoreDefaults); ui->syncHiddenFilesCheckBox->setChecked(!FolderMan::instance()->ignoreHiddenFiles()); } @@ -76,12 +59,11 @@ IgnoreListEditor::~IgnoreListEditor() delete ui; } -void IgnoreListEditor::setupTableReadOnlyItems(){ - ui->tableWidget->setRowCount(0); - addPattern(".csync_journal.db*", /*deletable=*/false, /*readonly=*/true); - addPattern("._sync_*.db*", /*deletable=*/false, /*readonly=*/true); - addPattern(".sync_*.db*", /*deletable=*/false, /*readonly=*/true); - ui->removeAllPushButton->setEnabled(false); +void IgnoreListEditor::setupTableReadOnlyItems() +{ + ui->ignoreTableWidget->addPattern(".csync_journal.db*", /*deletable=*/false, /*readonly=*/true); + ui->ignoreTableWidget->addPattern("._sync_*.db*", /*deletable=*/false, /*readonly=*/true); + ui->ignoreTableWidget->addPattern(".sync_*.db*", /*deletable=*/false, /*readonly=*/true); } bool IgnoreListEditor::ignoreHiddenFiles() @@ -89,140 +71,16 @@ bool IgnoreListEditor::ignoreHiddenFiles() return !ui->syncHiddenFilesCheckBox->isChecked(); } -void IgnoreListEditor::slotItemSelectionChanged() +void IgnoreListEditor::slotRestoreDefaults(QAbstractButton *button) { - QTableWidgetItem *item = ui->tableWidget->currentItem(); - if (!item) { - ui->removePushButton->setEnabled(false); + if(ui->buttonBox->buttonRole(button) != QDialogButtonBox::ResetRole) return; - } - - bool enable = item->flags() & Qt::ItemIsEnabled; - ui->removePushButton->setEnabled(enable); -} - -void IgnoreListEditor::slotRemoveCurrentItem() -{ - ui->tableWidget->removeRow(ui->tableWidget->currentRow()); - if(ui->tableWidget->rowCount() == readOnlyRows) - ui->removeAllPushButton->setEnabled(false); -} -void IgnoreListEditor::slotRemoveAllItems() -{ - ui->tableWidget->setRowCount(0); - if (qobject_cast(parent())) - setupTableReadOnlyItems(); -} - -void IgnoreListEditor::slotUpdateLocalIgnoreList() -{ - QFile ignores(m_ignoreFile); - if (ignores.open(QIODevice::WriteOnly)) { - // rewrites the whole file since now the user can also remove system patterns - QFile::resize(m_ignoreFile, 0); - for (int row = 0; row < ui->tableWidget->rowCount(); ++row) { - QTableWidgetItem *patternItem = ui->tableWidget->item(row, patternCol); - QTableWidgetItem *deletableItem = ui->tableWidget->item(row, deletableCol); - if (patternItem->flags() & Qt::ItemIsEnabled) { - QByteArray prepend; - if (deletableItem->checkState() == Qt::Checked) { - prepend = "]"; - } else if (patternItem->text().startsWith('#')) { - prepend = "\\"; - } - ignores.write(prepend + patternItem->text().toUtf8() + '\n'); - } - } - } else { - QMessageBox::warning(this, tr("Could not open file"), - tr("Cannot write changes to '%1'.").arg(m_ignoreFile)); - } - ignores.close(); //close the file before reloading stuff. - - FolderMan *folderMan = FolderMan::instance(); - - /* handle the hidden file checkbox */ - - /* the ignoreHiddenFiles flag is a folder specific setting, but for now, it is - * handled globally. Save it to every folder that is defined. - */ - folderMan->setIgnoreHiddenFiles(ignoreHiddenFiles()); - - // We need to force a remote discovery after a change of the ignore list. - // Otherwise we would not download the files/directories that are no longer - // ignored (because the remote etag did not change) (issue #3172) - foreach (Folder *folder, folderMan->map()) { - folder->journalDb()->forceRemoteDiscoveryNextSync(); - folderMan->scheduleFolder(folder); - } -} - -void IgnoreListEditor::slotAddPattern() -{ - bool okClicked; - QString pattern = QInputDialog::getText(this, tr("Add Ignore Pattern"), - tr("Add a new ignore pattern:"), - QLineEdit::Normal, QString(), &okClicked); + ui->ignoreTableWidget->slotRemoveAllItems(); - if (!okClicked || pattern.isEmpty()) - return; - - addPattern(pattern, false, false); - ui->tableWidget->scrollToBottom(); -} - -void IgnoreListEditor::slotRestoreDefaults(QAbstractButton *button){ - if(ui->buttonBox->buttonRole(button) == QDialogButtonBox::ResetRole){ - ConfigFile cfgFile; - if (qobject_cast(parent())) - setupTableReadOnlyItems(); - readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); - } -} - -void IgnoreListEditor::readIgnoreFile(const QString &file, bool readOnly) -{ - QFile ignores(file); - if (ignores.open(QIODevice::ReadOnly)) { - while (!ignores.atEnd()) { - QString line = QString::fromUtf8(ignores.readLine()); - line.chop(1); - if (!line.isEmpty() && !line.startsWith("#")) { - bool deletable = false; - if (line.startsWith(']')) { - deletable = true; - line = line.mid(1); - } - addPattern(line, deletable, readOnly); - } - } - } -} - -int IgnoreListEditor::addPattern(const QString &pattern, bool deletable, bool readOnly) -{ - int newRow = ui->tableWidget->rowCount(); - ui->tableWidget->setRowCount(newRow + 1); - - QTableWidgetItem *patternItem = new QTableWidgetItem; - patternItem->setText(pattern); - ui->tableWidget->setItem(newRow, patternCol, patternItem); - - QTableWidgetItem *deletableItem = new QTableWidgetItem; - deletableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); - deletableItem->setCheckState(deletable ? Qt::Checked : Qt::Unchecked); - ui->tableWidget->setItem(newRow, deletableCol, deletableItem); - - if (readOnly) { - patternItem->setFlags(patternItem->flags() ^ Qt::ItemIsEnabled); - patternItem->setToolTip(readOnlyTooltip); - deletableItem->setFlags(deletableItem->flags() ^ Qt::ItemIsEnabled); - } - - ui->removeAllPushButton->setEnabled(true); - - return newRow; + ConfigFile cfgFile; + setupTableReadOnlyItems(); + ui->ignoreTableWidget->readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); } } // namespace OCC diff --git a/src/gui/ignorelisteditor.h b/src/gui/ignorelisteditor.h index f18709e6d..39c38d74d 100644 --- a/src/gui/ignorelisteditor.h +++ b/src/gui/ignorelisteditor.h @@ -35,26 +35,18 @@ class IgnoreListEditor : public QDialog Q_OBJECT public: - IgnoreListEditor(QString ignoreFile, QWidget *parent = nullptr); + IgnoreListEditor(QWidget *parent = nullptr); ~IgnoreListEditor(); bool ignoreHiddenFiles(); private slots: - void slotItemSelectionChanged(); - void slotRemoveCurrentItem(); - void slotUpdateLocalIgnoreList(); - void slotAddPattern(); void slotRestoreDefaults(QAbstractButton *button); - void slotRemoveAllItems(); private: - void readIgnoreFile(const QString &file, bool readOnly); void setupTableReadOnlyItems(); - int addPattern(const QString &pattern, bool deletable, bool readOnly); QString readOnlyTooltip; Ui::IgnoreListEditor *ui; - QString m_ignoreFile; }; } // namespace OCC diff --git a/src/gui/ignorelisteditor.ui b/src/gui/ignorelisteditor.ui index 8e544a911..891fbc6e9 100644 --- a/src/gui/ignorelisteditor.ui +++ b/src/gui/ignorelisteditor.ui @@ -36,96 +36,8 @@ Files Ignored by Patterns - - - - true - - - - 0 - 0 - - - - - - - Qt::PlainText - - - true - - - - - - - true - - - Qt::Vertical - - - - 20 - 213 - - - - - - - - true - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - 2 - - - - Pattern - - - - - Allow Deletion - - - - - - - - true - - - Remove - - - - - - - true - - - Add - - - - - - - Remove all - - + + @@ -139,6 +51,14 @@ + + + IgnoreListTableWidget + QWidget +
ignorelisttablewidget.h
+ 1 +
+
diff --git a/src/gui/ignorelisttablewidget.cpp b/src/gui/ignorelisttablewidget.cpp new file mode 100644 index 000000000..67c8ab4f2 --- /dev/null +++ b/src/gui/ignorelisttablewidget.cpp @@ -0,0 +1,167 @@ +#include "ignorelisttablewidget.h" +#include "ui_ignorelisttablewidget.h" + +#include "folderman.h" + +#include +#include +#include +#include + +namespace OCC { + +static constexpr int patternCol = 0; +static constexpr int deletableCol = 1; +static constexpr int readOnlyRows = 3; + +IgnoreListTableWidget::IgnoreListTableWidget(QWidget *parent) + : QWidget(parent) + , ui(new Ui::IgnoreListTableWidget) +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); + + ui->descriptionLabel->setText(tr("Files or folders matching a pattern will not be synchronized.\n\n" + "Items where deletion is allowed will be deleted if they prevent a " + "directory from being removed. " + "This is useful for meta data.")); + + ui->removePushButton->setEnabled(false); + connect(ui->tableWidget, &QTableWidget::itemSelectionChanged, + this, &IgnoreListTableWidget::slotItemSelectionChanged); + connect(ui->removePushButton, &QAbstractButton::clicked, + this, &IgnoreListTableWidget::slotRemoveCurrentItem); + connect(ui->addPushButton, &QAbstractButton::clicked, + this, &IgnoreListTableWidget::slotAddPattern); + connect(ui->removeAllPushButton, &QAbstractButton::clicked, + this, &IgnoreListTableWidget::slotRemoveAllItems); + + ui->tableWidget->resizeColumnsToContents(); + ui->tableWidget->horizontalHeader()->setSectionResizeMode(patternCol, QHeaderView::Stretch); + ui->tableWidget->verticalHeader()->setVisible(false); +} + +IgnoreListTableWidget::~IgnoreListTableWidget() +{ + delete ui; +} + +void IgnoreListTableWidget::slotItemSelectionChanged() +{ + QTableWidgetItem *item = ui->tableWidget->currentItem(); + if (!item) { + ui->removePushButton->setEnabled(false); + return; + } + + bool enable = item->flags() & Qt::ItemIsEnabled; + ui->removePushButton->setEnabled(enable); +} + +void IgnoreListTableWidget::slotRemoveCurrentItem() +{ + ui->tableWidget->removeRow(ui->tableWidget->currentRow()); + if(ui->tableWidget->rowCount() == readOnlyRows) + ui->removeAllPushButton->setEnabled(false); +} + +void IgnoreListTableWidget::slotRemoveAllItems() +{ + ui->tableWidget->setRowCount(0); +} + +void IgnoreListTableWidget::slotWriteIgnoreFile(const QString & file) +{ + QFile ignores(file); + if (ignores.open(QIODevice::WriteOnly)) { + // rewrites the whole file since now the user can also remove system patterns + QFile::resize(file, 0); + for (int row = 0; row < ui->tableWidget->rowCount(); ++row) { + QTableWidgetItem *patternItem = ui->tableWidget->item(row, patternCol); + QTableWidgetItem *deletableItem = ui->tableWidget->item(row, deletableCol); + if (patternItem->flags() & Qt::ItemIsEnabled) { + QByteArray prepend; + if (deletableItem->checkState() == Qt::Checked) { + prepend = "]"; + } else if (patternItem->text().startsWith('#')) { + prepend = "\\"; + } + ignores.write(prepend + patternItem->text().toUtf8() + '\n'); + } + } + } else { + QMessageBox::warning(this, tr("Could not open file"), + tr("Cannot write changes to '%1'.").arg(file)); + } + ignores.close(); //close the file before reloading stuff. + + FolderMan *folderMan = FolderMan::instance(); + + // We need to force a remote discovery after a change of the ignore list. + // Otherwise we would not download the files/directories that are no longer + // ignored (because the remote etag did not change) (issue #3172) + foreach (Folder *folder, folderMan->map()) { + folder->journalDb()->forceRemoteDiscoveryNextSync(); + folderMan->scheduleFolder(folder); + } +} + +void IgnoreListTableWidget::slotAddPattern() +{ + bool okClicked; + QString pattern = QInputDialog::getText(this, tr("Add Ignore Pattern"), + tr("Add a new ignore pattern:"), + QLineEdit::Normal, QString(), &okClicked); + + if (!okClicked || pattern.isEmpty()) + return; + + addPattern(pattern, false, false); + ui->tableWidget->scrollToBottom(); +} + +void IgnoreListTableWidget::readIgnoreFile(const QString &file, bool readOnly) +{ + QFile ignores(file); + if (ignores.open(QIODevice::ReadOnly)) { + while (!ignores.atEnd()) { + QString line = QString::fromUtf8(ignores.readLine()); + line.chop(1); + if (!line.isEmpty() && !line.startsWith("#")) { + bool deletable = false; + if (line.startsWith(']')) { + deletable = true; + line = line.mid(1); + } + addPattern(line, deletable, readOnly); + } + } + } +} + +int IgnoreListTableWidget::addPattern(const QString &pattern, bool deletable, bool readOnly) +{ + int newRow = ui->tableWidget->rowCount(); + ui->tableWidget->setRowCount(newRow + 1); + + QTableWidgetItem *patternItem = new QTableWidgetItem; + patternItem->setText(pattern); + ui->tableWidget->setItem(newRow, patternCol, patternItem); + + QTableWidgetItem *deletableItem = new QTableWidgetItem; + deletableItem->setFlags(Qt::ItemIsUserCheckable | Qt::ItemIsEnabled); + deletableItem->setCheckState(deletable ? Qt::Checked : Qt::Unchecked); + ui->tableWidget->setItem(newRow, deletableCol, deletableItem); + + if (readOnly) { + patternItem->setFlags(patternItem->flags() ^ Qt::ItemIsEnabled); + patternItem->setToolTip(readOnlyTooltip); + deletableItem->setFlags(deletableItem->flags() ^ Qt::ItemIsEnabled); + } + + ui->removeAllPushButton->setEnabled(true); + + return newRow; +} + +} // namespace OCC diff --git a/src/gui/ignorelisttablewidget.h b/src/gui/ignorelisttablewidget.h new file mode 100644 index 000000000..3bded0462 --- /dev/null +++ b/src/gui/ignorelisttablewidget.h @@ -0,0 +1,38 @@ +#pragma once + +#include + +class QAbstractButton; + +namespace OCC { + +namespace Ui { + class IgnoreListTableWidget; +} + +class IgnoreListTableWidget : public QWidget +{ + Q_OBJECT + +public: + IgnoreListTableWidget(QWidget *parent = nullptr); + ~IgnoreListTableWidget(); + + void readIgnoreFile(const QString &file, bool readOnly = false); + int addPattern(const QString &pattern, bool deletable, bool readOnly); + +public slots: + void slotRemoveAllItems(); + void slotWriteIgnoreFile(const QString & file); + +private slots: + void slotItemSelectionChanged(); + void slotRemoveCurrentItem(); + void slotAddPattern(); + +private: + void setupTableReadOnlyItems(); + QString readOnlyTooltip; + Ui::IgnoreListTableWidget *ui; +}; +} // namespace OCC diff --git a/src/gui/ignorelisttablewidget.ui b/src/gui/ignorelisttablewidget.ui new file mode 100644 index 000000000..2a618395a --- /dev/null +++ b/src/gui/ignorelisttablewidget.ui @@ -0,0 +1,112 @@ + + + OCC::IgnoreListTableWidget + + + + 0 + 0 + 342 + 378 + + + + IgnoreListTableWidget + + + + + + true + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 2 + + + + Pattern + + + + + Allow Deletion + + + + + + + + true + + + Add + + + + + + + true + + + Remove + + + + + + + Remove all + + + + + + + true + + + Qt::Vertical + + + + 20 + 322 + + + + + + + + true + + + + 0 + 0 + + + + + + + Qt::PlainText + + + true + + + + + + + + -- cgit v1.2.3 From acf0b0f7c44992543b8ce23c3e5dd27725cd9b1d Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Wed, 14 Aug 2019 22:27:17 +0100 Subject: Add menu action to each subfolder Signed-off-by: Samir Benmendil --- src/gui/accountsettings.cpp | 34 ++++++++++++++++++++++++++++++++++ src/gui/accountsettings.h | 1 + 2 files changed, 35 insertions(+) diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 5e96ac226..8a37a6c66 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -35,10 +35,12 @@ #include "filesystem.h" #include "clientsideencryptionjobs.h" #include "syncresult.h" +#include "ignorelisttablewidget.h" #include #include +#include #include #include #include @@ -539,6 +541,34 @@ void AccountSettings::slotLockForDecryptionError(const QByteArray& fileId, int h qDebug() << "Error Locking for decryption"; } +void AccountSettings::slotOpenIgnoredFilesDialog(const FolderStatusModel::SubFolderInfo* folderInfo) +{ + const auto syncPath = folderInfo->_folder->path(); + const auto folderPath = folderInfo->_path; + const QString ignoreFile = syncPath + folderPath + ".sync-exclude.lst"; + + auto layout = new QVBoxLayout(); + auto ignoreListWidget = new IgnoreListTableWidget(this); + ignoreListWidget->readIgnoreFile(ignoreFile); + layout->addWidget(ignoreListWidget); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); + layout->addWidget(buttonBox); + + auto dialog = new QDialog(); + dialog->setLayout(layout); + + connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton * button) { + if (buttonBox->buttonRole(button) == QDialogButtonBox::AcceptRole) + ignoreListWidget->slotWriteIgnoreFile(ignoreFile); + dialog->close(); + }); + connect(buttonBox, &QDialogButtonBox::rejected, + dialog, &QDialog::close); + + dialog->open(); +} + void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index, const QPoint& pos) { Q_UNUSED(pos); @@ -567,6 +597,10 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index // connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderDecrypted(info); }); } } + + ac = menu.addAction(tr("Edit Ignored Files")); + connect(ac, &QAction::triggered, [this, &info] { slotOpenIgnoredFilesDialog(info); }); + menu.exec(QCursor::pos()); } diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index eb6a78b17..282ab6aba 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -89,6 +89,7 @@ protected slots: void refreshSelectiveSyncStatus(); void slotMarkSubfolderEncrpted(const FolderStatusModel::SubFolderInfo* folderInfo); void slotMarkSubfolderDecrypted(const FolderStatusModel::SubFolderInfo* folderInfo); + void slotOpenIgnoredFilesDialog(const FolderStatusModel::SubFolderInfo* folderInfo); void slotSubfolderContextMenuRequested(const QModelIndex& idx, const QPoint& point); void slotCustomContextMenuRequested(const QPoint &); void slotFolderListClicked(const QModelIndex &indx); -- cgit v1.2.3 From 94448a8b3315a45def9c07a74f2f2e77c12a8ece Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Wed, 14 Aug 2019 23:04:16 +0100 Subject: Add menu action to main sync folder too Signed-off-by: Samir Benmendil --- src/gui/accountsettings.cpp | 30 +++++++++++++++++++++++++----- src/gui/accountsettings.h | 4 +++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 8a37a6c66..5a0c87769 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -541,12 +541,29 @@ void AccountSettings::slotLockForDecryptionError(const QByteArray& fileId, int h qDebug() << "Error Locking for decryption"; } -void AccountSettings::slotOpenIgnoredFilesDialog(const FolderStatusModel::SubFolderInfo* folderInfo) +void AccountSettings::slotEditCurrentIgnoredFiles() { - const auto syncPath = folderInfo->_folder->path(); - const auto folderPath = folderInfo->_path; - const QString ignoreFile = syncPath + folderPath + ".sync-exclude.lst"; + Folder *f = FolderMan::instance()->folder(selectedFolderAlias()); + if (f == nullptr) + return; + openIgnoredFilesDialog(f->path()); +} + +void AccountSettings::slotEditCurrentLocalIgnoredFiles() +{ + QModelIndex selected = ui->_folderList->selectionModel()->currentIndex(); + if (!selected.isValid() || _model->classify(selected) != FolderStatusModel::SubFolder) + return; + QString fileName = _model->data(selected, FolderStatusDelegate::FolderPathRole).toString(); + openIgnoredFilesDialog(fileName); +} +void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) +{ + Q_ASSERT(absFolderPath.startsWith('/')); + Q_ASSERT(absFolderPath.endsWith('/')); + + const QString ignoreFile = absFolderPath + ".sync-exclude.lst"; auto layout = new QVBoxLayout(); auto ignoreListWidget = new IgnoreListTableWidget(this); ignoreListWidget->readIgnoreFile(ignoreFile); @@ -599,7 +616,7 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index } ac = menu.addAction(tr("Edit Ignored Files")); - connect(ac, &QAction::triggered, [this, &info] { slotOpenIgnoredFilesDialog(info); }); + connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentLocalIgnoredFiles); menu.exec(QCursor::pos()); } @@ -634,6 +651,9 @@ void AccountSettings::slotCustomContextMenuRequested(const QPoint &pos) QAction *ac = menu->addAction(tr("Open folder")); connect(ac, &QAction::triggered, this, &AccountSettings::slotOpenCurrentFolder); + ac = menu->addAction(tr("Edit Ignored Files")); + connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentIgnoredFiles); + if (!ui->_folderList->isExpanded(index)) { ac = menu->addAction(tr("Choose what to sync")); ac->setEnabled(folderConnected); diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 282ab6aba..bd3f0e275 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -80,6 +80,8 @@ protected slots: void slotRemoveCurrentFolder(); void slotOpenCurrentFolder(); // sync folder void slotOpenCurrentLocalSubFolder(); // selected subfolder in sync folder + void slotEditCurrentIgnoredFiles(); + void slotEditCurrentLocalIgnoredFiles(); void slotFolderWizardAccepted(); void slotFolderWizardRejected(); void slotDeleteAccount(); @@ -89,7 +91,6 @@ protected slots: void refreshSelectiveSyncStatus(); void slotMarkSubfolderEncrpted(const FolderStatusModel::SubFolderInfo* folderInfo); void slotMarkSubfolderDecrypted(const FolderStatusModel::SubFolderInfo* folderInfo); - void slotOpenIgnoredFilesDialog(const FolderStatusModel::SubFolderInfo* folderInfo); void slotSubfolderContextMenuRequested(const QModelIndex& idx, const QPoint& point); void slotCustomContextMenuRequested(const QPoint &); void slotFolderListClicked(const QModelIndex &indx); @@ -126,6 +127,7 @@ private: QStringList errors = QStringList()); bool event(QEvent *) override; void createAccountToolbox(); + void openIgnoredFilesDialog(const QString & absFolderPath); /// Returns the alias of the selected folder, empty string if none QString selectedFolderAlias() const; -- cgit v1.2.3 From 9f4873e86488cdc17a30bb50d81e3ba202eb9e08 Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Wed, 14 Aug 2019 23:08:21 +0100 Subject: Fix some typos Signed-off-by: Samir Benmendil --- src/gui/accountsettings.cpp | 4 ++-- src/gui/accountsettings.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 5a0c87769..2fc324573 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -424,7 +424,7 @@ bool AccountSettings::canEncryptOrDecrypt (const FolderStatusModel::SubFolderInf return true; } -void AccountSettings::slotMarkSubfolderEncrpted(const FolderStatusModel::SubFolderInfo* folderInfo) +void AccountSettings::slotMarkSubfolderEncrypted(const FolderStatusModel::SubFolderInfo* folderInfo) { if (!canEncryptOrDecrypt(folderInfo)) { return; @@ -608,7 +608,7 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index if (!isEncrypted) { ac = menu.addAction(tr("Encrypt")); - connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderEncrpted(info); }); + connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderEncrypted(info); }); } else { // Ingore decrypting for now since it only works with an empty folder // connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderDecrypted(info); }); diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index bd3f0e275..ce1885d2d 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -89,7 +89,7 @@ protected slots: void slotOpenAccountWizard(); void slotAccountAdded(AccountState *); void refreshSelectiveSyncStatus(); - void slotMarkSubfolderEncrpted(const FolderStatusModel::SubFolderInfo* folderInfo); + void slotMarkSubfolderEncrypted(const FolderStatusModel::SubFolderInfo* folderInfo); void slotMarkSubfolderDecrypted(const FolderStatusModel::SubFolderInfo* folderInfo); void slotSubfolderContextMenuRequested(const QModelIndex& idx, const QPoint& point); void slotCustomContextMenuRequested(const QPoint &); @@ -112,7 +112,7 @@ protected slots: void slotUploadMetadataSuccess(const QByteArray& folderId); void slotUpdateMetadataError(const QByteArray& folderId, int httpReturnCode); - // Remove Encryotion Bit. + // Remove Encryption Bit. void slotLockForDecryptionSuccess(const QByteArray& folderId, const QByteArray& token); void slotLockForDecryptionError(const QByteArray& folderId, int httpReturnCode); void slotDeleteMetadataSuccess(const QByteArray& folderId); -- cgit v1.2.3 From 758483bc6ebd455fa810b43293c70e4b7bfb4fea Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Wed, 14 Aug 2019 23:12:11 +0100 Subject: Silence CMake warning CMake Warning (dev) at NEXTCLOUD.cmake:31 (set): implicitly converting 'string' to 'STRING' type Signed-off-by: Samir Benmendil --- NEXTCLOUD.cmake | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/NEXTCLOUD.cmake b/NEXTCLOUD.cmake index 7cc321c14..b6bc79c6b 100644 --- a/NEXTCLOUD.cmake +++ b/NEXTCLOUD.cmake @@ -3,10 +3,10 @@ set( APPLICATION_SHORTNAME "Nextcloud" ) set( APPLICATION_EXECUTABLE "nextcloud" ) set( APPLICATION_DOMAIN "nextcloud.com" ) set( APPLICATION_VENDOR "Nextcloud GmbH" ) -set( APPLICATION_UPDATE_URL "https://updates.nextcloud.org/client/" CACHE string "URL for updater" ) -set( APPLICATION_HELP_URL "" CACHE string "URL for the help menu" ) +set( APPLICATION_UPDATE_URL "https://updates.nextcloud.org/client/" CACHE STRING "URL for updater" ) +set( APPLICATION_HELP_URL "" CACHE STRING "URL for the help menu" ) set( APPLICATION_ICON_NAME "Nextcloud" ) -set( APPLICATION_SERVER_URL "" CACHE string "URL for the server to use. If entered the server can only connect to this instance" ) +set( APPLICATION_SERVER_URL "" CACHE STRING "URL for the server to use. If entered the server can only connect to this instance" ) set( LINUX_PACKAGE_SHORTNAME "nextcloud" ) @@ -20,14 +20,14 @@ set( MAC_INSTALLER_BACKGROUND_FILE "${CMAKE_SOURCE_DIR}/admin/osx/installer-back # set( APPLICATION_LICENSE "${OEM_THEME_DIR}/license.txt ) option( WITH_CRASHREPORTER "Build crashreporter" OFF ) -#set( CRASHREPORTER_SUBMIT_URL "https://crash-reports.owncloud.com/submit" CACHE string "URL for crash reporter" ) +#set( CRASHREPORTER_SUBMIT_URL "https://crash-reports.owncloud.com/submit" CACHE STRING "URL for crash reporter" ) #set( CRASHREPORTER_ICON ":/owncloud-icon.png" ) option( WITH_PROVIDERS "Build with providers list" ON ) ## Theming options -set( APPLICATION_WIZARD_HEADER_BACKGROUND_COLOR "#0082c9" CACHE string "Hex color of the wizard header background") -set( APPLICATION_WIZARD_HEADER_TITLE_COLOR "#ffffff" CACHE string "Hex color of the text in the wizard header") +set( APPLICATION_WIZARD_HEADER_BACKGROUND_COLOR "#0082c9" CACHE STRING "Hex color of the wizard header background") +set( APPLICATION_WIZARD_HEADER_TITLE_COLOR "#ffffff" CACHE STRING "Hex color of the text in the wizard header") option( APPLICATION_WIZARD_USE_CUSTOM_LOGO "Use the logo from ':/client/theme/colored/wizard_logo.png' else the default application icon is used" ON ) -- cgit v1.2.3 From 34fcb13e78e3075126cc8c2677826ee5b693e219 Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Wed, 14 Aug 2019 23:27:10 +0100 Subject: Never ignore .sync-exclude, even if excludeHidden That is unless any of the parent folders is hidden. Signed-off-by: Samir Benmendil --- src/csync/csync_exclude.cpp | 6 +++--- src/csync/csync_update.cpp | 4 +++- src/gui/ignorelisteditor.cpp | 8 ++++++++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index 030783c6b..40d0afcff 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -343,15 +343,15 @@ bool ExcludedFiles::isExcluded( return true; } + //TODO this seems a waste, hidden files are ignored before hitting this function it seems if (excludeHidden) { QString path = filePath; // Check all path subcomponents, but to *not* check the base path: // We do want to be able to sync with a hidden folder as the target. while (path.size() > basePath.size()) { QFileInfo fi(path); - //TODO probably not ignore `.sync-exclude.lst` files as it makes sense for them to be - //synced - if (fi.isHidden() || fi.fileName().startsWith(QLatin1Char('.'))) { + if (fi.fileName() != ".sync-exclude.lst" + && (fi.isHidden() || fi.fileName().startsWith(QLatin1Char('.')))) { return true; } diff --git a/src/csync/csync_update.cpp b/src/csync/csync_update.cpp index fd6c6ec95..ecea21cae 100644 --- a/src/csync/csync_update.cpp +++ b/src/csync/csync_update.cpp @@ -124,7 +124,9 @@ static int _csync_detect_update(CSYNC *ctx, std::unique_ptr f * because it's a hidden file that should not be synced. * This code should probably be in csync_exclude, but it does not have the fs parameter. * Keep it here for now */ - if (ctx->ignore_hidden_files && (fs->is_hidden)) { + if (ctx->ignore_hidden_files + && fs->is_hidden + && !fs->path.endsWith(".sync-exclude.lst")) { qCInfo(lcUpdate, "file excluded because it is a hidden file: %s", fs->path.constData()); excluded = CSYNC_FILE_EXCLUDE_HIDDEN; } diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index 22182b5a0..6e5a3d784 100644 --- a/src/gui/ignorelisteditor.cpp +++ b/src/gui/ignorelisteditor.cpp @@ -47,6 +47,14 @@ IgnoreListEditor::IgnoreListEditor(QWidget *parent) connect(this, &QDialog::accepted, [=]() { ui->ignoreTableWidget->slotWriteIgnoreFile(userConfig); + /* handle the hidden file checkbox */ + + /* the ignoreHiddenFiles flag is a folder specific setting, but for now, it is + * handled globally. Save it to every folder that is defined. + * TODO this can now be fixed, simply attach this IgnoreListEditor to top-level account + * settings + */ + FolderMan::instance()->setIgnoreHiddenFiles(ignoreHiddenFiles()); }); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &IgnoreListEditor::slotRestoreDefaults); -- cgit v1.2.3 From e27645cb00a8426ad4879edba4c109d1af46c0c9 Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Thu, 15 Aug 2019 00:53:09 +0100 Subject: Extract loadExcludeFile and use it when discovering new exclude files Signed-off-by: Samir Benmendil --- src/csync/csync_exclude.cpp | 46 +++++++++++++++++++++++++++++---------------- src/csync/csync_exclude.h | 4 ++++ 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index 40d0afcff..7c9ea3458 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -304,32 +304,47 @@ void ExcludedFiles::setWildcardsMatchSlash(bool onoff) prepare(); } +bool ExcludedFiles::loadExcludeFile(const QByteArray & basePath, const QString & file) +{ + QFile f(file); + if (!f.open(QIODevice::ReadOnly)) + return false; + + while (!f.atEnd()) { + QByteArray line = f.readLine().trimmed(); + if (line.isEmpty() || line.startsWith('#')) + continue; + csync_exclude_expand_escapes(line); + _allExcludes[basePath].append(line); + } + prepare(basePath); + return true; +} + bool ExcludedFiles::reloadExcludeFiles() { _allExcludes.clear(); + // clear all regex + _bnameTraversalRegexFile.clear(); + _bnameTraversalRegexDir.clear(); + _fullTraversalRegexFile.clear(); + _fullTraversalRegexDir.clear(); + _fullRegexFile.clear(); + _fullRegexDir.clear(); + bool success = true; for (auto basePath : _excludeFiles.keys()) { for (auto file : _excludeFiles.value(basePath)) { - QFile f(file); - if (!f.open(QIODevice::ReadOnly)) { - success = false; - continue; - } - while (!f.atEnd()) { - QByteArray line = f.readLine().trimmed(); - if (line.isEmpty() || line.startsWith('#')) - continue; - csync_exclude_expand_escapes(line); - _allExcludes[basePath].append(line); - } + success = loadExcludeFile(basePath, file); } } auto endManual = _manualExcludes.cend(); - for (auto kv = _manualExcludes.cbegin(); kv != endManual; ++kv) + for (auto kv = _manualExcludes.cbegin(); kv != endManual; ++kv) { _allExcludes[kv.key()].append(kv.value()); + prepare(kv.key()); + } - prepare(); return success; } @@ -387,8 +402,7 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const char *path, ItemTy QFileInfo fi = QFileInfo(_localPath + path + "/.sync-exclude.lst"); if (fi.isReadable()) { addInTreeExcludeFilePath(fi.absoluteFilePath()); - //really we only need to load this file and prepare(this basePath) - reloadExcludeFiles(); + loadExcludeFile(fi.absolutePath().toUtf8(), fi.absoluteFilePath()); } } diff --git a/src/csync/csync_exclude.h b/src/csync/csync_exclude.h index 5e2d3109f..eeda99c17 100644 --- a/src/csync/csync_exclude.h +++ b/src/csync/csync_exclude.h @@ -130,6 +130,10 @@ public slots: * Reloads the exclude patterns from the registered paths. */ bool reloadExcludeFiles(); + /** + * Loads the exclude patterns from file the registered base paths. + */ + bool loadExcludeFile(const QByteArray & basePath, const QString & file); private: /** -- cgit v1.2.3 From 7a9b13a563fdad350919b8fa445085a18aa0312d Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Thu, 15 Aug 2019 02:47:53 +0100 Subject: Manual exclude are anchored to _localPath by default This makes a lot of sense, since there should be no file to be synced outside of _localPath. Signed-off-by: Samir Benmendil --- src/csync/csync_exclude.cpp | 5 +++++ src/csync/csync_exclude.h | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index 7c9ea3458..60a7220d2 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -281,6 +281,11 @@ void ExcludedFiles::setExcludeConflictFiles(bool onoff) _excludeConflictFiles = onoff; } +void ExcludedFiles::addManualExclude(const QByteArray &expr) +{ + addManualExclude(expr, _localPath.toUtf8()); +} + void ExcludedFiles::addManualExclude(const QByteArray &expr, const QByteArray &basePath) { Q_ASSERT(basePath.startsWith('/')); diff --git a/src/csync/csync_exclude.h b/src/csync/csync_exclude.h index eeda99c17..0b1147791 100644 --- a/src/csync/csync_exclude.h +++ b/src/csync/csync_exclude.h @@ -101,7 +101,8 @@ public: * Primarily used in tests. Patterns added this way are preserved when * reloadExcludeFiles() is called. */ - void addManualExclude(const QByteArray &expr, const QByteArray &basePath = "/"); + void addManualExclude(const QByteArray &expr); + void addManualExclude(const QByteArray &expr, const QByteArray &basePath); /** * Removes all manually added exclude patterns. -- cgit v1.2.3 From 5e3c2d2a96013200957f34aa0e052ed1fcf7bddb Mon Sep 17 00:00:00 2001 From: Samir Benmendil Date: Thu, 15 Aug 2019 02:57:39 +0100 Subject: Fix fullPath matching Signed-off-by: Samir Benmendil --- src/csync/csync_exclude.cpp | 9 +++++++++ test/csync/csync_tests/check_csync_exclude.cpp | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index 60a7220d2..9a4f196ed 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -730,6 +730,15 @@ void ExcludedFiles::prepare(const BasePathByteArray & basePath) auto &fullFileDir = removeExcluded ? fullFileDirRemove : fullFileDirKeep; auto &fullDir = removeExcluded ? fullDirRemove : fullDirKeep; + if (fullPath) { + // The full pattern is matched against a path relative to _localPath, however exclude is + // relative to basePath at this point. + // We know for sure that both _localPath and basePath are absolute and that basePath is + // contained in _localPath. So we can simply remove it from the begining. + auto relPath = basePath.mid(_localPath.size()); + // Make exclude relative to _localPath + exclude.prepend(relPath); + } auto regexExclude = convertToRegexpSyntax(QString::fromUtf8(exclude), _wildcardsMatchSlash); if (!fullPath) { regexAppend(bnameFileDir, bnameDir, regexExclude, matchDirOnly); diff --git a/test/csync/csync_tests/check_csync_exclude.cpp b/test/csync/csync_tests/check_csync_exclude.cpp index bfbf36095..07ec665a8 100644 --- a/test/csync/csync_tests/check_csync_exclude.cpp +++ b/test/csync/csync_tests/check_csync_exclude.cpp @@ -137,6 +137,11 @@ static void check_csync_exclude_add_per_dir(void **) excludedFiles->addManualExclude("foo"); assert_true(excludedFiles->_fullRegexFile["/"].pattern().contains("foo")); + + excludedFiles->addManualExclude("foo/bar", "/tmp/check_csync1/"); + assert_true(excludedFiles->_fullRegexFile["/tmp/check_csync1/"].pattern().contains("bar")); + assert_true(excludedFiles->_fullTraversalRegexFile["/tmp/check_csync1/"].pattern().contains("bar")); + assert_false(excludedFiles->_bnameTraversalRegexFile["/tmp/check_csync1/"].pattern().contains("foo")); } static void check_csync_excluded(void **) @@ -258,6 +263,13 @@ static void check_csync_excluded_per_dir(void **) assert_int_equal(check_file_full("A"), CSYNC_NOT_EXCLUDED); assert_int_equal(check_file_full("B/A"), CSYNC_FILE_EXCLUDE_LIST); + excludedFiles->clearManualExcludes(); + excludedFiles->addManualExclude("A/a1", "/B/"); + excludedFiles->reloadExcludeFiles(); + + assert_int_equal(check_file_full("A"), CSYNC_NOT_EXCLUDED); + assert_int_equal(check_file_full("B/A/a1"), CSYNC_FILE_EXCLUDE_LIST); + #define FOO_DIR "/tmp/check_csync1/foo" #define FOO_EXCLUDE_LIST FOO_DIR "/.sync-exclude.lst" int rc; @@ -279,6 +291,16 @@ static void check_csync_excluded_per_dir(void **) #undef FOO_EXCLUDE_LIST } +static void check_csync_excluded_traversal_per_dir(void **) +{ + assert_int_equal(check_file_traversal("/"), CSYNC_NOT_EXCLUDED); + + /* path wildcards */ + excludedFiles->addManualExclude("*/*.tex.tmp", "/latex/"); + assert_int_equal(check_file_traversal("latex/my_manuscript.tex.tmp"), CSYNC_NOT_EXCLUDED); + assert_int_equal(check_file_traversal("latex/songbook/my_manuscript.tex.tmp"), CSYNC_FILE_EXCLUDE_LIST); +} + static void check_csync_excluded_traversal(void **) { assert_int_equal(check_file_traversal(""), CSYNC_NOT_EXCLUDED); @@ -684,6 +706,7 @@ int torture_run_tests(void) cmocka_unit_test_setup_teardown(T::check_csync_excluded, T::setup_init, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_excluded_per_dir, T::setup, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_excluded_traversal, T::setup_init, T::teardown), + cmocka_unit_test_setup_teardown(T::check_csync_excluded_traversal_per_dir, T::setup, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_dir_only, T::setup, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_pathes, T::setup_init, T::teardown), cmocka_unit_test_setup_teardown(T::check_csync_wildcards, T::setup, T::teardown), -- cgit v1.2.3