diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 92b05d269b..29a9f46bcc 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -682,6 +682,40 @@ void Folder::slotFilesLockImposed(const QSet &files) } } +void Folder::slotLockedFilesFound(const QSet &files) +{ + qCDebug(lcFolder) << "Found new lock files" << files; + + for (const auto &file : files) { + const auto fileRecordPath = fileFromLocalPath(file); + SyncJournalFileRecord rec; + + const auto canLockFile = journalDb()->getFileRecord(fileRecordPath, &rec) && rec.isValid() + && (!rec._lockstate._locked + || (rec._lockstate._lockOwnerType == static_cast(SyncFileItem::LockOwnerType::UserLock) + && rec._lockstate._lockOwnerId == _accountState->account()->davUser())); + + if (!canLockFile) { + qCDebug(lcFolder) << "Skipping locking file" << file << "with rec.isValid():" << rec.isValid() + << "and rec._lockstate._lockOwnerId:" << rec._lockstate._lockOwnerId << "and davUser:" << _accountState->account()->davUser(); + continue; + } + + const QString remoteFilePath = remotePathTrailingSlash() + rec.path(); + qCDebug(lcFolder) << "Automatically locking file on server" << remoteFilePath; + _fileLockSuccess = connect(_accountState->account().data(), &Account::lockFileSuccess, this, [this, remoteFilePath] { + disconnect(_fileLockSuccess); + qCDebug(lcFolder) << "Locking file succeeded" << remoteFilePath; + startSync(); + }); + _fileLockFailure = connect(_accountState->account().data(), &Account::lockFileError, this, [this, remoteFilePath](const QString &message) { + disconnect(_fileLockFailure); + qCWarning(lcFolder) << "Failed to lock a file:" << remoteFilePath << message; + }); + _accountState->account()->setLockFileState(remoteFilePath, journalDb(), SyncFileItem::LockStatus::LockedItem); + } +} + void Folder::implicitlyHydrateFile(const QString &relativepath) { qCInfo(lcFolder) << "Implicitly hydrate virtual file:" << relativepath; @@ -1475,6 +1509,7 @@ void Folder::slotCapabilitiesChanged() { if (_accountState->account()->capabilities().filesLockAvailable()) { connect(_folderWatcher.data(), &FolderWatcher::filesLockReleased, this, &Folder::slotFilesLockReleased, Qt::UniqueConnection); + connect(_folderWatcher.data(), &FolderWatcher::lockedFilesFound, this, &Folder::slotLockedFilesFound, Qt::UniqueConnection); } } @@ -1520,6 +1555,7 @@ void Folder::registerFolderWatcher() this, &Folder::slotWatcherUnreliable); if (_accountState->account()->capabilities().filesLockAvailable()) { connect(_folderWatcher.data(), &FolderWatcher::filesLockReleased, this, &Folder::slotFilesLockReleased); + connect(_folderWatcher.data(), &FolderWatcher::lockedFilesFound, this, &Folder::slotLockedFilesFound); } connect(_folderWatcher.data(), &FolderWatcher::filesLockImposed, this, &Folder::slotFilesLockImposed, Qt::UniqueConnection); _folderWatcher->init(path()); diff --git a/src/gui/folder.h b/src/gui/folder.h index edc8484b4c..f0622f48c1 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -359,6 +359,8 @@ public slots: */ void slotFilesLockImposed(const QSet &files); + void slotLockedFilesFound(const QSet &files); + /** * Mark a virtual file as being requested for download, and start a sync. * @@ -562,6 +564,8 @@ private: QMetaObject::Connection _officeFileLockReleaseUnlockSuccess; QMetaObject::Connection _officeFileLockReleaseUnlockFailure; + QMetaObject::Connection _fileLockSuccess; + QMetaObject::Connection _fileLockFailure; }; } diff --git a/src/gui/folderwatcher.cpp b/src/gui/folderwatcher.cpp index 44b56d391c..0dd183f00c 100644 --- a/src/gui/folderwatcher.cpp +++ b/src/gui/folderwatcher.cpp @@ -19,15 +19,6 @@ #include "account.h" #include "capabilities.h" -#include - -#include -#include -#include -#include -#include -#include - #if defined(Q_OS_WIN) #include "folderwatcher_win.h" #elif defined(Q_OS_MAC) @@ -39,9 +30,44 @@ #include "folder.h" #include "filesystem.h" +#include +#include +#include +#include +#include +#include + +#include +#include + namespace { -const char *lockFilePatterns[] = {".~lock.", "~$"}; +const std::array lockFilePatterns = {{".~lock.", "~$"}}; + +QString filePathLockFilePatternMatch(const QString &path) +{ + qCDebug(OCC::lcFolderWatcher) << "Checking if it is a lock file:" << path; + + const auto pathSplit = path.split(QLatin1Char('/'), Qt::SkipEmptyParts); + if (pathSplit.isEmpty()) { + return {}; + } + QString lockFilePatternFound; + for (const auto &lockFilePattern : lockFilePatterns) { + if (pathSplit.last().startsWith(lockFilePattern)) { + lockFilePatternFound = lockFilePattern; + break; + } + } + + if (lockFilePatternFound.isEmpty()) { + return {}; + } + + qCDebug(OCC::lcFolderWatcher) << "Found a lock file with prefix:" << lockFilePatternFound << "in path:" << path; + return lockFilePatternFound; +} + } namespace OCC { @@ -160,7 +186,7 @@ void FolderWatcher::changeDetected(const QStringList &paths) // - why do we skip the file altogether instead of e.g. reducing the upload frequency? // Check if the same path was reported within the last second. - QSet pathsSet = paths.toSet(); + const auto pathsSet = paths.toSet(); if (pathsSet == _lastPaths && _timer.elapsed() < 1000) { // the same path was reported within the last second. Skip. return; @@ -172,20 +198,21 @@ void FolderWatcher::changeDetected(const QStringList &paths) QSet unlockedFiles; QSet lockedFiles; - for (int i = 0; i < paths.size(); ++i) { - QString path = paths[i]; + for (const auto &path : paths) { if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(path, _testNotificationPath)) { _testNotificationPath.clear(); } - - const auto checkResult = checkIfFileIsLockOrUnlock(path); - if (_shouldWatchForFileUnlocking) { - if (checkResult.type == FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty()) { - unlockedFiles.insert(checkResult.path); - } - qCDebug(lcFolderWatcher) << "Unlocked files:" << unlockedFiles.values(); + const auto lockFileNamePattern = filePathLockFilePatternMatch(path); + const auto checkResult = lockFileTargetFilePath(path,lockFileNamePattern); + if (_shouldWatchForFileUnlocking) { + // Lock file has been deleted, file now unlocked + if (checkResult.type == FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty() && !QFile::exists(path)) { + unlockedFiles.insert(checkResult.path); + } else if (!checkResult.path.isEmpty() && QFile::exists(path)) { // Lock file found + lockedFiles.insert(checkResult.path); + } } if (checkResult.type == FileLockingInfo::Type::Locked && !checkResult.path.isEmpty()) { @@ -194,7 +221,7 @@ void FolderWatcher::changeDetected(const QStringList &paths) qCDebug(lcFolderWatcher) << "Locked files:" << lockedFiles.values(); - // ------- handle ignores: + // ------- handle ignores: if (pathIsIgnored(path)) { continue; } @@ -202,6 +229,9 @@ void FolderWatcher::changeDetected(const QStringList &paths) changedPaths.insert(path); } + qCDebug(lcFolderWatcher) << "Unlocked files:" << unlockedFiles.values(); + qCDebug(lcFolderWatcher) << "Locked files:" << lockedFiles; + if (!unlockedFiles.isEmpty()) { emit filesLockReleased(unlockedFiles); } @@ -210,12 +240,16 @@ void FolderWatcher::changeDetected(const QStringList &paths) emit filesLockImposed(lockedFiles); } + if (!lockedFiles.isEmpty()) { + emit lockedFilesFound(lockedFiles); + } + if (changedPaths.isEmpty()) { return; } qCInfo(lcFolderWatcher) << "Detected changes in paths:" << changedPaths; - foreach (const QString &path, changedPaths) { + for (const auto &path : changedPaths) { emit pathChanged(path); } } @@ -225,31 +259,16 @@ void FolderWatcher::folderAccountCapabilitiesChanged() _shouldWatchForFileUnlocking = _folder->accountState()->account()->capabilities().filesLockAvailable(); } -FolderWatcher::FileLockingInfo FolderWatcher::checkIfFileIsLockOrUnlock(const QString &path) const +FolderWatcher::FileLockingInfo FolderWatcher::lockFileTargetFilePath(const QString &path, const QString &lockFileNamePattern) const { - qCDebug(lcFolderWatcher) << "Checking if it is a lock file:" << path; - FileLockingInfo result; - const auto pathSplit = path.split(QLatin1Char('/'), Qt::SkipEmptyParts); - if (pathSplit.isEmpty()) { - return result; - } - QString lockFilePatternFound; - for (const auto &lockFilePattern : lockFilePatterns) { - if (pathSplit.last().startsWith(lockFilePattern)) { - lockFilePatternFound = lockFilePattern; - break; - } - } - if (lockFilePatternFound.isEmpty()) { + if (lockFileNamePattern.isEmpty()) { return result; } - qCDebug(lcFolderWatcher) << "Found a lock file with prefix:" << lockFilePatternFound << "in path:" << path; - - const auto lockFilePathWitoutPrefix = QString(path).replace(lockFilePatternFound, QStringLiteral("")); - auto lockFilePathWithoutPrefixSplit = lockFilePathWitoutPrefix.split(QLatin1Char('.')); + const auto lockFilePathWithoutPrefix = QString(path).replace(lockFileNamePattern, QStringLiteral("")); + auto lockFilePathWithoutPrefixSplit = lockFilePathWithoutPrefix.split(QLatin1Char('.')); if (lockFilePathWithoutPrefixSplit.size() < 2) { return result; @@ -265,10 +284,10 @@ FolderWatcher::FileLockingInfo FolderWatcher::checkIfFileIsLockOrUnlock(const QS ); lockFilePathWithoutPrefixSplit.push_back(QString::fromStdString(extensionSanitized)); - const auto lockFilePathWithoutPrefix = lockFilePathWithoutPrefixSplit.join(QLatin1Char('.')); + const auto lockFilePathWithoutPrefixNew = lockFilePathWithoutPrefixSplit.join(QLatin1Char('.')); - qCDebug(lcFolderWatcher) << "Assumed locked/unlocked file path" << lockFilePathWithoutPrefix << "Going to try to find matching file"; - auto splitFilePath = lockFilePathWithoutPrefix.split(QLatin1Char('/')); + qCDebug(lcFolderWatcher) << "Assumed locked/unlocked file path" << lockFilePathWithoutPrefixNew << "Going to try to find matching file"; + auto splitFilePath = lockFilePathWithoutPrefixNew.split(QLatin1Char('/')); if (splitFilePath.size() > 1) { const auto lockFileNameWithoutPrefix = splitFilePath.takeLast(); // some software will modify lock file name such that it does not correspond to original file (removing some symbols from the name, so we will search @@ -288,7 +307,8 @@ QString FolderWatcher::findMatchingUnlockedFileInDir(const QString &dirPath, con { QString foundFilePath; const QDir dir(dirPath); - for (const auto &candidateUnlockedFileInfo : dir.entryInfoList(QDir::Files)) { + const auto entryList = dir.entryInfoList(QDir::Files); + for (const auto &candidateUnlockedFileInfo : entryList) { if (candidateUnlockedFileInfo.fileName().contains(lockFileName)) { foundFilePath = candidateUnlockedFileInfo.absoluteFilePath(); break; diff --git a/src/gui/folderwatcher.h b/src/gui/folderwatcher.h index f2d59f703d..4e2c569b98 100644 --- a/src/gui/folderwatcher.h +++ b/src/gui/folderwatcher.h @@ -102,6 +102,10 @@ signals: */ void filesLockImposed(const QSet &files); + void lockFilesFound(const QSet &files); + + void lockedFilesFound(const QSet &files); + /** * Emitted if some notifications were lost. * @@ -141,9 +145,11 @@ private: void appendSubPaths(QDir dir, QStringList& subPaths); - [[nodiscard]] FileLockingInfo checkIfFileIsLockOrUnlock(const QString &path) const; + [[nodiscard]] FileLockingInfo lockFileTargetFilePath(const QString &path, const QString &lockFileNamePattern) const; [[nodiscard]] QString findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName) const; + QString findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName); + /* Check if the path should be igored by the FolderWatcher. */ [[nodiscard]] bool pathIsIgnored(const QString &path) const;