From 492fee325a2cc5fd00b5d83a9dbcac5a2f08c862 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Tue, 20 Jan 2026 13:36:06 +0100 Subject: [PATCH 1/8] style(excludedfiles): avoid nested `if` in loops Signed-off-by: Jyrki Gadinger --- src/csync/csync_exclude.cpp | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index b92cfa08e43ca..1af432fe9c49a 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -470,8 +470,9 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const QString &path, Ite continue; } - 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) { @@ -494,12 +495,13 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const QString &path, Ite 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; - } + if (!m.hasMatch()) { + continue; + } + 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; @@ -533,12 +535,13 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::fullPatternMatch(const QString &p, ItemType fi 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; - } + if (!m.hasMatch()) { + continue; + } + if (m.capturedStart(QStringLiteral("exclude")) != -1) { + return CSYNC_FILE_EXCLUDE_LIST; + } else if (m.capturedStart(QStringLiteral("excluderemove")) != -1) { + return CSYNC_FILE_EXCLUDE_AND_REMOVE; } } From 4125b79653f2ee1cfe3fcc1e6cacba7ba4d95e09 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Tue, 20 Jan 2026 13:36:18 +0100 Subject: [PATCH 2/8] fix(excludedfiles): inherit directory-specific excludes from parents and global exclusion list Signed-off-by: Jyrki Gadinger --- src/csync/csync_exclude.cpp | 2 +- test/testexcludedfiles.cpp | 75 +++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 4 deletions(-) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index 1af432fe9c49a..e8c52b45ddc8a 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -471,7 +471,7 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const QString &path, Ite } if (!m.hasMatch()) { - return CSYNC_NOT_EXCLUDED; + continue; } if (m.capturedStart(QStringLiteral("exclude")) != -1) { return CSYNC_FILE_EXCLUDE_LIST; diff --git a/test/testexcludedfiles.cpp b/test/testexcludedfiles.cpp index 988b4f8f548ab..504db5fc4d9de 100644 --- a/test/testexcludedfiles.cpp +++ b/test/testexcludedfiles.cpp @@ -11,12 +11,16 @@ #include #include +#include "common/utility.h" +#include "csync.h" #include "csync_exclude.h" #include "logger.h" using namespace OCC; -#define EXCLUDE_LIST_FILE SOURCEDIR "/../../sync-exclude.lst" +using namespace Qt::StringLiterals; + +constexpr auto EXCLUDE_LIST_FILE = SOURCEDIR "/../../sync-exclude.lst"; // The tests were converted from the old CMocka framework, that's why there is a global static QScopedPointer excludedFiles; @@ -397,8 +401,8 @@ private slots: QCOMPARE(check_file_traversal("/excludepath/withsubdir"), CSYNC_FILE_EXCLUDE_LIST); QCOMPARE(check_dir_traversal("/excludepath/withsubdir2"), CSYNC_NOT_EXCLUDED); - // because leading dirs aren't checked! - QCOMPARE(check_dir_traversal("/excludepath/withsubdir/foo"), CSYNC_NOT_EXCLUDED); + // Parent directories are considered too + QCOMPARE(check_dir_traversal("/excludepath/withsubdir/foo"), CSYNC_FILE_EXCLUDE_LIST); /* Check ending of pattern */ QCOMPARE(check_file_traversal("/exclude"), CSYNC_FILE_EXCLUDE_LIST); @@ -785,6 +789,71 @@ private slots: QCOMPARE(excludedFiles->reloadExcludeFiles(), true); QCOMPARE(excludedFiles->_allExcludes.size(), 1); } + + void testFolderExcludeListInheritsGlobalExcludes() + { + QTemporaryDir tempDir; + const auto localPath = Utility::trailingSlashPath(tempDir.path()); + excludedFiles.reset(new ExcludedFiles(localPath)); + + // create .sync-exclude.lst files inside `a/b` and `a/b/c` subdirectories + QDir localDir(localPath); + QVERIFY(localDir.mkpath("a/b/c/d")); + const auto writeExcludeList = [&localDir](const QString& path, const QStringList& contents) -> bool { + QFile f(localDir.filePath("%1/.sync-exclude.lst"_L1.arg(path))); + if (!f.open(QIODevice::WriteOnly)) { + return false; + } + f.write(contents.join("\n").toLocal8Bit()); + f.close(); + return true; + }; + QVERIFY(writeExcludeList("a/b", {"B_ignore*"})); + QVERIFY(writeExcludeList("a/b/c", {"C_ignore*"})); + + // add default/global exclude list from the client + excludedFiles->addExcludeFilePath(EXCLUDE_LIST_FILE); + QVERIFY(excludedFiles->reloadExcludeFiles()); + + QCOMPARE(excludedFiles->traversalPatternMatch("~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + + // according to `ExcludedFiles::traversalPatternMatch`, directories are + // guaranteed to be visited before their files, so match those first. + // + // The function has the side effect of reading additional excludes from + // a ".sync-exclude.lst" file in the passed directory + QCOMPARE(excludedFiles->_allExcludes.size(), 1); + QCOMPARE(excludedFiles->traversalPatternMatch("a", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 1); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 2); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 3); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d", ItemTypeDirectory), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->_allExcludes.size(), 3); + + // validate custom ignores from the directory-specific .sync-exclude.lst + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/B_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/C_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + + // excludes from subfolders are not propagated to their parent(s) + QCOMPARE(excludedFiles->traversalPatternMatch("B_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("a/B_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("C_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("a/C_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/C_ignoredFile", ItemTypeFile), CSYNC_NOT_EXCLUDED); + + // global exclude list should still match in subdirs + QCOMPARE(excludedFiles->traversalPatternMatch("a/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d/~$_patternExcludedByDefault", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + + // excludes from subfolders inherit the parent excludes + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/B_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d/B_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + QCOMPARE(excludedFiles->traversalPatternMatch("a/b/c/d/C_ignoredFile", ItemTypeFile), CSYNC_FILE_EXCLUDE_LIST); + } }; QTEST_APPLESS_MAIN(TestExcludedFiles) From ad6e5bd55a72db1794057152ab11edb4fc9179a4 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Mon, 2 Feb 2026 16:29:54 +0100 Subject: [PATCH 3/8] fix(ignoredfiles): delete editor dialog when closing Signed-off-by: Jyrki Gadinger --- src/gui/accountsettings.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 0c0c62dbc9f90..30ba7ded216d7 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -560,6 +560,7 @@ void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) layout->addWidget(buttonBox); const auto dialog = new QDialog(); + dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setLayout(layout); connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton * button) { From 455a002d702c148e0218c842806ea395be258843 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Mon, 2 Feb 2026 17:32:08 +0100 Subject: [PATCH 4/8] fix(ignoredfiles): add window title to editor dialog Signed-off-by: Jyrki Gadinger --- src/gui/accountsettings.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 30ba7ded216d7..3d2f2a0a1aecd 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -560,6 +560,7 @@ void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) layout->addWidget(buttonBox); const auto dialog = new QDialog(); + dialog->setWindowTitle(tr("Ignored Files Editor")); dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setLayout(layout); From 8bcde8db3363905f0d042f492df3cc967d525a83 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Mon, 2 Feb 2026 17:33:14 +0100 Subject: [PATCH 5/8] chore(excludedfiles): remove undocumented version-specific excludes handling Apparently this was supposed to be used for excluding placeholder files from earlier versions, however the entry in the ignore list that used this special syntax was removed in 4e3f2f755a9e0d430aae7a549a077511be1eb3c6. As this feature seems rather unusual and unknown, might as well get rid of it to improve maintenance... Signed-off-by: Jyrki Gadinger --- src/csync/csync_exclude.cpp | 42 +++---------------------------------- src/csync/csync_exclude.h | 30 -------------------------- test/testexcludedfiles.cpp | 26 ----------------------- 3 files changed, 3 insertions(+), 95 deletions(-) diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp index e8c52b45ddc8a..55a678f0310d9 100644 --- a/src/csync/csync_exclude.cpp +++ b/src/csync/csync_exclude.cpp @@ -21,7 +21,6 @@ #include "common/utility.h" #include "common/filesystembase.h" -#include "../version.h" #include #include @@ -220,7 +219,6 @@ using namespace OCC; ExcludedFiles::ExcludedFiles(const QString &localPath) : _localPath(localPath) - , _clientVersion(MIRALL_VERSION_MAJOR, MIRALL_VERSION_MINOR, MIRALL_VERSION_PATCH) { Q_ASSERT(_localPath.endsWith(QStringLiteral("/"))); // Windows used to use PathMatchSpec which allows *foo to match abc/deffoo. @@ -283,22 +281,14 @@ void ExcludedFiles::setWildcardsMatchSlash(bool onoff) prepare(); } -void ExcludedFiles::setClientVersion(ExcludedFiles::Version version) -{ - _clientVersion = version; -} - void ExcludedFiles::loadExcludeFilePatterns(const QString &basePath, QFile &file) { QStringList patterns; while (!file.atEnd()) { QByteArray line = file.readLine().trimmed(); - if (line.startsWith("#!version")) { - if (!versionDirectiveKeepNextLine(line)) - file.readLine(); - } - if (line.isEmpty() || line.startsWith('#')) + if (line.isEmpty() || line.startsWith('#')) { continue; + } const auto patternStr = QString::fromUtf8(line); if (QStringView{patternStr}.trimmed() == QLatin1StringView("*")) { continue; @@ -309,7 +299,7 @@ void ExcludedFiles::loadExcludeFilePatterns(const QString &basePath, QFile &file _allExcludes[basePath].append(patterns); // nothing to prepare if the user decided to not exclude anything - if (!_allExcludes.value(basePath).isEmpty()){ + if (!_allExcludes.value(basePath).isEmpty()) { prepare(basePath); } } @@ -360,32 +350,6 @@ bool ExcludedFiles::reloadExcludeFiles() return success; } -bool ExcludedFiles::versionDirectiveKeepNextLine(const QByteArray &directive) const -{ - if (!directive.startsWith("#!version")) - return true; - QByteArrayList args = directive.split(' '); - if (args.size() != 3) - return true; - QByteArray op = args[1]; - QByteArrayList argVersions = args[2].split('.'); - if (argVersions.size() != 3) - return true; - - auto argVersion = std::make_tuple(argVersions[0].toInt(), argVersions[1].toInt(), argVersions[2].toInt()); - if (op == "<=") - return _clientVersion <= argVersion; - if (op == "<") - return _clientVersion < argVersion; - if (op == ">") - return _clientVersion > argVersion; - if (op == ">=") - return _clientVersion >= argVersion; - if (op == "==") - return _clientVersion == argVersion; - return true; -} - bool ExcludedFiles::isExcluded( const QString &filePath, const QString &basePath, diff --git a/src/csync/csync_exclude.h b/src/csync/csync_exclude.h index 5e4c9358d28a4..9011be72b876d 100644 --- a/src/csync/csync_exclude.h +++ b/src/csync/csync_exclude.h @@ -59,8 +59,6 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject { Q_OBJECT public: - using Version = std::tuple; - explicit ExcludedFiles(const QString &localPath = QStringLiteral("/")); ~ExcludedFiles() override; @@ -110,11 +108,6 @@ class OCSYNC_EXPORT ExcludedFiles : public QObject */ void setWildcardsMatchSlash(bool onoff); - /** - * Sets the client version, only used for testing. - */ - void setClientVersion(Version version); - /** * @brief Check if the given path should be excluded in a traversal situation. * @@ -148,23 +141,6 @@ public slots: void loadExcludeFilePatterns(const QString &basePath, QFile &file); private: - /** - * Returns true if the version directive indicates the next line - * should be skipped. - * - * A version directive has the form "#!version " - * where can be <, <=, ==, >, >= and can be any version - * like 2.5.0. - * - * Example: - * - * #!version < 2.5.0 - * myexclude - * - * Would enable the "myexclude" pattern only for versions before 2.5.0. - */ - [[nodiscard]] bool versionDirectiveKeepNextLine(const QByteArray &directive) const; - /** * @brief Match the exclude pattern against the full path. * @@ -257,12 +233,6 @@ public slots: */ bool _wildcardsMatchSlash = false; - /** - * The client version. Used to evaluate version-dependent excludes, - * see versionDirectiveKeepNextLine(). - */ - Version _clientVersion; - friend class TestExcludedFiles; }; diff --git a/test/testexcludedfiles.cpp b/test/testexcludedfiles.cpp index 504db5fc4d9de..e69d809b5fd34 100644 --- a/test/testexcludedfiles.cpp +++ b/test/testexcludedfiles.cpp @@ -693,32 +693,6 @@ private slots: QVERIFY(0 == strcmp(line.constData(), "\\")); } - void check_version_directive() - { - ExcludedFiles excludes; - excludes.setClientVersion(ExcludedFiles::Version(2, 5, 0)); - - std::vector> tests = { - { "#!version == 2.5.0", true }, - { "#!version == 2.6.0", false }, - { "#!version < 2.6.0", true }, - { "#!version <= 2.6.0", true }, - { "#!version > 2.6.0", false }, - { "#!version >= 2.6.0", false }, - { "#!version < 2.4.0", false }, - { "#!version <= 2.4.0", false }, - { "#!version > 2.4.0", true }, - { "#!version >= 2.4.0", true }, - { "#!version < 2.5.0", false }, - { "#!version <= 2.5.0", true }, - { "#!version > 2.5.0", false }, - { "#!version >= 2.5.0", true }, - }; - for (auto test : tests) { - QVERIFY(excludes.versionDirectiveKeepNextLine(test.first) == test.second); - } - } - void testAddExcludeFilePath_addSameFilePath_listSizeDoesNotIncrease() { excludedFiles.reset(new ExcludedFiles()); From b56ef966acb2e359f93d3af08b951b8ae55e9cf8 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Mon, 2 Feb 2026 17:36:50 +0100 Subject: [PATCH 6/8] chore(ignoredfiles): remove unused readOnlyTooltip The tool tip was wrong anyway -- system entries could be modified for a long time already. I would rather figure out a nicer way to display why a certain rule appears in the list at a glance Signed-off-by: Jyrki Gadinger --- src/gui/ignorelisteditor.cpp | 4 ---- src/gui/ignorelisteditor.h | 1 - src/gui/ignorelisttablewidget.cpp | 1 - src/gui/ignorelisttablewidget.h | 1 - 4 files changed, 7 deletions(-) diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index f17252168419b..5103f3f1cc913 100644 --- a/src/gui/ignorelisteditor.cpp +++ b/src/gui/ignorelisteditor.cpp @@ -28,10 +28,6 @@ IgnoreListEditor::IgnoreListEditor(QWidget *parent) ui->setupUi(this); ConfigFile cfgFile; - //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(); const auto userConfig = cfgFile.excludeFile(ConfigFile::Scope::UserScope); diff --git a/src/gui/ignorelisteditor.h b/src/gui/ignorelisteditor.h index dbda14e28a3aa..025439a3ee531 100644 --- a/src/gui/ignorelisteditor.h +++ b/src/gui/ignorelisteditor.h @@ -37,7 +37,6 @@ private slots: private: void setupTableReadOnlyItems(); - QString readOnlyTooltip; Ui::IgnoreListEditor *ui; }; diff --git a/src/gui/ignorelisttablewidget.cpp b/src/gui/ignorelisttablewidget.cpp index 047d8251f6e12..e5d016ecaee0e 100644 --- a/src/gui/ignorelisttablewidget.cpp +++ b/src/gui/ignorelisttablewidget.cpp @@ -181,7 +181,6 @@ int IgnoreListTableWidget::addPattern(const QString &pattern, const bool deletab if (readOnly) { patternItem->setFlags(patternItem->flags() ^ Qt::ItemIsEnabled); - patternItem->setToolTip(readOnlyTooltip); deletableItem->setFlags(deletableItem->flags() ^ Qt::ItemIsEnabled); } diff --git a/src/gui/ignorelisttablewidget.h b/src/gui/ignorelisttablewidget.h index e0b1e0314ead1..f446955d69082 100644 --- a/src/gui/ignorelisttablewidget.h +++ b/src/gui/ignorelisttablewidget.h @@ -37,7 +37,6 @@ private slots: private: void setupTableReadOnlyItems(); - QString readOnlyTooltip; Ui::IgnoreListTableWidget *ui; }; } // namespace OCC From 74e49aae98997464238bd2e4816bb9b2a5a052a2 Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Thu, 5 Feb 2026 16:50:33 +0100 Subject: [PATCH 7/8] refactor(ignoredfiles): use the same ignore list editor for both global + folder-specific settings Signed-off-by: Jyrki Gadinger --- src/gui/accountsettings.cpp | 26 ++------- src/gui/ignorelisteditor.cpp | 107 +++++++++++++++++++++++------------ src/gui/ignorelisteditor.h | 17 +++++- src/gui/ignorelisteditor.ui | 6 +- 4 files changed, 94 insertions(+), 62 deletions(-) diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 3d2f2a0a1aecd..92940994cca91 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -32,7 +32,7 @@ #include "filesystem.h" #include "encryptfolderjob.h" #include "syncresult.h" -#include "ignorelisttablewidget.h" +#include "ignorelisteditor.h" #include "wizard/owncloudwizard.h" #include "networksettings.h" #include "ui_mnemonicdialog.h" @@ -551,28 +551,10 @@ void AccountSettings::openIgnoredFilesDialog(const QString & absFolderPath) Q_ASSERT(QFileInfo(absFolderPath).isAbsolute()); const QString ignoreFile{absFolderPath + ".sync-exclude.lst"}; - const auto layout = new QVBoxLayout(); - const auto ignoreListWidget = new IgnoreListTableWidget(this); - ignoreListWidget->readIgnoreFile(ignoreFile); - layout->addWidget(ignoreListWidget); - - const auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - layout->addWidget(buttonBox); - - const auto dialog = new QDialog(); - dialog->setWindowTitle(tr("Ignored Files Editor")); - dialog->setAttribute(Qt::WA_DeleteOnClose); - 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(); + auto ignoreListEditor = new IgnoreListEditor(ignoreFile, this); + ignoreListEditor->setAttribute(Qt::WA_DeleteOnClose); + ignoreListEditor->open(); } void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index, const QPoint& pos) diff --git a/src/gui/ignorelisteditor.cpp b/src/gui/ignorelisteditor.cpp index 5103f3f1cc913..0834da9ca4f81 100644 --- a/src/gui/ignorelisteditor.cpp +++ b/src/gui/ignorelisteditor.cpp @@ -9,45 +9,37 @@ #include "folderman.h" #include "generalsettings.h" #include "ignorelisteditor.h" +#include "ignorelisttablewidget.h" #include "ui_ignorelisteditor.h" -#include #include +#include +#include #include #include #include -#include +#include namespace OCC { IgnoreListEditor::IgnoreListEditor(QWidget *parent) - : QDialog(parent) - , ui(new Ui::IgnoreListEditor) + : QDialog{parent} + , ui{new Ui::IgnoreListEditor} + , _ignoreListType{IgnoreListType::Global} { - setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); - ui->setupUi(this); - ConfigFile cfgFile; + _ignoreFile = cfgFile.excludeFile(ConfigFile::Scope::UserScope); + setupUi(); +} - setupTableReadOnlyItems(); - const auto userConfig = cfgFile.excludeFile(ConfigFile::Scope::UserScope); - ui->ignoreTableWidget->readIgnoreFile(userConfig); - - connect(this, &QDialog::accepted, [=, this]() { - 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()); +IgnoreListEditor::IgnoreListEditor(const QString &ignoreFile, QWidget *parent) + : QDialog{parent} + , ui{new Ui::IgnoreListEditor} + , _ignoreFile{ignoreFile} + , _ignoreListType{IgnoreListType::Folder} +{ + setupUi(); + ui->groupboxGlobalIgnoreSettings->hide(); } IgnoreListEditor::~IgnoreListEditor() @@ -55,28 +47,73 @@ IgnoreListEditor::~IgnoreListEditor() delete ui; } -void IgnoreListEditor::setupTableReadOnlyItems() +bool IgnoreListEditor::ignoreHiddenFiles() const { - 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); + return !ui->syncHiddenFilesCheckBox->isChecked(); } -bool IgnoreListEditor::ignoreHiddenFiles() +void IgnoreListEditor::slotSaveIgnoreList() { - return !ui->syncHiddenFilesCheckBox->isChecked(); + // TODO: this will tell the file provider extension a different set of files to globally ignore + // when called from the local editor -- not good! + ui->ignoreTableWidget->slotWriteIgnoreFile(_ignoreFile); + + if (_ignoreListType != Global) { + return; + } + + /* handle the hidden file checkbox for the global ignore list editor */ + + /* 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()); } void IgnoreListEditor::slotRestoreDefaults(QAbstractButton *button) { - if(ui->buttonBox->buttonRole(button) != QDialogButtonBox::ResetRole) + if(ui->buttonBox->buttonRole(button) != QDialogButtonBox::ResetRole) { return; + } ui->ignoreTableWidget->slotRemoveAllItems(); - ConfigFile cfgFile; setupTableReadOnlyItems(); - ui->ignoreTableWidget->readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); + + if (_ignoreListType == Global) { + ConfigFile cfgFile; + ui->ignoreTableWidget->readIgnoreFile(cfgFile.excludeFile(ConfigFile::SystemScope), false); + return; + } + + ui->ignoreTableWidget->readIgnoreFile(_ignoreFile); +} + +void IgnoreListEditor::setupUi() +{ + setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); + ui->setupUi(this); + + setupTableReadOnlyItems(); + ui->ignoreTableWidget->readIgnoreFile(_ignoreFile); + + connect(this, &QDialog::accepted, this, &IgnoreListEditor::slotSaveIgnoreList); + connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &IgnoreListEditor::slotRestoreDefaults); + + ui->syncHiddenFilesCheckBox->setChecked(!FolderMan::instance()->ignoreHiddenFiles()); +} + +void IgnoreListEditor::setupTableReadOnlyItems() +{ + if (_ignoreListType != Global) { + return; + } + + 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); } } // namespace OCC diff --git a/src/gui/ignorelisteditor.h b/src/gui/ignorelisteditor.h index 025439a3ee531..2301eafbad050 100644 --- a/src/gui/ignorelisteditor.h +++ b/src/gui/ignorelisteditor.h @@ -27,17 +27,30 @@ class IgnoreListEditor : public QDialog Q_OBJECT public: + enum IgnoreListType { + Global, + Folder, + }; + IgnoreListEditor(QWidget *parent = nullptr); + IgnoreListEditor(const QString &ignoreFile, QWidget *parent = nullptr); + ~IgnoreListEditor() override; - bool ignoreHiddenFiles(); + [[nodiscard]] bool ignoreHiddenFiles() const; private slots: + void slotSaveIgnoreList(); void slotRestoreDefaults(QAbstractButton *button); private: - void setupTableReadOnlyItems(); Ui::IgnoreListEditor *ui; + QString _ignoreFile; + + IgnoreListType _ignoreListType; + + void setupUi(); + void setupTableReadOnlyItems(); }; } // namespace OCC diff --git a/src/gui/ignorelisteditor.ui b/src/gui/ignorelisteditor.ui index 3ebdf524938c0..518332201b225 100644 --- a/src/gui/ignorelisteditor.ui +++ b/src/gui/ignorelisteditor.ui @@ -15,7 +15,7 @@ - + Global Ignore Settings @@ -31,7 +31,7 @@ - + Files Ignored by Patterns @@ -45,7 +45,7 @@ - QDialogButtonBox::Cancel|QDialogButtonBox::Ok|QDialogButtonBox::RestoreDefaults + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok|QDialogButtonBox::StandardButton::RestoreDefaults From 274310fce7f7b89d399753ef0ca727d8a2e6cb7b Mon Sep 17 00:00:00 2001 From: Jyrki Gadinger Date: Thu, 5 Feb 2026 17:06:44 +0100 Subject: [PATCH 8/8] fix(ignoredfiles): handle ignore list write errors also log a message in case it failed Signed-off-by: Jyrki Gadinger --- src/gui/ignorelisttablewidget.cpp | 53 +++++++++++++++++++------------ 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/src/gui/ignorelisttablewidget.cpp b/src/gui/ignorelisttablewidget.cpp index e5d016ecaee0e..8fa34ee6e7085 100644 --- a/src/gui/ignorelisttablewidget.cpp +++ b/src/gui/ignorelisttablewidget.cpp @@ -24,6 +24,8 @@ static constexpr int patternCol = 0; static constexpr int deletableCol = 1; static constexpr int readOnlyRows = 3; +Q_LOGGING_CATEGORY(lcIgnoreListTableWidget, "nextcloud.gui.ignorelisttablewidget", QtInfoMsg) + IgnoreListTableWidget::IgnoreListTableWidget(QWidget *parent) : QWidget(parent) , ui(new Ui::IgnoreListTableWidget) @@ -84,30 +86,38 @@ 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 (auto row = 0; row < ui->tableWidget->rowCount(); ++row) { - const auto patternItem = ui->tableWidget->item(row, patternCol); - const auto 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 { + if (!ignores.open(QIODevice::WriteOnly)) { + qCWarning(lcIgnoreListTableWidget).nospace() << "failed to write ignore list" + << " file=" << file + << " errorString=" << ignores.errorString(); QMessageBox::warning(this, tr("Could not open file"), tr("Cannot write changes to \"%1\".").arg(file)); + + ignores.close(); + return; } - ignores.close(); //close the file before reloading stuff. + + // rewrite the whole file since the user can also remove system patterns + ignores.resize(0); + + for (auto row = 0; row < ui->tableWidget->rowCount(); ++row) { + const auto patternItem = ui->tableWidget->item(row, patternCol); + if (!(patternItem->flags() & Qt::ItemIsEnabled)) { + // skip read-only patterns + continue; + } + const auto deletableItem = ui->tableWidget->item(row, deletableCol); + + QByteArray prefix; + if (deletableItem && deletableItem->checkState() == Qt::Checked) { + prefix = "]"; + } else if (patternItem->text().startsWith('#')) { + prefix = "\\"; + } + ignores.write(prefix + patternItem->text().toUtf8() + '\n'); + } + ignores.close(); // close the file before reloading stuff. const auto folderMan = FolderMan::instance(); @@ -134,8 +144,9 @@ void IgnoreListTableWidget::slotAddPattern() {}, &okClicked); - if (!okClicked || pattern.isEmpty()) + if (!okClicked || pattern.isEmpty()) { return; + } addPattern(pattern, false, false); ui->tableWidget->scrollToBottom();