diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index 23e33c16999c..03719c6d2bf8 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -205,6 +205,33 @@ bool SyncJournalDb::maybeMigrateDb(const QString &localPath, const QString &abso return true; } +bool SyncJournalDb::findPathInSelectiveSyncList(const QStringList &list, const QString &path) +{ + Q_ASSERT(std::is_sorted(list.cbegin(), list.cend())); + + if (list.size() == 1 && list.first() == QStringLiteral("/")) { + // Special case for the case "/" is there, it matches everything + return true; + } + + const QString pathSlash = path + QLatin1Char('/'); + + // Since the list is sorted, we can do a binary search. + // If the path is a prefix of another item or right after in the lexical order. + auto it = std::lower_bound(list.cbegin(), list.cend(), pathSlash); + + if (it != list.cend() && *it == pathSlash) { + return true; + } + + if (it == list.cbegin()) { + return false; + } + --it; + Q_ASSERT(it->endsWith(QLatin1Char('/'))); // Folder::setSelectiveSyncBlackList makes sure of that + return pathSlash.startsWith(*it); +} + bool SyncJournalDb::exists() { QMutexLocker locker(&_mutex); @@ -1958,10 +1985,7 @@ QStringList SyncJournalDb::getSelectiveSyncList(SyncJournalDb::SelectiveSyncList if (!next.hasData) break; - auto entry = query->stringValue(0); - if (!entry.endsWith(QLatin1Char('/'))) { - entry.append(QLatin1Char('/')); - } + const auto entry = Utility::trailingSlashPath(query->stringValue(0)); result.append(entry); } *ok = true; @@ -1986,7 +2010,7 @@ void SyncJournalDb::setSelectiveSyncList(SyncJournalDb::SelectiveSyncListType ty } SqlQuery insQuery("INSERT INTO selectivesync VALUES (?1, ?2)", _db); - foreach (const auto &path, list) { + for (const auto &path : list) { insQuery.reset_and_clear_bindings(); insQuery.bindValue(1, path); insQuery.bindValue(2, int(type)); diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index d9a3e36b84b3..eb0ab0e21e0a 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -58,6 +58,9 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject /// Migrate a csync_journal to the new path, if necessary. Returns false on error static bool maybeMigrateDb(const QString &localPath, const QString &absoluteJournalPath); + /// Given a sorted list of paths ending with '/', return whether or not the given path is within one of the paths of the list + static bool findPathInSelectiveSyncList(const QStringList &list, const QString &path); + // To verify that the record could be found check with SyncJournalFileRecord::isValid() [[nodiscard]] bool getFileRecord(const QString &filename, SyncJournalFileRecord *rec) { return getFileRecord(filename.toUtf8(), rec); } [[nodiscard]] bool getFileRecord(const QByteArray &filename, SyncJournalFileRecord *rec); diff --git a/src/common/utility.cpp b/src/common/utility.cpp index 9baf1377720f..61f2489e4255 100644 --- a/src/common/utility.cpp +++ b/src/common/utility.cpp @@ -723,4 +723,10 @@ bool Utility::isCaseClashConflictFile(const QString &name) return bname.contains(QStringLiteral("(case clash from")); } +QString Utility::trailingSlashPath(const QString &path) +{ + static const auto slash = QLatin1Char('/'); + return path.endsWith(slash) ? path : QString(path + slash); +} + } // namespace OCC diff --git a/src/common/utility.h b/src/common/utility.h index 0dd693f284a7..b554a196be18 100644 --- a/src/common/utility.h +++ b/src/common/utility.h @@ -255,6 +255,8 @@ namespace Utility { */ OCSYNC_EXPORT void registerUriHandlerForLocalEditing(); + OCSYNC_EXPORT QString trailingSlashPath(const QString &path); + #ifdef Q_OS_WIN OCSYNC_EXPORT bool registryKeyExists(HKEY hRootKey, const QString &subKey); OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName); diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index b4ca6b3b7808..cc300174d506 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -14,6 +14,7 @@ #include "accountsettings.h" +#include "common/syncjournaldb.h" #include "common/syncjournalfilerecord.h" #include "qmessagebox.h" #include "ui_accountsettings.h" @@ -1480,48 +1481,81 @@ void AccountSettings::folderTerminateSyncAndUpdateBlackList(const QStringList &b void AccountSettings::refreshSelectiveSyncStatus() { - QString msg; - auto cnt = 0; + QString unsyncedFoldersString; + QString becameBigFoldersString; + const auto folders = FolderMan::instance()->map().values(); + + static const auto folderSeparatorString = QStringLiteral(", "); + static const auto folderLinkString = [](const QString &slashlessFolderPath, const QString &folderName) { + return QStringLiteral("%1").arg(slashlessFolderPath, folderName); + }; + static const auto appendFolderDisplayString = [](QString &foldersString, const QString &folderDisplayString) { + if (!foldersString.isEmpty()) { + foldersString += folderSeparatorString; + } + foldersString += folderDisplayString; + }; + _ui->bigFolderUi->setVisible(false); + for (const auto folder : folders) { if (folder->accountState() != _accountState) { continue; } auto ok = false; + auto blacklistOk = false; const auto undecidedList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &ok); + auto blacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &blacklistOk); + blacklist.sort(); + for (const auto &it : undecidedList) { // FIXME: add the folder alias in a hoover hint. // folder->alias() + QLatin1String("/") - if (cnt++) { - msg += QLatin1String(", "); - } - auto myFolder = (it); - if (myFolder.endsWith('/')) { - myFolder.chop(1); - } - const auto theIndx = _model->indexForPath(folder, myFolder); - if (theIndx.isValid()) { - msg += QString::fromLatin1("%1") - .arg(Utility::escape(myFolder), Utility::escape(folder->alias())); + + const auto folderTrailingSlash = Utility::trailingSlashPath(it); + const auto folderWithoutTrailingSlash = it.endsWith('/') ? it.left(it.length() - 1) : it; + const auto escapedFolderString = Utility::escape(folderWithoutTrailingSlash); + const auto escapedFolderName = Utility::escape(folder->alias()); + const auto folderIdx = _model->indexForPath(folder, folderWithoutTrailingSlash); + + // If we do not know the index yet then do not provide a link string + const auto folderDisplayString = folderIdx.isValid() ? folderLinkString(escapedFolderString, escapedFolderName) : folderWithoutTrailingSlash; + + // The new big folder procedure automatically places these new big folders in the blacklist. + // This is not the case for existing folders discovered to have gone beyond the limit. + // So we need to check if the folder is in the blacklist or not and tweak the message accordingly. + if (SyncJournalDb::findPathInSelectiveSyncList(blacklist, folderTrailingSlash)) { + appendFolderDisplayString(unsyncedFoldersString, folderDisplayString); } else { - msg += myFolder; // no link because we do not know the index yet. + appendFolderDisplayString(becameBigFoldersString, folderDisplayString); } } } - if (!msg.isEmpty()) { - ConfigFile cfg; - const auto info = !cfg.confirmExternalStorage() ? - tr("There are folders that were not synchronized because they are too big: ") : - !cfg.newBigFolderSizeLimit().first ? - tr("There are folders that were not synchronized because they are external storages: ") : - tr("There are folders that were not synchronized because they are too big or external storages: "); + ConfigFile cfg; + QString infoString; - _ui->selectiveSyncNotification->setText(info + msg); - _ui->bigFolderUi->setVisible(true); + if (!unsyncedFoldersString.isEmpty()) { + infoString += !cfg.confirmExternalStorage() ? tr("There are folders that were not synchronized because they are too big: ") + : !cfg.newBigFolderSizeLimit().first ? tr("There are folders that were not synchronized because they are external storages: ") + : tr("There are folders that were not synchronized because they are too big or external storages: "); + + infoString += unsyncedFoldersString; } + + if (!becameBigFoldersString.isEmpty()) { + if (!infoString.isEmpty()) { + infoString += QStringLiteral("\n"); + } + + const auto folderSizeLimitString = QString::number(cfg.newBigFolderSizeLimit().second); + infoString += tr("There are folders that have grown in size beyond %1MB: %2").arg(folderSizeLimitString, becameBigFoldersString); + } + + _ui->selectiveSyncNotification->setText(infoString); + _ui->bigFolderUi->setVisible(!infoString.isEmpty()); } bool AccountSettings::event(QEvent *e) diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 8d15e8da3021..0511cba81a10 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -13,6 +13,7 @@ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ +#include "common/syncjournaldb.h" #include "config.h" #include "account.h" @@ -52,6 +53,7 @@ namespace { constexpr auto versionC = "version"; #endif } + namespace OCC { Q_LOGGING_CATEGORY(lcFolder, "nextcloud.gui.folder", QtInfoMsg) @@ -102,6 +104,7 @@ Folder::Folder(const FolderDefinition &definition, this, &Folder::slotItemCompleted); connect(_engine.data(), &SyncEngine::newBigFolder, this, &Folder::slotNewBigFolderDiscovered); + connect(_engine.data(), &SyncEngine::existingFolderNowBig, this, &Folder::slotExistingFolderNowBig); connect(_engine.data(), &SyncEngine::seenLockedFile, FolderMan::instance(), &FolderMan::slotSyncOnceFileUnlocks); connect(_engine.data(), &SyncEngine::aboutToPropagate, this, &Folder::slotLogPropagationStart); @@ -167,10 +170,10 @@ void Folder::checkLocalPath() if (_canonicalLocalPath.isEmpty()) { qCWarning(lcFolder) << "Broken symlink:" << _definition.localPath; _canonicalLocalPath = _definition.localPath; - } else if (!_canonicalLocalPath.endsWith('/')) { - _canonicalLocalPath.append('/'); } + _canonicalLocalPath = Utility::trailingSlashPath(_canonicalLocalPath); + if (fi.isDir() && fi.isReadable()) { qCDebug(lcFolder) << "Checked local path ok"; } else { @@ -214,10 +217,8 @@ QString Folder::path() const QString Folder::shortGuiLocalPath() const { QString p = _definition.localPath; - QString home = QDir::homePath(); - if (!home.endsWith('/')) { - home.append('/'); - } + const auto home = Utility::trailingSlashPath(QDir::homePath()); + if (p.startsWith(home)) { p = p.mid(home.length()); } @@ -266,10 +267,7 @@ QString Folder::remotePath() const QString Folder::remotePathTrailingSlash() const { - QString result = remotePath(); - if (!result.endsWith('/')) - result.append('/'); - return result; + return Utility::trailingSlashPath(remotePath()); } QUrl Folder::remoteUrl() const @@ -484,7 +482,7 @@ void Folder::createGuiLog(const QString &filename, LogStatus status, int count, if (!text.isEmpty()) { // Ignores the settings in case of an error or conflict if(status == LogStatusError || status == LogStatusConflict) - logger->postOptionalGuiLog(tr("Sync Activity"), text); + logger->postGuiLog(tr("Sync Activity"), text); } } } @@ -840,6 +838,46 @@ bool Folder::pathIsIgnored(const QString &path) const return false; } +void Folder::appendPathToSelectiveSyncList(const QString &path, const SyncJournalDb::SelectiveSyncListType listType) +{ + const auto folderPath = Utility::trailingSlashPath(path); + const auto journal = journalDb(); + auto ok = false; + auto list = journal->getSelectiveSyncList(listType, &ok); + + if (ok) { + list.append(folderPath); + journal->setSelectiveSyncList(listType, list); + } +} + +void Folder::removePathFromSelectiveSyncList(const QString &path, const SyncJournalDb::SelectiveSyncListType listType) +{ + const auto folderPath = Utility::trailingSlashPath(path); + const auto journal = journalDb(); + auto ok = false; + auto list = journal->getSelectiveSyncList(listType, &ok); + + if (ok) { + list.removeAll(folderPath); + journal->setSelectiveSyncList(listType, list); + } +} + +void Folder::whitelistPath(const QString &path) +{ + removePathFromSelectiveSyncList(path, SyncJournalDb::SelectiveSyncUndecidedList); + removePathFromSelectiveSyncList(path, SyncJournalDb::SelectiveSyncBlackList); + appendPathToSelectiveSyncList(path, SyncJournalDb::SelectiveSyncWhiteList); +} + +void Folder::blacklistPath(const QString &path) +{ + removePathFromSelectiveSyncList(path, SyncJournalDb::SelectiveSyncUndecidedList); + removePathFromSelectiveSyncList(path, SyncJournalDb::SelectiveSyncWhiteList); + appendPathToSelectiveSyncList(path, SyncJournalDb::SelectiveSyncBlackList); +} + bool Folder::isFileExcludedAbsolute(const QString &fullPath) const { return _engine->excludedFiles().isExcluded(fullPath, path(), _definition.ignoreHiddenFiles); @@ -1092,11 +1130,6 @@ void Folder::slotSyncFinished(bool success) qCInfo(lcFolder) << "the last" << _consecutiveFailingSyncs << "syncs failed"; } - if (_syncResult.status() == SyncResult::Success && success) { - // Clear the white list as all the folders that should be on that list are sync-ed - journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, QStringList()); - } - if ((_syncResult.status() == SyncResult::Success || _syncResult.status() == SyncResult::Problem) && success) { @@ -1183,10 +1216,7 @@ void Folder::slotItemCompleted(const SyncFileItemPtr &item, ErrorCategory errorC void Folder::slotNewBigFolderDiscovered(const QString &newF, bool isExternal) { - auto newFolder = newF; - if (!newFolder.endsWith(QLatin1Char('/'))) { - newFolder += QLatin1Char('/'); - } + const auto newFolder = Utility::trailingSlashPath(newF); auto journal = journalDb(); // Add the entry to the blacklist if it is neither in the blacklist or whitelist already @@ -1214,10 +1244,102 @@ void Folder::slotNewBigFolderDiscovered(const QString &newF, bool isExternal) message += tr("Please go in the settings to select it if you wish to download it."); auto logger = Logger::instance(); - logger->postOptionalGuiLog(Theme::instance()->appNameGUI(), message); + logger->postGuiLog(Theme::instance()->appNameGUI(), message); + } +} + +void Folder::slotExistingFolderNowBig(const QString &folderPath) +{ + const auto trailSlashFolderPath = Utility::trailingSlashPath(folderPath); + const auto journal = journalDb(); + const auto stopSyncing = ConfigFile().stopSyncingExistingFoldersOverLimit(); + + // Add the entry to the whitelist if it is neither in the blacklist or whitelist already + bool ok1 = false; + bool ok2 = false; + auto blacklist = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok1); + auto whitelist = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncWhiteList, &ok2); + + const auto inDecidedLists = blacklist.contains(trailSlashFolderPath) || whitelist.contains(trailSlashFolderPath); + if (inDecidedLists) { + return; + } + + auto relevantList = stopSyncing ? blacklist : whitelist; + const auto relevantListType = stopSyncing ? SyncJournalDb::SelectiveSyncBlackList : SyncJournalDb::SelectiveSyncWhiteList; + + if (ok1 && ok2 && !inDecidedLists) { + relevantList.append(trailSlashFolderPath); + journal->setSelectiveSyncList(relevantListType, relevantList); + + if (stopSyncing) { + // Abort current down sync and start again + slotTerminateSync(); + scheduleThisFolderSoon(); + } + } + + auto undecidedListQueryOk = false; + auto undecidedList = journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, &undecidedListQueryOk); + if (undecidedListQueryOk) { + if (!undecidedList.contains(trailSlashFolderPath)) { + undecidedList.append(trailSlashFolderPath); + journal->setSelectiveSyncList(SyncJournalDb::SelectiveSyncUndecidedList, undecidedList); + emit newBigFolderDiscovered(trailSlashFolderPath); + } + + postExistingFolderNowBigNotification(folderPath); + postExistingFolderNowBigActivity(folderPath); } } +void Folder::postExistingFolderNowBigNotification(const QString &folderPath) +{ + const auto stopSyncing = ConfigFile().stopSyncingExistingFoldersOverLimit(); + const auto messageInstruction = + stopSyncing ? "Synchronisation of this folder has been disabled." : "Synchronisation of this folder can be disabled in the settings window."; + const auto message = tr("A folder has surpassed the set folder size limit of %1MB: %2.\n%3") + .arg(QString::number(ConfigFile().newBigFolderSizeLimit().second), folderPath, messageInstruction); + Logger::instance()->postGuiLog(Theme::instance()->appNameGUI(), message); +} + +void Folder::postExistingFolderNowBigActivity(const QString &folderPath) const +{ + const auto stopSyncing = ConfigFile().stopSyncingExistingFoldersOverLimit(); + const auto trailSlashFolderPath = Utility::trailingSlashPath(folderPath); + + auto whitelistActivityLink = ActivityLink(); + whitelistActivityLink._label = tr("Keep syncing"); + whitelistActivityLink._primary = false; + whitelistActivityLink._verb = ActivityLink::WhitelistFolderVerb; + + QVector activityLinks = {whitelistActivityLink}; + + if (!stopSyncing) { + auto blacklistActivityLink = ActivityLink(); + blacklistActivityLink._label = tr("Stop syncing"); + blacklistActivityLink._primary = true; + blacklistActivityLink._verb = ActivityLink::BlacklistFolderVerb; + + activityLinks.append(blacklistActivityLink); + } + + auto existingFolderNowBigActivity = Activity(); + existingFolderNowBigActivity._type = Activity::NotificationType; + existingFolderNowBigActivity._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate); + existingFolderNowBigActivity._subject = + tr("The folder %1 has surpassed the set folder size limit of %2MB.").arg(folderPath, QString::number(ConfigFile().newBigFolderSizeLimit().second)); + existingFolderNowBigActivity._message = tr("Would you like to stop syncing this folder?"); + existingFolderNowBigActivity._accName = _accountState->account()->displayName(); + existingFolderNowBigActivity._folder = alias(); + existingFolderNowBigActivity._file = cleanPath() + '/' + trailSlashFolderPath; + existingFolderNowBigActivity._links = activityLinks; + existingFolderNowBigActivity._id = qHash(existingFolderNowBigActivity._file); + + const auto user = UserModel::instance()->findUserForAccount(_accountState.data()); + user->slotAddNotification(this, existingFolderNowBigActivity); +} + void Folder::slotLogPropagationStart() { _fileLog->logLap("Propagation starts"); @@ -1283,7 +1405,7 @@ void Folder::warnOnNewExcludedItem(const SyncJournalFileRecord &record, const QS "It will not be synchronized.") .arg(fi.filePath()); - Logger::instance()->postOptionalGuiLog(Theme::instance()->appNameGUI(), message); + Logger::instance()->postGuiLog(Theme::instance()->appNameGUI(), message); } void Folder::slotWatcherUnreliable(const QString &message) @@ -1451,7 +1573,7 @@ void Folder::removeLocalE2eFiles() } if (!parentPathEncrypted) { - const auto pathAdjusted = rec._path.endsWith('/') ? rec._path : QString(rec._path + QStringLiteral("/")); + const auto pathAdjusted = Utility::trailingSlashPath(rec._path); e2eFoldersToBlacklist.append(pathAdjusted); } } @@ -1566,11 +1688,8 @@ bool FolderDefinition::load(QSettings &settings, const QString &alias, QString FolderDefinition::prepareLocalPath(const QString &path) { - QString p = QDir::fromNativeSeparators(path); - if (!p.endsWith(QLatin1Char('/'))) { - p.append(QLatin1Char('/')); - } - return p; + const auto normalisedPath = QDir::fromNativeSeparators(path); + return Utility::trailingSlashPath(normalisedPath); } QString FolderDefinition::prepareTargetPath(const QString &path) diff --git a/src/gui/folder.h b/src/gui/folder.h index cac47809ba11..13f493102ee9 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -301,6 +301,9 @@ class Folder : public QObject QString fileFromLocalPath(const QString &localPath) const; + void whitelistPath(const QString &path); + void blacklistPath(const QString &path); + signals: void syncStateChange(); void syncStarted(); @@ -405,6 +408,7 @@ private slots: void slotEmitFinishedDelayed(); void slotNewBigFolderDiscovered(const QString &, bool isExternal); + void slotExistingFolderNowBig(const QString &folderPath); void slotLogPropagationStart(); @@ -467,6 +471,12 @@ private slots: void correctPlaceholderFiles(); + void appendPathToSelectiveSyncList(const QString &path, const SyncJournalDb::SelectiveSyncListType listType); + void removePathFromSelectiveSyncList(const QString &path, const SyncJournalDb::SelectiveSyncListType listType); + + static void postExistingFolderNowBigNotification(const QString &folderPath); + void postExistingFolderNowBigActivity(const QString &folderPath) const; + AccountStatePtr _accountState; FolderDefinition _definition; QString _canonicalLocalPath; // As returned with QFileInfo:canonicalFilePath. Always ends with "/" diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 27a79520036f..f41a3142480b 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -1259,6 +1259,38 @@ Folder *FolderMan::folderForPath(const QString &path) return it != folders.cend() ? *it : nullptr; } +void FolderMan::addFolderToSelectiveSyncList(const QString &path, const SyncJournalDb::SelectiveSyncListType list) +{ + const auto folder = folderForPath(path); + if (!folder) { + return; + } + + const QString folderPath = folder->cleanPath() + QLatin1Char('/'); + const auto relPath = path.mid(folderPath.length()); + + switch (list) { + case SyncJournalDb::SelectiveSyncListType::SelectiveSyncWhiteList: + folder->whitelistPath(relPath); + break; + case SyncJournalDb::SelectiveSyncListType::SelectiveSyncBlackList: + folder->blacklistPath(relPath); + break; + default: + Q_UNREACHABLE(); + } +} + +void FolderMan::whitelistFolderPath(const QString &path) +{ + addFolderToSelectiveSyncList(path, SyncJournalDb::SelectiveSyncListType::SelectiveSyncWhiteList); +} + +void FolderMan::blacklistFolderPath(const QString &path) +{ + addFolderToSelectiveSyncList(path, SyncJournalDb::SelectiveSyncListType::SelectiveSyncBlackList); +} + QStringList FolderMan::findFileInLocalFolders(const QString &relPath, const AccountPtr acc) { QStringList re; diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 6c236ce8e8a4..684ba29829e4 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -104,6 +104,10 @@ class FolderMan : public QObject /** Returns the folder which the file or directory stored in path is in */ Folder *folderForPath(const QString &path); + // Takes local file paths and finds the corresponding folder, adding to correct selective sync list + void whitelistFolderPath(const QString &path); + void blacklistFolderPath(const QString &path); + /** * returns a list of local files that exist on the local harddisk for an * incoming relative server path. The method checks with all existing sync @@ -354,6 +358,8 @@ private slots: [[nodiscard]] bool isSwitchToVfsNeeded(const FolderDefinition &folderDefinition) const; + void addFolderToSelectiveSyncList(const QString &path, const SyncJournalDb::SelectiveSyncListType list); + QSet _disabledFolders; Folder::Map _folderMap; QString _folderConfigPath; diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index 8a92b291ab67..061efc8624b0 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -13,12 +13,13 @@ */ #include "folderstatusmodel.h" -#include "folderman.h" #include "accountstate.h" #include "common/asserts.h" -#include -#include +#include "common/utility.h" +#include "folderman.h" #include "folderstatusdelegate.h" +#include +#include #include #include @@ -703,9 +704,7 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list) parentInfo->_fetched = true; QUrl url = parentInfo->_folder->remoteUrl(); - QString pathToRemove = url.path(); - if (!pathToRemove.endsWith('/')) - pathToRemove += '/'; + const auto pathToRemove = Utility::trailingSlashPath(url.path()); QStringList selectiveSyncBlackList; bool ok1 = true; diff --git a/src/gui/folderwatcher_win.h b/src/gui/folderwatcher_win.h index d72665f0607d..6c4a261e0e05 100644 --- a/src/gui/folderwatcher_win.h +++ b/src/gui/folderwatcher_win.h @@ -15,8 +15,9 @@ #ifndef MIRALL_FOLDERWATCHER_WIN_H #define MIRALL_FOLDERWATCHER_WIN_H -#include +#include "common/utility.h" #include +#include #include namespace OCC { @@ -33,7 +34,7 @@ class WatcherThread : public QThread public: WatcherThread(const QString &path) : QThread() - , _path(path + (path.endsWith(QLatin1Char('/')) ? QString() : QStringLiteral("/"))) + , _path(Utility::trailingSlashPath(path)) , _directory(0) , _resultEvent(0) , _stopEvent(0) diff --git a/src/gui/generalsettings.cpp b/src/gui/generalsettings.cpp index 0296c7083280..394d13c0d977 100644 --- a/src/gui/generalsettings.cpp +++ b/src/gui/generalsettings.cpp @@ -186,6 +186,8 @@ GeneralSettings::GeneralSettings(QWidget *parent) connect(_ui->crashreporterCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); connect(_ui->newFolderLimitCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); connect(_ui->newFolderLimitSpinBox, static_cast(&QSpinBox::valueChanged), this, &GeneralSettings::saveMiscSettings); + connect(_ui->existingFolderLimitCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); + connect(_ui->stopExistingFolderNowBigSyncCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); connect(_ui->newExternalStorage, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); connect(_ui->moveFilesToTrashCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::saveMiscSettings); @@ -261,6 +263,12 @@ void GeneralSettings::loadMiscSettings() auto newFolderLimit = cfgFile.newBigFolderSizeLimit(); _ui->newFolderLimitCheckBox->setChecked(newFolderLimit.first); _ui->newFolderLimitSpinBox->setValue(newFolderLimit.second); + _ui->existingFolderLimitCheckBox->setEnabled(_ui->newFolderLimitCheckBox->isChecked()); + _ui->existingFolderLimitCheckBox->setChecked(_ui->newFolderLimitCheckBox->isChecked() && cfgFile.notifyExistingFoldersOverLimit()); + _ui->stopExistingFolderNowBigSyncCheckBox->setEnabled(_ui->existingFolderLimitCheckBox->isChecked()); + _ui->stopExistingFolderNowBigSyncCheckBox->setChecked(_ui->existingFolderLimitCheckBox->isChecked() && cfgFile.stopSyncingExistingFoldersOverLimit()); + _ui->newExternalStorage->setChecked(cfgFile.confirmExternalStorage()); + _ui->monoIconsCheckBox->setChecked(cfgFile.monoIcons()); } #if defined(BUILD_UPDATER) @@ -274,8 +282,12 @@ void GeneralSettings::slotUpdateInfo() } if (updater) { - connect(_ui->updateButton, &QAbstractButton::clicked, this, - &GeneralSettings::slotUpdateCheckNow, Qt::UniqueConnection); + connect(_ui->updateButton, + &QAbstractButton::clicked, + this, + + &GeneralSettings::slotUpdateCheckNow, + Qt::UniqueConnection); connect(_ui->autoCheckForUpdatesCheckBox, &QAbstractButton::toggled, this, &GeneralSettings::slotToggleAutoUpdateCheck, Qt::UniqueConnection); _ui->autoCheckForUpdatesCheckBox->setChecked(ConfigFile().autoUpdateCheck()); @@ -422,14 +434,22 @@ void GeneralSettings::saveMiscSettings() ConfigFile cfgFile; - const auto monoIconsChecked = _ui->monoIconsCheckBox->isChecked(); - cfgFile.setMonoIcons(monoIconsChecked); - Theme::instance()->setSystrayUseMonoIcons(monoIconsChecked); + const auto useMonoIcons = _ui->monoIconsCheckBox->isChecked(); + const auto newFolderLimitEnabled = _ui->newFolderLimitCheckBox->isChecked(); + const auto existingFolderLimitEnabled = newFolderLimitEnabled && _ui->existingFolderLimitCheckBox->isChecked(); + const auto stopSyncingExistingFoldersOverLimit = existingFolderLimitEnabled && _ui->stopExistingFolderNowBigSyncCheckBox->isChecked(); + Theme::instance()->setSystrayUseMonoIcons(useMonoIcons); + cfgFile.setMonoIcons(useMonoIcons); cfgFile.setCrashReporter(_ui->crashreporterCheckBox->isChecked()); - cfgFile.setNewBigFolderSizeLimit(_ui->newFolderLimitCheckBox->isChecked(), _ui->newFolderLimitSpinBox->value()); - cfgFile.setConfirmExternalStorage(_ui->newExternalStorage->isChecked()); cfgFile.setMoveToTrash(_ui->moveFilesToTrashCheckBox->isChecked()); + cfgFile.setNewBigFolderSizeLimit(newFolderLimitEnabled, _ui->newFolderLimitSpinBox->value()); + cfgFile.setConfirmExternalStorage(_ui->newExternalStorage->isChecked()); + cfgFile.setNotifyExistingFoldersOverLimit(existingFolderLimitEnabled); + cfgFile.setStopSyncingExistingFoldersOverLimit(stopSyncingExistingFoldersOverLimit); + + _ui->existingFolderLimitCheckBox->setEnabled(newFolderLimitEnabled); + _ui->stopExistingFolderNowBigSyncCheckBox->setEnabled(existingFolderLimitEnabled); } void GeneralSettings::slotToggleLaunchOnStartup(bool enable) diff --git a/src/gui/generalsettings.ui b/src/gui/generalsettings.ui index ee82031c911e..dad84c66c7ea 100644 --- a/src/gui/generalsettings.ui +++ b/src/gui/generalsettings.ui @@ -6,8 +6,8 @@ 0 0 - 556 - 613 + 581 + 663 @@ -222,46 +222,94 @@ - - - - - Ask for confirmation before synchronizing folders larger than - - - true - - - + + + 0 + - - - 999999 - - - 99 - - + + + + + Ask for confirmation before synchronizing new folders larger than + + + true + + + + + + + 999999 + + + 99 + + + + + + + MB + + + + - - - MB - - + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Notify when synchronised folders grow larger than specified limit + + + + - - - Qt::Horizontal - - - - 40 - 20 - - - + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + Automatically disable synchronisation of folders that overcome limit + + + + diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index 2ea725564937..9d1d5856f340 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -107,12 +107,8 @@ ownCloudGui::ownCloudGui(Application *parent) connect(folderMan, &FolderMan::folderSyncStateChange, this, &ownCloudGui::slotSyncStateChange); - connect(Logger::instance(), &Logger::guiLog, - this, &ownCloudGui::slotShowTrayMessage); - connect(Logger::instance(), &Logger::optionalGuiLog, - this, &ownCloudGui::slotShowOptionalTrayMessage); - connect(Logger::instance(), &Logger::guiMessage, - this, &ownCloudGui::slotShowGuiMessage); + connect(Logger::instance(), &Logger::guiLog, this, &ownCloudGui::slotShowTrayMessage); + connect(Logger::instance(), &Logger::guiMessage, this, &ownCloudGui::slotShowGuiMessage); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "SyncStatusSummary"); qmlRegisterType("com.nextcloud.desktopclient", 1, 0, "EmojiModel"); @@ -426,11 +422,6 @@ void ownCloudGui::slotShowTrayUpdateMessage(const QString &title, const QString } } -void ownCloudGui::slotShowOptionalTrayMessage(const QString &title, const QString &msg) -{ - slotShowTrayMessage(title, msg); -} - /* * open the folder with the given Alias */ diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index 41c7895ef686..8315fe228caa 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -76,7 +76,6 @@ public slots: void slotComputeOverallSyncStatus(); void slotShowTrayMessage(const QString &title, const QString &msg); void slotShowTrayUpdateMessage(const QString &title, const QString &msg, const QUrl &webUrl); - void slotShowOptionalTrayMessage(const QString &title, const QString &msg); void slotFolderOpenAction(const QString &alias); void slotUpdateProgress(const QString &folder, const OCC::ProgressInfo &progress); void slotShowGuiMessage(const QString &title, const QString &message); diff --git a/src/gui/owncloudsetupwizard.cpp b/src/gui/owncloudsetupwizard.cpp index 551fc487dc72..b0354b58e03b 100644 --- a/src/gui/owncloudsetupwizard.cpp +++ b/src/gui/owncloudsetupwizard.cpp @@ -20,19 +20,20 @@ #include #include -#include "wizard/owncloudwizardcommon.h" -#include "wizard/owncloudwizard.h" -#include "owncloudsetupwizard.h" -#include "configfile.h" -#include "folderman.h" #include "accessmanager.h" #include "account.h" -#include "networkjobs.h" -#include "sslerrordialog.h" #include "accountmanager.h" #include "clientproxy.h" +#include "common/utility.h" +#include "configfile.h" #include "filesystem.h" +#include "folderman.h" +#include "networkjobs.h" #include "owncloudgui.h" +#include "owncloudsetupwizard.h" +#include "sslerrordialog.h" +#include "wizard/owncloudwizard.h" +#include "wizard/owncloudwizardcommon.h" #include "creds/credentialsfactory.h" #include "creds/abstractcredentials.h" @@ -125,13 +126,7 @@ void OwncloudSetupWizard::startWizard() } // remember the local folder to compare later if it changed, but clean first - QString lf = QDir::fromNativeSeparators(localFolder); - if (!lf.endsWith(QLatin1Char('/'))) { - lf.append(QLatin1Char('/')); - } - - _initLocalFolder = lf; - + _initLocalFolder = Utility::trailingSlashPath(QDir::fromNativeSeparators(localFolder)); _ocWizard->setRemoteFolder(_remoteFolder); const auto isEnforcedServerSetup = diff --git a/src/gui/selectivesyncdialog.cpp b/src/gui/selectivesyncdialog.cpp index 4f4ca1ec1fc2..d83d1b9cdf7b 100644 --- a/src/gui/selectivesyncdialog.cpp +++ b/src/gui/selectivesyncdialog.cpp @@ -12,23 +12,23 @@ * for more details. */ #include "selectivesyncdialog.h" -#include "folder.h" #include "account.h" +#include "common/utility.h" +#include "configfile.h" +#include "folder.h" +#include "folderman.h" #include "networkjobs.h" #include "theme.h" -#include "folderman.h" -#include "configfile.h" #include -#include -#include -#include #include #include -#include +#include #include +#include +#include #include -#include #include +#include namespace OCC { @@ -203,10 +203,7 @@ void SelectiveSyncWidget::slotUpdateDirectories(QStringList list) auto *root = dynamic_cast(_folderTree->topLevelItem(0)); QUrl url = _account->davUrl(); - QString pathToRemove = url.path(); - if (!pathToRemove.endsWith('/')) { - pathToRemove.append('/'); - } + auto pathToRemove = Utility::trailingSlashPath(url.path()); pathToRemove.append(_folderPath); if (!_folderPath.isEmpty()) pathToRemove.append('/'); diff --git a/src/gui/socketapi/socketuploadjob.cpp b/src/gui/socketapi/socketuploadjob.cpp index b8ee787a1145..6e4ebc968909 100644 --- a/src/gui/socketapi/socketuploadjob.cpp +++ b/src/gui/socketapi/socketuploadjob.cpp @@ -13,6 +13,7 @@ */ #include "socketuploadjob.h" +#include "common/utility.h" #include "socketapi_p.h" #include "accountmanager.h" @@ -55,7 +56,7 @@ SocketUploadJob::SocketUploadJob(const QSharedPointer &job) SyncOptions opt; opt.fillFromEnvironmentVariables(); opt.verifyChunkSizes(); - _engine = new SyncEngine(account->account(), _localPath.endsWith(QLatin1Char('/')) ? _localPath : _localPath + QLatin1Char('/'), opt, _remotePath, _db); + _engine = new SyncEngine(account->account(), Utility::trailingSlashPath(_localPath), opt, _remotePath, _db); _engine->setParent(_db); connect(_engine, &OCC::SyncEngine::itemCompleted, this, [this](const OCC::SyncFileItemPtr item) { diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h index 29df3c878cc3..e613ad5bf5dd 100644 --- a/src/gui/tray/activitydata.h +++ b/src/gui/tray/activitydata.h @@ -45,6 +45,9 @@ class ActivityLink public: static ActivityLink createFomJsonObject(const QJsonObject &obj); + static constexpr auto WhitelistFolderVerb = "WHITELIST_FOLDER"; + static constexpr auto BlacklistFolderVerb = "BLACKLIST_FOLDER"; + public: QString _imageSource; QString _imageSourceHovered; diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index f6c28a85d359..b43cb45c016f 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -836,6 +836,14 @@ void ActivityListModel::slotTriggerAction(const int activityIndex, const int act (activity._syncFileItemStatus == SyncFileItem::Conflict || activity._syncFileItemStatus == SyncFileItem::FileNameClash)) { slotTriggerDefaultAction(activityIndex); return; + } else if (action._verb == ActivityLink::WhitelistFolderVerb && !activity._file.isEmpty()) { // _folder == folder alias/name, _file == folder/file path + FolderMan::instance()->whitelistFolderPath(activity._file); + removeActivityFromActivityList(activity); + return; + } else if (action._verb == ActivityLink::BlacklistFolderVerb && !activity._file.isEmpty()) { + FolderMan::instance()->blacklistFolderPath(activity._file); + removeActivityFromActivityList(activity); + return; } emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex); @@ -863,13 +871,16 @@ QVariantList ActivityListModel::convertLinksToActionButtons(const Activity &acti { QVariantList customList; - for (const auto &activityLink : activity._links) { - if (!activityLink._primary) { + for (int i = 0; i < activity._links.size() && static_cast(i) <= maxActionButtons(); ++i) { + const auto activityLink = activity._links[i]; + + // Use the isDismissable model role to present custom dismiss button if needed + // Also don't show "View chat" for talk activities, default action will open chat anyway + if (activityLink._verb == "DELETE" || (activityLink._verb == "WEB" && activity._objectType == "chat")) { continue; } customList << ActivityListModel::convertLinkToActionButton(activityLink); - break; } return customList; @@ -893,16 +904,16 @@ QVariant ActivityListModel::convertLinkToActionButton(const OCC::ActivityLink &a QVariantList ActivityListModel::convertLinksToMenuEntries(const Activity &activity) { + if (static_cast(activity._links.size()) <= maxActionButtons()) { + return {}; + } + QVariantList customList; - if (static_cast(activity._links.size()) > maxActionButtons()) { - for (int i = 0; i < activity._links.size(); ++i) { - const auto &activityLink = activity._links[i]; - if (!activityLink._primary) { - customList << QVariantMap{ - {QStringLiteral("actionIndex"), i}, {QStringLiteral("label"), activityLink._label}}; - } - } + for (int i = maxActionButtons(); i < activity._links.size(); ++i) { + const auto activityLinkLabel = activity._links[i]._label; + const auto menuEntry = QVariantMap{{"actionIndex", i}, {"label", activityLinkLabel}}; + customList << menuEntry; } return customList; diff --git a/src/gui/tray/notificationhandler.cpp b/src/gui/tray/notificationhandler.cpp index 62ff2f32832b..a1321d4c7dc0 100644 --- a/src/gui/tray/notificationhandler.cpp +++ b/src/gui/tray/notificationhandler.cpp @@ -149,14 +149,6 @@ void ServerNotificationHandler::slotNotificationsReceived(const QJsonDocument &j a._links.insert(al._primary? 0 : a._links.size(), al); } - if (a._links.isEmpty()) { - ActivityLink dismissLink; - dismissLink._label = tr("Dismiss"); - dismissLink._verb = "DELETE"; - dismissLink._primary = false; - a._links.insert(0, dismissLink); - } - QUrl link(json.value("link").toString()); if (!link.isEmpty()) { if (link.host().isEmpty()) { diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index e9a5ed73ae43..896ff85e4d86 100644 --- a/src/gui/tray/usermodel.cpp +++ b/src/gui/tray/usermodel.cpp @@ -733,6 +733,16 @@ void User::slotAddErrorToGui(const QString &folderAlias, const SyncFileItem::Sta } } +void User::slotAddNotification(const Folder *folder, const Activity &activity) +{ + if (!isActivityOfCurrentAccount(folder) || _notifiedNotifications.contains(activity._id)) { + return; + } + + _notifiedNotifications.insert(activity._id); + _activityModel->addNotificationToActivityList(activity); +} + bool User::isActivityOfCurrentAccount(const Folder *folder) const { return folder->accountState() == _account.data(); @@ -1526,6 +1536,21 @@ User *UserModel::currentUser() const return _users[currentUserId()]; } +User *UserModel::findUserForAccount(AccountState *account) const +{ + Q_ASSERT(account); + + const auto it = std::find_if(_users.cbegin(), _users.cend(), [account](const User *user) { + return user->account()->id() == account->account()->id(); + }); + + if (it == _users.cend()) { + return nullptr; + } + + return *it; +} + int UserModel::findUserIdForAccount(AccountState *account) const { const auto it = std::find_if(std::cbegin(_users), std::cend(_users), [=](const User *user) { diff --git a/src/gui/tray/usermodel.h b/src/gui/tray/usermodel.h index bfeaff46d248..c92e98b7c938 100644 --- a/src/gui/tray/usermodel.h +++ b/src/gui/tray/usermodel.h @@ -8,12 +8,13 @@ #include #include -#include "activitylistmodel.h" #include "accountfwd.h" #include "accountmanager.h" +#include "activitydata.h" +#include "activitylistmodel.h" #include "folderman.h" -#include "userstatusselectormodel.h" #include "userstatusconnector.h" +#include "userstatusselectormodel.h" #include namespace OCC { @@ -118,6 +119,7 @@ public slots: void slotProgressInfo(const QString &folder, const OCC::ProgressInfo &progress); void slotAddError(const QString &folderAlias, const QString &message, OCC::ErrorCategory category); void slotAddErrorToGui(const QString &folderAlias, const OCC::SyncFileItem::Status status, const QString &errorMessage, const QString &subject, const OCC::ErrorCategory category); + void slotAddNotification(const OCC::Folder *folder, const OCC::Activity &activity); void slotNotificationRequestFinished(int statusCode); void slotNotifyNetworkError(QNetworkReply *reply); void slotEndNotificationRequest(int replyCode); @@ -210,8 +212,8 @@ class UserModel : public QAbstractListModel [[nodiscard]] QImage avatarById(const int id) const; [[nodiscard]] User *currentUser() const; - - int findUserIdForAccount(AccountState *account) const; + [[nodiscard]] User *findUserForAccount(AccountState *account) const; + [[nodiscard]] int findUserIdForAccount(AccountState *account) const; Q_INVOKABLE int numUsers(); Q_INVOKABLE QString currentUserServer(); diff --git a/src/gui/wizard/webviewpage.cpp b/src/gui/wizard/webviewpage.cpp index 0b5ccd9ca59f..8e492ea27605 100644 --- a/src/gui/wizard/webviewpage.cpp +++ b/src/gui/wizard/webviewpage.cpp @@ -6,10 +6,11 @@ #include #include -#include "owncloudwizard.h" +#include "account.h" +#include "common/utility.h" #include "creds/webflowcredentials.h" +#include "owncloudwizard.h" #include "webview.h" -#include "account.h" namespace OCC { @@ -46,11 +47,7 @@ void WebViewPage::initializePage() { if (_ocWizard->registration()) { url = "https://nextcloud.com/register"; } else { - url = _ocWizard->ocUrl(); - if (!url.endsWith('/')) { - url += "/"; - } - url += "index.php/login/flow"; + url = Utility::trailingSlashPath(_ocWizard->ocUrl()) + "index.php/login/flow"; } qCInfo(lcWizardWebiewPage()) << "Url to auth at: " << url; _webView->setUrl(QUrl(url)); diff --git a/src/libsync/configfile.cpp b/src/libsync/configfile.cpp index fd8d938d643f..5931929d559d 100644 --- a/src/libsync/configfile.cpp +++ b/src/libsync/configfile.cpp @@ -99,6 +99,8 @@ static constexpr char downloadLimitC[] = "BWLimit/downloadLimit"; static constexpr char newBigFolderSizeLimitC[] = "newBigFolderSizeLimit"; static constexpr char useNewBigFolderSizeLimitC[] = "useNewBigFolderSizeLimit"; +static constexpr char notifyExistingFoldersOverLimitC[] = "notifyExistingFoldersOverLimit"; +static constexpr char stopSyncingExistingFoldersOverLimitC[] = "stopSyncingExistingFoldersOverLimit"; static constexpr char confirmExternalStorageC[] = "confirmExternalStorage"; static constexpr char moveToTrashC[] = "moveToTrash"; @@ -362,11 +364,8 @@ QString ConfigFile::configPath() const _confDir = newLocation; } } - QString dir = _confDir; - if (!dir.endsWith(QLatin1Char('/'))) - dir.append(QLatin1Char('/')); - return dir; + return Utility::trailingSlashPath(_confDir); } static const QLatin1String exclFile("sync-exclude.lst"); @@ -959,6 +958,29 @@ bool ConfigFile::useNewBigFolderSizeLimit() const return getPolicySetting(QLatin1String(useNewBigFolderSizeLimitC), fallback).toBool(); } +bool ConfigFile::notifyExistingFoldersOverLimit() const +{ + const auto fallback = getValue(notifyExistingFoldersOverLimitC, {}, false); + return getPolicySetting(QString(notifyExistingFoldersOverLimitC), fallback).toBool(); +} + +void ConfigFile::setNotifyExistingFoldersOverLimit(const bool notify) +{ + setValue(notifyExistingFoldersOverLimitC, notify); +} + +bool ConfigFile::stopSyncingExistingFoldersOverLimit() const +{ + const auto notifyExistingBigEnabled = notifyExistingFoldersOverLimit(); + const auto fallback = getValue(stopSyncingExistingFoldersOverLimitC, {}, notifyExistingBigEnabled); + return getPolicySetting(QString(stopSyncingExistingFoldersOverLimitC), fallback).toBool(); +} + +void ConfigFile::setStopSyncingExistingFoldersOverLimit(const bool stopSyncing) +{ + setValue(stopSyncingExistingFoldersOverLimitC, stopSyncing); +} + void ConfigFile::setConfirmExternalStorage(bool isChecked) { setValue(confirmExternalStorageC, isChecked); diff --git a/src/libsync/configfile.h b/src/libsync/configfile.h index 5d0ee5460fce..6906c21b7600 100644 --- a/src/libsync/configfile.h +++ b/src/libsync/configfile.h @@ -141,6 +141,10 @@ class OWNCLOUDSYNC_EXPORT ConfigFile /** [checked, size in MB] **/ [[nodiscard]] QPair newBigFolderSizeLimit() const; void setNewBigFolderSizeLimit(bool isChecked, qint64 mbytes); + [[nodiscard]] bool notifyExistingFoldersOverLimit() const; + void setNotifyExistingFoldersOverLimit(const bool notify); + [[nodiscard]] bool stopSyncingExistingFoldersOverLimit() const; + void setStopSyncingExistingFoldersOverLimit(const bool stopSyncing); [[nodiscard]] bool useNewBigFolderSizeLimit() const; [[nodiscard]] bool confirmExternalStorage() const; void setConfirmExternalStorage(bool); diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index dcabd45e11c7..8d24b56f633a 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -437,7 +437,7 @@ void ProcessDirectoryJob::checkAndUpdateSelectiveSyncListsForE2eeFolders(const Q { bool ok = false; - const auto pathWithTrailingSpace = path.endsWith(QLatin1Char('/')) ? path : path + QLatin1Char('/'); + const auto pathWithTrailingSpace = Utility::trailingSlashPath(path); auto blackListSet = _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok).toSet(); blackListSet.insert(pathWithTrailingSpace); @@ -457,8 +457,8 @@ void ProcessDirectoryJob::processFile(PathTuple path, const LocalInfo &localEntry, const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry) { - const char *hasServer = serverEntry.isValid() ? "true" : _queryServer == ParentNotChanged ? "db" : "false"; - const char *hasLocal = localEntry.isValid() ? "true" : _queryLocal == ParentNotChanged ? "db" : "false"; + const auto hasServer = serverEntry.isValid() ? "true" : _queryServer == ParentNotChanged ? "db" : "false"; + const auto hasLocal = localEntry.isValid() ? "true" : _queryLocal == ParentNotChanged ? "db" : "false"; const auto serverFileIsLocked = (serverEntry.isValid() ? (serverEntry.locked == SyncFileItem::LockStatus::LockedItem ? "locked" : "not locked") : ""); const auto localFileIsLocked = dbEntry._lockstate._locked ? "locked" : "not locked"; qCInfo(lcDisco).nospace() << "Processing " << path._original @@ -488,7 +488,7 @@ void ProcessDirectoryJob::processFile(PathTuple path, return; // Ignore this. } - auto item = SyncFileItem::fromSyncJournalFileRecord(dbEntry); + const auto item = SyncFileItem::fromSyncJournalFileRecord(dbEntry); item->_file = path._target; item->_originalFile = path._original; item->_previousSize = dbEntry._fileSize; @@ -559,9 +559,58 @@ static bool computeLocalChecksum(const QByteArray &header, const QString &path, return false; } -void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( - const SyncFileItemPtr &item, PathTuple path, const LocalInfo &localEntry, - const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry) +void ProcessDirectoryJob::postProcessServerNew(const SyncFileItemPtr &item, + PathTuple &path, + const LocalInfo &localEntry, + const RemoteInfo &serverEntry, + const SyncJournalFileRecord &dbEntry) +{ + if (item->isDirectory()) { + _pendingAsyncJobs++; + _discoveryData->checkSelectiveSyncNewFolder(path._server, + serverEntry.remotePerm, + [=](bool result) { + --_pendingAsyncJobs; + if (!result) { + processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer); + } + QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs); + }); + return; + } + + // Turn new remote files into virtual files if the option is enabled. + const auto opts = _discoveryData->_syncOptions; + if (!localEntry.isValid() && + item->_type == ItemTypeFile && + opts._vfs->mode() != Vfs::Off && + !FileSystem::isLnkFile(item->_file) && + _pinState != PinState::AlwaysLocal && + !FileSystem::isExcludeFile(item->_file)) { + + item->_type = ItemTypeVirtualFile; + if (isVfsWithSuffix()) { + addVirtualFileSuffix(path._original); + } + } + + if (opts._vfs->mode() != Vfs::Off && !item->_encryptedFileName.isEmpty()) { + // We are syncing a file for the first time (local entry is invalid) and it is encrypted file that will be virtual once synced + // to avoid having error of "file has changed during sync" when trying to hydrate it explicitly - we must remove Constants::e2EeTagSize bytes from the end + // as explicit hydration does not care if these bytes are present in the placeholder or not, but, the size must not change in the middle of the sync + // this way it works for both implicit and explicit hydration by making a placeholder size that does not includes encryption tag Constants::e2EeTagSize bytes + // another scenario - we are syncing a file which is on disk but not in the database (database was removed or file was not written there yet) + item->_size = serverEntry.size - Constants::e2EeTagSize; + } + + processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer); +} + +void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &item, + PathTuple path, + const LocalInfo &localEntry, + const RemoteInfo &serverEntry, + const SyncJournalFileRecord &dbEntry) { item->_checksumHeader = serverEntry.checksumHeader; item->_fileId = serverEntry.fileId; @@ -593,7 +642,15 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_lockEditorApp = serverEntry.lockEditorApp; item->_lockTime = serverEntry.lockTime; item->_lockTimeout = serverEntry.lockTimeout; - qCDebug(lcDisco()) << item->_locked << item->_lockOwnerDisplayName << item->_lockOwnerId << item->_lockOwnerType << item->_lockEditorApp << item->_lockTime << item->_lockTimeout; + + qCDebug(lcDisco()) << "item lock for:" << item->_file + << item->_locked + << item->_lockOwnerDisplayName + << item->_lockOwnerId + << item->_lockOwnerType + << item->_lockEditorApp + << item->_lockTime + << item->_lockTimeout; // Check for missing server data { @@ -640,11 +697,16 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( // The file is known in the db already if (dbEntry.isValid()) { - const bool isDbEntryAnE2EePlaceholder = dbEntry.isVirtualFile() && !dbEntry.e2eMangledName().isEmpty(); + const auto isDbEntryAnE2EePlaceholder = dbEntry.isVirtualFile() && !dbEntry.e2eMangledName().isEmpty(); Q_ASSERT(!isDbEntryAnE2EePlaceholder || serverEntry.size >= Constants::e2EeTagSize); - const bool isVirtualE2EePlaceholder = isDbEntryAnE2EePlaceholder && serverEntry.size >= Constants::e2EeTagSize; - const qint64 sizeOnServer = isVirtualE2EePlaceholder ? serverEntry.size - Constants::e2EeTagSize : serverEntry.size; - const bool metaDataSizeNeedsUpdateForE2EeFilePlaceholder = isVirtualE2EePlaceholder && dbEntry._fileSize == serverEntry.size; + const auto isVirtualE2EePlaceholder = isDbEntryAnE2EePlaceholder && serverEntry.size >= Constants::e2EeTagSize; + const auto sizeOnServer = isVirtualE2EePlaceholder ? serverEntry.size - Constants::e2EeTagSize : serverEntry.size; + const auto metaDataSizeNeedsUpdateForE2EeFilePlaceholder = isVirtualE2EePlaceholder && dbEntry._fileSize == serverEntry.size; + + if (serverEntry.isDirectory) { + // Even if over quota, continue syncing as normal for now + _discoveryData->checkSelectiveSyncExistingFolder(path._server); + } if (serverEntry.isDirectory != dbEntry.isDirectory()) { // If the type of the entity changed, it's like NEW, but @@ -664,6 +726,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_direction = SyncFileItem::Down; item->_modtime = serverEntry.modtime; item->_size = sizeOnServer; + if (serverEntry.isDirectory) { ENFORCE(dbEntry.isDirectory()); item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA; @@ -678,7 +741,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( << "serverEntry.isDirectory:" << serverEntry.isDirectory << "dbEntry.isDirectory:" << dbEntry.isDirectory(); } - } else if (dbEntry._modtime != serverEntry.modtime && localEntry.size == serverEntry.size && dbEntry._fileSize == serverEntry.size && dbEntry._etag == serverEntry.etag) { + } else if (dbEntry._modtime != serverEntry.modtime && localEntry.size == serverEntry.size && dbEntry._fileSize == serverEntry.size + && dbEntry._etag == serverEntry.etag) { item->_direction = SyncFileItem::Down; item->_modtime = serverEntry.modtime; item->_size = sizeOnServer; @@ -696,7 +760,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( // to update a placeholder with corrected size (-16 Bytes) // or, maybe, add a flag to the database - vfsE2eeSizeCorrected? if it is not set - subtract it from the placeholder's size and re-create/update a placeholder? const QueryMode serverQueryMode = [this, &dbEntry, &serverEntry]() { - const bool isVfsModeOn = _discoveryData && _discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() != Vfs::Off; + const auto isVfsModeOn = _discoveryData && _discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() != Vfs::Off; if (isVfsModeOn && dbEntry.isDirectory() && dbEntry.isE2eEncrypted()) { qint64 localFolderSize = 0; const auto listFilesCallback = [&localFolderSize](const OCC::SyncJournalFileRecord &record) { @@ -709,7 +773,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( } }; - const bool listFilesSucceeded = _discoveryData->_statedb->listFilesInPath(dbEntry.path().toUtf8(), listFilesCallback); + const auto listFilesSucceeded = _discoveryData->_statedb->listFilesInPath(dbEntry.path().toUtf8(), listFilesCallback); if (listFilesSucceeded && localFolderSize != 0 && localFolderSize == serverEntry.sizeOfFolder) { qCInfo(lcDisco) << "Migration of E2EE folder " << dbEntry.path() << " from older version to the one, supporting the implicit VFS hydration."; @@ -735,7 +799,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_modtime = serverEntry.modtime; item->_size = serverEntry.size; - auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByBasePath(item->_file); + const auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByBasePath(item->_file); if (conflictRecord.isValid() && QString::fromUtf8(conflictRecord.path).contains(QStringLiteral("(case clash from"))) { qCInfo(lcDisco) << "should ignore" << item->_file << "has already a case clash conflict record" << conflictRecord.path; @@ -744,46 +808,9 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( return; } - auto postProcessServerNew = [=]() mutable { - if (item->isDirectory()) { - _pendingAsyncJobs++; - _discoveryData->checkSelectiveSyncNewFolder(path._server, serverEntry.remotePerm, - [=](bool result) { - --_pendingAsyncJobs; - if (!result) { - processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer); - } - QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs); - }); - return; - } - // Turn new remote files into virtual files if the option is enabled. - auto &opts = _discoveryData->_syncOptions; - if (!localEntry.isValid() - && item->_type == ItemTypeFile - && opts._vfs->mode() != Vfs::Off - && !FileSystem::isLnkFile(item->_file) - && _pinState != PinState::AlwaysLocal - && !FileSystem::isExcludeFile(item->_file)) { - item->_type = ItemTypeVirtualFile; - if (isVfsWithSuffix()) - addVirtualFileSuffix(path._original); - } - - if (opts._vfs->mode() != Vfs::Off && !item->_encryptedFileName.isEmpty()) { - // We are syncing a file for the first time (local entry is invalid) and it is encrypted file that will be virtual once synced - // to avoid having error of "file has changed during sync" when trying to hydrate it explicitly - we must remove Constants::e2EeTagSize bytes from the end - // as explicit hydration does not care if these bytes are present in the placeholder or not, but, the size must not change in the middle of the sync - // this way it works for both implicit and explicit hydration by making a placeholder size that does not includes encryption tag Constants::e2EeTagSize bytes - // another scenario - we are syncing a file which is on disk but not in the database (database was removed or file was not written there yet) - item->_size = serverEntry.size - Constants::e2EeTagSize; - } - processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer); - }; - // Potential NEW/NEW conflict is handled in AnalyzeLocal if (localEntry.isValid()) { - postProcessServerNew(); + postProcessServerNew(item, path, localEntry, serverEntry, dbEntry); return; } @@ -831,7 +858,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( } // Now we know there is a sane rename candidate. - QString originalPath = base.path(); + const auto originalPath = base.path(); if (_discoveryData->isRenamed(originalPath)) { qCInfo(lcDisco, "folder already has a rename entry, skipping"); @@ -847,7 +874,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( return; } - QString originalPathAdjusted = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up); + const auto originalPathAdjusted = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up); if (!base.isDirectory()) { csync_file_stat_t buf; @@ -873,10 +900,10 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_type = ItemTypeVirtualFile; } - bool wasDeletedOnServer = _discoveryData->findAndCancelDeletedJob(originalPath).first; + const auto wasDeletedOnServer = _discoveryData->findAndCancelDeletedJob(originalPath).first; auto postProcessRename = [this, item, base, originalPath](PathTuple &path) { - auto adjustedOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up); + const auto adjustedOriginalPath = _discoveryData->adjustRenamedPath(originalPath, SyncFileItem::Up); _discoveryData->_renamedItemsRemote.insert(originalPath, path._target); item->_modtime = base._modtime; item->_inode = base._inode; @@ -896,7 +923,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( } else { // we need to make a request to the server to know that the original file is deleted on the server _pendingAsyncJobs++; - auto job = new RequestEtagJob(_discoveryData->_account, _discoveryData->_remoteFolder + originalPath, this); + const auto job = new RequestEtagJob(_discoveryData->_account, _discoveryData->_remoteFolder + originalPath, this); connect(job, &RequestEtagJob::finishedWithResult, this, [=](const HttpResult &etag) mutable { _pendingAsyncJobs--; QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs); @@ -904,7 +931,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( // Somehow another item claimed this original path, consider as if it existed _discoveryData->isRenamed(originalPath)) { // If the file exist or if there is another error, consider it is a new file. - postProcessServerNew(); + postProcessServerNew(item, path, localEntry, serverEntry, dbEntry); return; } @@ -930,7 +957,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( } if (item->_instruction == CSYNC_INSTRUCTION_NEW) { - postProcessServerNew(); + postProcessServerNew(item, path, localEntry, serverEntry, dbEntry); return; } processFileAnalyzeLocalInfo(item, path, localEntry, serverEntry, dbEntry, _queryServer); diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h index debb4572de40..eaa2657697a9 100644 --- a/src/libsync/discovery.h +++ b/src/libsync/discovery.h @@ -162,6 +162,12 @@ class ProcessDirectoryJob : public QObject */ void processFile(PathTuple, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &); + void postProcessServerNew(const SyncFileItemPtr &item, + PathTuple &path, + const LocalInfo &localEntry, + const RemoteInfo &serverEntry, + const SyncJournalFileRecord &dbEntry); + /// processFile helper for when remote information is available, typically flows into AnalyzeLocalInfo when done void processFileAnalyzeRemoteInfo(const SyncFileItemPtr &item, PathTuple, const LocalInfo &, const RemoteInfo &, const SyncJournalFileRecord &); diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 3142a2968a74..27936ad866a2 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -13,6 +13,8 @@ */ #include "discoveryphase.h" +#include "common/utility.h" +#include "configfile.h" #include "discovery.h" #include "helpers.h" #include "progressdispatcher.h" @@ -39,51 +41,58 @@ namespace OCC { Q_LOGGING_CATEGORY(lcDiscovery, "nextcloud.sync.discovery", QtInfoMsg) -/* Given a sorted list of paths ending with '/', return whether or not the given path is within one of the paths of the list*/ -static bool findPathInList(const QStringList &list, const QString &path) +bool DiscoveryPhase::isInSelectiveSyncBlackList(const QString &path) const { - Q_ASSERT(std::is_sorted(list.begin(), list.end())); + if (_selectiveSyncBlackList.isEmpty()) { + // If there is no black list, everything is allowed + return false; + } - if (list.size() == 1 && list.first() == QLatin1String("/")) { - // Special case for the case "/" is there, it matches everything + // Block if it is in the black list + if (SyncJournalDb::findPathInSelectiveSyncList(_selectiveSyncBlackList, path)) { return true; } - QString pathSlash = path + QLatin1Char('/'); - - // Since the list is sorted, we can do a binary search. - // If the path is a prefix of another item or right after in the lexical order. - auto it = std::lower_bound(list.begin(), list.end(), pathSlash); + return false; +} - if (it != list.end() && *it == pathSlash) { - return true; - } +bool DiscoveryPhase::activeFolderSizeLimit() const +{ + return _syncOptions._newBigFolderSizeLimit > 0 && _syncOptions._vfs->mode() == Vfs::Off; +} - if (it == list.begin()) { - return false; - } - --it; - Q_ASSERT(it->endsWith(QLatin1Char('/'))); // Folder::setSelectiveSyncBlackList makes sure of that - return pathSlash.startsWith(*it); +bool DiscoveryPhase::notifyExistingFolderOverLimit() const +{ + return activeFolderSizeLimit() && ConfigFile().notifyExistingFoldersOverLimit(); } -bool DiscoveryPhase::isInSelectiveSyncBlackList(const QString &path) const +void DiscoveryPhase::checkFolderSizeLimit(const QString &path, const std::function completionCallback) { - if (_selectiveSyncBlackList.isEmpty()) { - // If there is no black list, everything is allowed - return false; + if (!activeFolderSizeLimit()) { + // no limit, everything is allowed; + return completionCallback(false); } - // Block if it is in the black list - if (findPathInList(_selectiveSyncBlackList, path)) { - return true; - } + // do a PROPFIND to know the size of this folder + const auto propfindJob = new PropfindJob(_account, _remoteFolder + path, this); + propfindJob->setProperties(QList() << "resourcetype" + << "http://owncloud.org/ns:size"); - return false; + connect(propfindJob, &PropfindJob::finishedWithError, this, [=] { + return completionCallback(false); + }); + connect(propfindJob, &PropfindJob::result, this, [=](const QVariantMap &values) { + const auto result = values.value(QLatin1String("size")).toLongLong(); + const auto limit = _syncOptions._newBigFolderSizeLimit; + qCDebug(lcDiscovery) << "Folder size check complete for" << path << "result:" << result << "limit:" << limit; + return completionCallback(result >= limit); + }); + propfindJob->start(); } -void DiscoveryPhase::checkSelectiveSyncNewFolder(const QString &path, RemotePermissions remotePerm, - std::function callback) +void DiscoveryPhase::checkSelectiveSyncNewFolder(const QString &path, + const RemotePermissions remotePerm, + const std::function callback) { if (_syncOptions._confirmExternalStorage && _syncOptions._vfs->mode() == Vfs::Off && remotePerm.hasPermission(RemotePermissions::IsMounted)) { @@ -103,41 +112,38 @@ void DiscoveryPhase::checkSelectiveSyncNewFolder(const QString &path, RemotePerm } // If this path or the parent is in the white list, then we do not block this file - if (findPathInList(_selectiveSyncWhiteList, path)) { + if (SyncJournalDb::findPathInSelectiveSyncList(_selectiveSyncWhiteList, path)) { return callback(false); } - auto limit = _syncOptions._newBigFolderSizeLimit; - if (limit < 0 || _syncOptions._vfs->mode() != Vfs::Off) { - // no limit, everything is allowed; - return callback(false); - } - - // do a PROPFIND to know the size of this folder - auto propfindJob = new PropfindJob(_account, _remoteFolder + path, this); - propfindJob->setProperties(QList() << "resourcetype" - << "http://owncloud.org/ns:size"); - QObject::connect(propfindJob, &PropfindJob::finishedWithError, - this, [=] { return callback(false); }); - QObject::connect(propfindJob, &PropfindJob::result, this, [=](const QVariantMap &values) { - auto result = values.value(QLatin1String("size")).toLongLong(); - if (result >= limit) { + checkFolderSizeLimit(path, [this, path, callback](const bool bigFolder) { + if (bigFolder) { // we tell the UI there is a new folder emit newBigFolder(path, false); return callback(true); - } else { - // it is not too big, put it in the white list (so we will not do more query for the children) - // and and do not block. - auto p = path; - if (!p.endsWith(QLatin1Char('/'))) - p += QLatin1Char('/'); - _selectiveSyncWhiteList.insert( - std::upper_bound(_selectiveSyncWhiteList.begin(), _selectiveSyncWhiteList.end(), p), - p); - return callback(false); + } + + // it is not too big, put it in the white list (so we will not do more query for the children) and and do not block. + const auto sanitisedPath = Utility::trailingSlashPath(path); + _selectiveSyncWhiteList.insert(std::upper_bound(_selectiveSyncWhiteList.begin(), _selectiveSyncWhiteList.end(), sanitisedPath), sanitisedPath); + return callback(false); + }); +} + +void DiscoveryPhase::checkSelectiveSyncExistingFolder(const QString &path) +{ + // If no size limit is enforced, or if is in whitelist (explicitly allowed) or in blacklist (explicitly disallowed), do nothing. + if (!notifyExistingFolderOverLimit() || SyncJournalDb::findPathInSelectiveSyncList(_selectiveSyncWhiteList, path) + || SyncJournalDb::findPathInSelectiveSyncList(_selectiveSyncBlackList, path)) { + return; + } + + checkFolderSizeLimit(path, [this, path](const bool bigFolder) { + if (bigFolder) { + // Notify the user and prompt for response. + emit existingFolderNowBig(path); } }); - propfindJob->start(); } /* Given a path on the remote, give the path as it is when the rename is done */ @@ -244,13 +250,13 @@ void DiscoveryPhase::startJob(ProcessDirectoryJob *job) void DiscoveryPhase::setSelectiveSyncBlackList(const QStringList &list) { _selectiveSyncBlackList = list; - std::sort(_selectiveSyncBlackList.begin(), _selectiveSyncBlackList.end()); + _selectiveSyncBlackList.sort(); } void DiscoveryPhase::setSelectiveSyncWhiteList(const QStringList &list) { _selectiveSyncWhiteList = list; - std::sort(_selectiveSyncWhiteList.begin(), _selectiveSyncWhiteList.end()); + _selectiveSyncWhiteList.sort(); } void DiscoveryPhase::scheduleMoreJobs() diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index 36b07b874daf..900584066cd0 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -255,10 +255,19 @@ class DiscoveryPhase : public QObject [[nodiscard]] bool isInSelectiveSyncBlackList(const QString &path) const; + [[nodiscard]] bool activeFolderSizeLimit() const; + [[nodiscard]] bool notifyExistingFolderOverLimit() const; + + void checkFolderSizeLimit(const QString &path, + const std::function callback); + // Check if the new folder should be deselected or not. // May be async. "Return" via the callback, true if the item is blacklisted - void checkSelectiveSyncNewFolder(const QString &path, RemotePermissions rp, - std::function callback); + void checkSelectiveSyncNewFolder(const QString &path, + const RemotePermissions rp, + const std::function callback); + + void checkSelectiveSyncExistingFolder(const QString &path); /** Given an original path, return the target path obtained when renaming is done. * @@ -316,6 +325,7 @@ class DiscoveryPhase : public QObject // A new folder was discovered and was not synced because of the confirmation feature void newBigFolder(const QString &folder, bool isExternal); + void existingFolderNowBig(const QString &folder); /** For excluded items that don't show up in itemDiscovered() * diff --git a/src/libsync/logger.cpp b/src/libsync/logger.cpp index fffbbc14c636..8cd029cf9b1e 100644 --- a/src/libsync/logger.cpp +++ b/src/libsync/logger.cpp @@ -100,11 +100,6 @@ void Logger::postGuiLog(const QString &title, const QString &message) emit guiLog(title, message); } -void Logger::postOptionalGuiLog(const QString &title, const QString &message) -{ - emit optionalGuiLog(title, message); -} - void Logger::postGuiMessage(const QString &title, const QString &message) { emit guiMessage(title, message); diff --git a/src/libsync/logger.h b/src/libsync/logger.h index 5fd14431e5ee..bfcb2989d5db 100644 --- a/src/libsync/logger.h +++ b/src/libsync/logger.h @@ -42,7 +42,6 @@ class OWNCLOUDSYNC_EXPORT Logger : public QObject static Logger *instance(); void postGuiLog(const QString &title, const QString &message); - void postOptionalGuiLog(const QString &title, const QString &message); void postGuiMessage(const QString &title, const QString &message); QString logFile() const; @@ -87,7 +86,6 @@ class OWNCLOUDSYNC_EXPORT Logger : public QObject void guiLog(const QString &, const QString &); void guiMessage(const QString &, const QString &); - void optionalGuiLog(const QString &, const QString &); public slots: void enterNextLogFile(); diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index f38291b1ed46..e837641a49bd 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -25,13 +25,14 @@ #include #include +#include "accountfwd.h" +#include "bandwidthmanager.h" +#include "common/syncjournaldb.h" +#include "common/utility.h" #include "csync.h" +#include "progressdispatcher.h" #include "syncfileitem.h" -#include "common/syncjournaldb.h" -#include "bandwidthmanager.h" -#include "accountfwd.h" #include "syncoptions.h" -#include "progressdispatcher.h" #include @@ -416,15 +417,13 @@ class OWNCLOUDSYNC_EXPORT OwncloudPropagator : public QObject bool _finishedEmited = false; // used to ensure that finished is only emitted once public: - OwncloudPropagator(AccountPtr account, const QString &localDir, - const QString &remoteFolder, SyncJournalDb *progressDb, - QSet &bulkUploadBlackList) + OwncloudPropagator(AccountPtr account, const QString &localDir, const QString &remoteFolder, SyncJournalDb *progressDb, QSet &bulkUploadBlackList) : _journal(progressDb) , _bandwidthManager(this) , _chunkSize(10 * 1000 * 1000) // 10 MB, overridden in setSyncOptions , _account(account) - , _localDir((localDir.endsWith(QChar('/'))) ? localDir : localDir + '/') - , _remoteFolder((remoteFolder.endsWith(QChar('/'))) ? remoteFolder : remoteFolder + '/') + , _localDir(Utility::trailingSlashPath(localDir)) + , _remoteFolder(Utility::trailingSlashPath(remoteFolder)) , _bulkUploadBlackList(bulkUploadBlackList) { qRegisterMetaType("PropagatorJob::AbortType"); diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 7a13f7133f30..52985fd4454a 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -617,12 +617,8 @@ void SyncEngine::startSync() _discoveryPhase->_excludes->reloadExcludeFiles(); } _discoveryPhase->_statedb = _journal; - _discoveryPhase->_localDir = _localPath; - if (!_discoveryPhase->_localDir.endsWith('/')) - _discoveryPhase->_localDir+='/'; - _discoveryPhase->_remoteFolder = _remotePath; - if (!_discoveryPhase->_remoteFolder.endsWith('/')) - _discoveryPhase->_remoteFolder+='/'; + _discoveryPhase->_localDir = Utility::trailingSlashPath(_localPath); + _discoveryPhase->_remoteFolder = Utility::trailingSlashPath(_remotePath); _discoveryPhase->_syncOptions = _syncOptions; _discoveryPhase->_shouldDiscoverLocaly = [this](const QString &path) { const auto result = shouldDiscoverLocally(path); @@ -656,6 +652,7 @@ void SyncEngine::startSync() connect(_discoveryPhase.data(), &DiscoveryPhase::itemDiscovered, this, &SyncEngine::slotItemDiscovered); connect(_discoveryPhase.data(), &DiscoveryPhase::newBigFolder, this, &SyncEngine::newBigFolder); + connect(_discoveryPhase.data(), &DiscoveryPhase::existingFolderNowBig, this, &SyncEngine::existingFolderNowBig); connect(_discoveryPhase.data(), &DiscoveryPhase::fatalError, this, [this](const QString &errorString, ErrorCategory errorCategory) { Q_EMIT syncError(errorString, errorCategory); finalize(false); diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h index fce3a96b6945..d1c4ab9d5169 100644 --- a/src/libsync/syncengine.h +++ b/src/libsync/syncengine.h @@ -187,6 +187,8 @@ public slots: // A new folder was discovered and was not synced because of the confirmation feature void newBigFolder(const QString &folder, bool isExternal); + void existingFolderNowBig(const QString &folder); + /** Emitted when propagation has problems with a locked file. * * Forwarded from OwncloudPropagator::seenLockedFile. diff --git a/src/libsync/theme.cpp b/src/libsync/theme.cpp index a8bcd127365b..fb589c29d601 100644 --- a/src/libsync/theme.cpp +++ b/src/libsync/theme.cpp @@ -408,12 +408,12 @@ QString Theme::helpUrl() const QString Theme::conflictHelpUrl() const { - auto baseUrl = helpUrl(); - if (baseUrl.isEmpty()) + const auto baseUrl = helpUrl(); + if (baseUrl.isEmpty()) { return QString(); - if (!baseUrl.endsWith('/')) - baseUrl.append('/'); - return baseUrl + QStringLiteral("conflicts.html"); + } + + return Utility::trailingSlashPath(baseUrl) + QStringLiteral("conflicts.html"); } QString Theme::overrideServerUrl() const diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp index 15c7f5a48924..82aa701664d1 100644 --- a/test/syncenginetestutils.cpp +++ b/test/syncenginetestutils.cpp @@ -6,9 +6,10 @@ */ #include "syncenginetestutils.h" -#include "httplogger.h" #include "accessmanager.h" +#include "common/utility.h" #include "gui/sharepermissions.h" +#include "httplogger.h" #include #include @@ -285,11 +286,7 @@ QString FileInfo::path() const QString FileInfo::absolutePath() const { - if (parentPath.endsWith(QLatin1Char('/'))) { - return parentPath + name; - } else { - return parentPath + QLatin1Char('/') + name; - } + return OCC::Utility::trailingSlashPath(parentPath) + name; } void FileInfo::fixupParentPathRecursively() @@ -339,10 +336,7 @@ FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAcces auto writeFileResponse = [&](const FileInfo &fileInfo) { xml.writeStartElement(davUri, QStringLiteral("response")); - auto url = QString::fromUtf8(QUrl::toPercentEncoding(fileInfo.absolutePath(), "/")); - if (!url.endsWith(QChar('/'))) { - url.append(QChar('/')); - } + const auto url = OCC::Utility::trailingSlashPath(QString::fromUtf8(QUrl::toPercentEncoding(fileInfo.absolutePath(), "/"))); const auto href = OCC::Utility::concatUrlPath(prefix, url).path(); xml.writeTextElement(davUri, QStringLiteral("href"), href); xml.writeStartElement(davUri, QStringLiteral("propstat")); @@ -352,6 +346,12 @@ FakePropfindReply::FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAcces xml.writeStartElement(davUri, QStringLiteral("resourcetype")); xml.writeEmptyElement(davUri, QStringLiteral("collection")); xml.writeEndElement(); // resourcetype + + auto totalSize = 0; + for (const auto &child : fileInfo.children.values()) { + totalSize += child.size; + } + xml.writeTextElement(ocUri, QStringLiteral("size"), QString::number(totalSize)); } else xml.writeEmptyElement(davUri, QStringLiteral("resourcetype")); @@ -1178,9 +1178,7 @@ FileInfo FakeFolder::currentLocalState() QString FakeFolder::localPath() const { // SyncEngine wants a trailing slash - if (_tempDir.path().endsWith(QLatin1Char('/'))) - return _tempDir.path(); - return _tempDir.path() + QLatin1Char('/'); + return OCC::Utility::trailingSlashPath(_tempDir.path()); } void FakeFolder::scheduleSync() diff --git a/test/testactivitylistmodel.cpp b/test/testactivitylistmodel.cpp index 7e2a734e7532..f5a69b3ea8c5 100644 --- a/test/testactivitylistmodel.cpp +++ b/test/testactivitylistmodel.cpp @@ -268,8 +268,7 @@ private slots: const auto actionsLinks = index.data(OCC::ActivityListModel::ActionsLinksRole).toList(); if (!actionsLinks.isEmpty()) { - const auto actionsLinksContextMenu = - index.data(OCC::ActivityListModel::ActionsLinksContextMenuRole).toList(); + const auto actionsLinksContextMenu = index.data(OCC::ActivityListModel::ActionsLinksContextMenuRole).toList(); // context menu must be shorter than total action links QVERIFY(actionsLinksContextMenu.isEmpty() || actionsLinksContextMenu.size() < actionsLinks.size()); @@ -281,8 +280,7 @@ private slots: const auto objectType = index.data(OCC::ActivityListModel::ObjectTypeRole).toString(); - const auto actionButtonsLinks = - index.data(OCC::ActivityListModel::ActionsLinksForActionButtonsRole).toList(); + const auto actionButtonsLinks = index.data(OCC::ActivityListModel::ActionsLinksForActionButtonsRole).toList(); // Login attempt notification if (objectType == QStringLiteral("2fa_id")) { @@ -323,15 +321,12 @@ private slots: QVERIFY(actionButtonsLinks[0].value()._label == QObject::tr("Reply")); if (static_cast(actionsLinks.size()) > OCC::ActivityListModel::maxActionButtons()) { - // in case total actions is longer than ActivityListModel::maxActionButtons, only one button must be present in a list of action buttons - QVERIFY(actionButtonsLinks.size() == 1); - const auto actionButtonsAndContextMenuEntries = actionButtonsLinks + actionsLinksContextMenu; + QCOMPARE(actionButtonsLinks.size(), OCC::ActivityListModel::maxActionButtons()); // in case total actions is longer than ActivityListModel::maxActionButtons, then a sum of action buttons and action menu entries must be equal to a total of action links - QVERIFY(actionButtonsLinks.size() + actionsLinksContextMenu.size() == actionsLinks.size()); + QCOMPARE(actionButtonsLinks.size() + actionsLinksContextMenu.size(), actionsLinks.size()); } } else if ((objectType == QStringLiteral("call"))) { - QVERIFY( - actionButtonsLinks[0].value()._label == QStringLiteral("Call back")); + QVERIFY(actionButtonsLinks[0].value()._label == QStringLiteral("Call back")); } } } diff --git a/test/testsyncengine.cpp b/test/testsyncengine.cpp index ec5aebeb2489..9516a05570dc 100644 --- a/test/testsyncengine.cpp +++ b/test/testsyncengine.cpp @@ -7,9 +7,10 @@ #include "syncenginetestutils.h" -#include "syncengine.h" -#include "propagatorjobs.h" #include "caseclashconflictsolver.h" +#include "configfile.h" +#include "propagatorjobs.h" +#include "syncengine.h" #include @@ -1663,6 +1664,35 @@ private slots: fakeFolder.remoteModifier().remove(testUpperCaseFile); QVERIFY(fakeFolder.syncOnce()); } + + void testExistingFolderBecameBig() + { + constexpr auto testFolder = "folder"; + constexpr auto testSmallFile = "folder/small_file.txt"; + constexpr auto testLargeFile = "folder/large_file.txt"; + + QTemporaryDir dir; + ConfigFile::setConfDir(dir.path()); // we don't want to pollute the user's config file + auto config = ConfigFile(); + config.setNotifyExistingFoldersOverLimit(true); + + FakeFolder fakeFolder{FileInfo{}}; + QSignalSpy spy(&fakeFolder.syncEngine(), &SyncEngine::existingFolderNowBig); + + auto syncOptions = fakeFolder.syncEngine().syncOptions(); + syncOptions._newBigFolderSizeLimit = 128; // 128 bytes + fakeFolder.syncEngine().setSyncOptions(syncOptions); + + fakeFolder.remoteModifier().mkdir(testFolder); + fakeFolder.remoteModifier().insert(testSmallFile, 64); + fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(spy.count(), 0); + + fakeFolder.remoteModifier().insert(testLargeFile, 256); + QVERIFY(fakeFolder.syncOnce()); + QCOMPARE(spy.count(), 1); + } }; QTEST_GUILESS_MAIN(TestSyncEngine)