Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/desktop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCamila Ayres <hello@camila.codes>2019-09-06 17:03:54 +0300
committerGitHub <noreply@github.com>2019-09-06 17:03:54 +0300
commit9ab52414592cfc3f9261183007b264f17a6418bb (patch)
tree95ea0083efee2ab4b2d6dc87493f7a58c61143f5
parent680f70aa1ac121456f38c36c281bbef104ed403f (diff)
parent0353724472bdc9bc144013b2ee36893733f6394f (diff)
Merge branch 'master' into w10-start-logo
-rw-r--r--NEXTCLOUD.cmake12
-rw-r--r--src/csync/csync_exclude.cpp269
-rw-r--r--src/csync/csync_exclude.h60
-rw-r--r--src/csync/csync_update.cpp4
-rw-r--r--src/gui/CMakeLists.txt2
-rw-r--r--src/gui/accountsettings.cpp58
-rw-r--r--src/gui/accountsettings.h7
-rw-r--r--src/gui/generalsettings.cpp1
-rw-r--r--src/gui/ignorelisteditor.cpp186
-rw-r--r--src/gui/ignorelisteditor.h9
-rw-r--r--src/gui/ignorelisteditor.ui100
-rw-r--r--src/gui/ignorelisttablewidget.cpp167
-rw-r--r--src/gui/ignorelisttablewidget.h38
-rw-r--r--src/gui/ignorelisttablewidget.ui112
-rw-r--r--src/libsync/syncengine.cpp2
-rw-r--r--test/csync/csync_tests/check_csync_exclude.cpp86
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),