diff options
author | Camila Ayres <hello@camila.codes> | 2019-09-06 17:03:54 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-09-06 17:03:54 +0300 |
commit | 9ab52414592cfc3f9261183007b264f17a6418bb (patch) | |
tree | 95ea0083efee2ab4b2d6dc87493f7a58c61143f5 | |
parent | 680f70aa1ac121456f38c36c281bbef104ed403f (diff) | |
parent | 0353724472bdc9bc144013b2ee36893733f6394f (diff) |
Merge branch 'master' into w10-start-logo
-rw-r--r-- | NEXTCLOUD.cmake | 12 | ||||
-rw-r--r-- | src/csync/csync_exclude.cpp | 269 | ||||
-rw-r--r-- | src/csync/csync_exclude.h | 60 | ||||
-rw-r--r-- | src/csync/csync_update.cpp | 4 | ||||
-rw-r--r-- | src/gui/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/gui/accountsettings.cpp | 58 | ||||
-rw-r--r-- | src/gui/accountsettings.h | 7 | ||||
-rw-r--r-- | src/gui/generalsettings.cpp | 1 | ||||
-rw-r--r-- | src/gui/ignorelisteditor.cpp | 186 | ||||
-rw-r--r-- | src/gui/ignorelisteditor.h | 9 | ||||
-rw-r--r-- | src/gui/ignorelisteditor.ui | 100 | ||||
-rw-r--r-- | src/gui/ignorelisttablewidget.cpp | 167 | ||||
-rw-r--r-- | src/gui/ignorelisttablewidget.h | 38 | ||||
-rw-r--r-- | src/gui/ignorelisttablewidget.ui | 112 | ||||
-rw-r--r-- | src/libsync/syncengine.cpp | 2 | ||||
-rw-r--r-- | test/csync/csync_tests/check_csync_exclude.cpp | 86 |
16 files changed, 751 insertions, 362 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 ) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index e3146a099..9a4f196ed 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) @@ -261,9 +283,18 @@ void ExcludedFiles::setExcludeConflictFiles(bool onoff) void ExcludedFiles::addManualExclude(const QByteArray &expr) { - _manualExcludes.append(expr); - _allExcludes.append(expr); - prepare(); + addManualExclude(expr, _localPath.toUtf8()); +} + +void ExcludedFiles::addManualExclude(const QByteArray &expr, const QByteArray &basePath) +{ + Q_ASSERT(basePath.startsWith('/')); + Q_ASSERT(basePath.endsWith('/')); + + auto key = basePath; + _manualExcludes[key].append(expr); + _allExcludes[key].append(expr); + prepare(key); } void ExcludedFiles::clearManualExcludes() @@ -278,26 +309,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; - 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('#')) - continue; - csync_exclude_expand_escapes(line); - _allExcludes.append(line); + for (auto basePath : _excludeFiles.keys()) { + for (auto file : _excludeFiles.value(basePath)) { + success = loadExcludeFile(basePath, file); } } - _allExcludes.append(_manualExcludes); - prepare(); + + auto endManual = _manualExcludes.cend(); + for (auto kv = _manualExcludes.cbegin(); kv != endManual; ++kv) { + _allExcludes[kv.key()].append(kv.value()); + prepare(kv.key()); + } + return success; } @@ -311,13 +363,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); - if (fi.isHidden() || fi.fileName().startsWith(QLatin1Char('.'))) { + if (fi.fileName() != ".sync-exclude.lst" + && (fi.isHidden() || fi.fileName().startsWith(QLatin1Char('.')))) { return true; } @@ -340,7 +394,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 +402,15 @@ 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()); + loadExcludeFile(fi.absolutePath().toUtf8(), fi.absoluteFilePath()); + } + } + // Check the bname part of the path to see whether the full // regex should be run. @@ -359,35 +422,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 +481,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<CSYNC_EXCLUDE_TYPE(const char *path, ItemType filetype)> { return [this](const char *path, ItemType filetype) { return this->traversalPatternMatch(path, filetype); }; @@ -555,6 +651,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 +708,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') @@ -618,6 +730,15 @@ void ExcludedFiles::prepare() 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); @@ -654,11 +775,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<exclude>" + bnameFileDirKeep + ")$|" + "^(?P<excluderemove>" + bnameFileDirRemove + ")$|" + "^(?P<trigger>" + bnameTriggerFileDir + ")$"); - _bnameTraversalRegexDir.setPattern( + _bnameTraversalRegexDir[basePath].setPattern( "^(?P<exclude>" + bnameFileDirKeep + "|" + bnameDirKeep + ")$|" + "^(?P<excluderemove>" + bnameFileDirRemove + "|" + bnameDirRemove + ")$|" + "^(?P<trigger>" + bnameTriggerFileDir + "|" + bnameTriggerDir + ")$"); @@ -667,13 +788,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<exclude>" + fullFileDirKeep + ")(?:$|/)" + "|" + "^(?P<excluderemove>" + fullFileDirRemove + ")(?:$|/)"); - _fullTraversalRegexDir.setPattern( + _fullTraversalRegexDir[basePath].setPattern( QLatin1String("") + "^(?P<exclude>" + fullFileDirKeep + "|" + fullDirKeep + ")(?:$|/)" + "|" @@ -681,7 +802,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<exclude>") // Full patterns are anchored to the beginning + "^(?:" + fullFileDirKeep + ")(?:$|/)" + "|" @@ -697,7 +818,7 @@ void ExcludedFiles::prepare() + "(?:^|/)(?:" + bnameFileDirRemove + ")(?:$|/)" + "|" + "(?:^|/)(?:" + bnameDirRemove + ")/" + ")"); - _fullRegexDir.setPattern( + _fullRegexDir[basePath].setPattern( QLatin1String("(?P<exclude>") + "^(?:" + fullFileDirKeep + "|" + fullDirKeep + ")(?:$|/)" + "|" + "(?:^|/)(?:" + bnameFileDirKeep + "|" + bnameDirKeep + ")(?:$|/)" @@ -711,16 +832,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..0b1147791 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,13 @@ 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 +123,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<CSYNC_EXCLUDE_TYPE(const char *path, ItemType filetype)>; public slots: @@ -129,6 +131,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: /** @@ -156,10 +162,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 +215,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<QString> _excludeFiles; + QMap<BasePathByteArray, QList<QString>> _excludeFiles; /// Exclude patterns added with addManualExclude() - QList<QByteArray> _manualExcludes; + QMap<BasePathByteArray, QList<QByteArray>> _manualExcludes; /// List of all active exclude patterns - QList<QByteArray> _allExcludes; + QMap<BasePathByteArray, QList<QByteArray>> _allExcludes; /// see prepare() - QRegularExpression _bnameTraversalRegexFile; - QRegularExpression _bnameTraversalRegexDir; - QRegularExpression _fullTraversalRegexFile; - QRegularExpression _fullTraversalRegexDir; - QRegularExpression _fullRegexFile; - QRegularExpression _fullRegexDir; + QMap<BasePathByteArray, QRegularExpression> _bnameTraversalRegexFile; + QMap<BasePathByteArray, QRegularExpression> _bnameTraversalRegexDir; + QMap<BasePathByteArray, QRegularExpression> _fullTraversalRegexFile; + QMap<BasePathByteArray, QRegularExpression> _fullTraversalRegexDir; + QMap<BasePathByteArray, QRegularExpression> _fullRegexFile; + QMap<BasePathByteArray, QRegularExpression> _fullRegexDir; bool _excludeConflictFiles = 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<csync_file_stat_t> 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/CMakeLists.txt b/src/gui/CMakeLists.txt index ec21cc848..44eec27b6 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 @@ -61,6 +62,7 @@ set(client_SRCS generalsettings.cpp legalnotice.cpp ignorelisteditor.cpp + ignorelisttablewidget.cpp lockwatcher.cpp logbrowser.cpp navigationpanehelper.cpp diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 5e96ac226..2fc324573 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 <math.h> #include <QDesktopServices> +#include <QDialogButtonBox> #include <QDir> #include <QListWidgetItem> #include <QMessageBox> @@ -422,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; @@ -539,6 +541,51 @@ void AccountSettings::slotLockForDecryptionError(const QByteArray& fileId, int h qDebug() << "Error Locking for decryption"; } +void AccountSettings::slotEditCurrentIgnoredFiles() +{ + 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); + 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); @@ -561,12 +608,16 @@ 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); }); } } + + ac = menu.addAction(tr("Edit Ignored Files")); + connect(ac, &QAction::triggered, this, &AccountSettings::slotEditCurrentLocalIgnoredFiles); + menu.exec(QCursor::pos()); } @@ -600,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 eb6a78b17..ce1885d2d 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(); @@ -87,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 &); @@ -110,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); @@ -125,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; diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index b8c10d592..dd4235834 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -184,6 +184,7 @@ void GeneralSettings::slotShowInExplorerNavigationPane(bool checked) void GeneralSettings::slotIgnoreFilesEditor() { if (_ignoreEditor.isNull()) { + ConfigFile cfgFile; _ignoreEditor = new IgnoreListEditor(this); _ignoreEditor->setAttribute(Qt::WA_DeleteOnClose, true); _ignoreEditor->open(); diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index 9b44c25a0..6e5a3d784 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 <QFile> @@ -27,10 +28,6 @@ namespace OCC { -static int patternCol = 0; -static int deletableCol = 1; -static int readOnlyRows = 3; - IgnoreListEditor::IgnoreListEditor(QWidget *parent) : QDialog(parent) , ui(new Ui::IgnoreListEditor) @@ -39,28 +36,28 @@ IgnoreListEditor::IgnoreListEditor(QWidget *parent) 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))); setupTableReadOnlyItems(); - readIgnoreFile(cfgFile.excludeFile(ConfigFile::UserScope), 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); - - ui->tableWidget->resizeColumnsToContents(); - ui->tableWidget->horizontalHeader()->setSectionResizeMode(patternCol, QHeaderView::Stretch); - ui->tableWidget->verticalHeader()->setVisible(false); + const auto userConfig = cfgFile.excludeFile(ConfigFile::Scope::UserScope); + ui->ignoreTableWidget->readIgnoreFile(userConfig); + + 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); ui->syncHiddenFilesCheckBox->setChecked(!FolderMan::instance()->ignoreHiddenFiles()); } @@ -70,12 +67,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() @@ -83,140 +79,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->clearContents(); - setupTableReadOnlyItems(); -} + ui->ignoreTableWidget->slotRemoveAllItems(); -void IgnoreListEditor::slotUpdateLocalIgnoreList() -{ ConfigFile cfgFile; - QString ignoreFile = cfgFile.excludeFile(ConfigFile::UserScope); - QFile ignores(ignoreFile); - if (ignores.open(QIODevice::WriteOnly)) { - // rewrites the whole file since now the user can also remove system patterns - QFile::resize(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(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); - - 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; - 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; + setupTableReadOnlyItems(); + ui->ignoreTableWidget->readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); } } // namespace OCC diff --git a/src/gui/ignorelisteditor.h b/src/gui/ignorelisteditor.h index 74137a736..39c38d74d 100644 --- a/src/gui/ignorelisteditor.h +++ b/src/gui/ignorelisteditor.h @@ -35,23 +35,16 @@ class IgnoreListEditor : public QDialog Q_OBJECT public: - explicit IgnoreListEditor(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; }; 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 @@ <string>Files Ignored by Patterns</string> </property> <layout class="QGridLayout" name="gridLayout"> - <item row="4" column="0" colspan="2"> - <widget class="QLabel" name="descriptionLabel"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="text"> - <string/> - </property> - <property name="textFormat"> - <enum>Qt::PlainText</enum> - </property> - <property name="wordWrap"> - <bool>true</bool> - </property> - </widget> - </item> - <item row="3" column="1"> - <spacer name="verticalSpacer"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>213</height> - </size> - </property> - </spacer> - </item> - <item row="0" column="0" rowspan="4"> - <widget class="QTableWidget" name="tableWidget"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="selectionMode"> - <enum>QAbstractItemView::SingleSelection</enum> - </property> - <property name="selectionBehavior"> - <enum>QAbstractItemView::SelectRows</enum> - </property> - <property name="columnCount"> - <number>2</number> - </property> - <column> - <property name="text"> - <string>Pattern</string> - </property> - </column> - <column> - <property name="text"> - <string>Allow Deletion</string> - </property> - </column> - </widget> - </item> - <item row="1" column="1"> - <widget class="QPushButton" name="removePushButton"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="text"> - <string>Remove</string> - </property> - </widget> - </item> - <item row="0" column="1"> - <widget class="QPushButton" name="addPushButton"> - <property name="enabled"> - <bool>true</bool> - </property> - <property name="text"> - <string>Add</string> - </property> - </widget> - </item> - <item row="2" column="1"> - <widget class="QPushButton" name="removeAllPushButton"> - <property name="text"> - <string>Remove all</string> - </property> - </widget> + <item row="0" column="0"> + <widget class="IgnoreListTableWidget" name="ignoreTableWidget" native="true"/> </item> </layout> </widget> @@ -139,6 +51,14 @@ </item> </layout> </widget> + <customwidgets> + <customwidget> + <class>IgnoreListTableWidget</class> + <extends>QWidget</extends> + <header>ignorelisttablewidget.h</header> + <container>1</container> + </customwidget> + </customwidgets> <resources/> <connections> <connection> 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 <QFile> +#include <QInputDialog> +#include <QLineEdit> +#include <QMessageBox> + +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 <QWidget> + +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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>OCC::IgnoreListTableWidget</class> + <widget class="QWidget" name="OCC::IgnoreListTableWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>342</width> + <height>378</height> + </rect> + </property> + <property name="windowTitle"> + <string>IgnoreListTableWidget</string> + </property> + <layout class="QGridLayout" name="gridLayout_2"> + <item row="0" column="0" rowspan="4"> + <widget class="QTableWidget" name="tableWidget"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="selectionMode"> + <enum>QAbstractItemView::SingleSelection</enum> + </property> + <property name="selectionBehavior"> + <enum>QAbstractItemView::SelectRows</enum> + </property> + <property name="columnCount"> + <number>2</number> + </property> + <column> + <property name="text"> + <string>Pattern</string> + </property> + </column> + <column> + <property name="text"> + <string>Allow Deletion</string> + </property> + </column> + </widget> + </item> + <item row="0" column="1"> + <widget class="QPushButton" name="addPushButton"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Add</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="removePushButton"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="text"> + <string>Remove</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QPushButton" name="removeAllPushButton"> + <property name="text"> + <string>Remove all</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <spacer name="verticalSpacer"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>322</height> + </size> + </property> + </spacer> + </item> + <item row="4" column="0" colspan="2"> + <widget class="QLabel" name="descriptionLabel"> + <property name="enabled"> + <bool>true</bool> + </property> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + <property name="textFormat"> + <enum>Qt::PlainText</enum> + </property> + <property name="wordWrap"> + <bool>true</bool> + </property> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> 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..07ec665a8 100644 --- a/test/csync/csync_tests/check_csync_exclude.cpp +++ b/test/csync/csync_tests/check_csync_exclude.cpp @@ -21,6 +21,7 @@ #include <string.h> #include <time.h> #include <sys/time.h> +#include <stdio.h> #define CSYNC_TEST 1 #include "csync_exclude.cpp" @@ -115,16 +116,32 @@ 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")); + + 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 **) @@ -232,6 +249,58 @@ 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); + + 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; + 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_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); @@ -633,8 +702,11 @@ 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_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), |