diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 9db1c3f77..96b26798f 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -123,6 +123,8 @@ Folder::Folder(const FolderDefinition &definition, connect(_engine.data(), &SyncEngine::itemCompleted, _localDiscoveryTracker.data(), &LocalDiscoveryTracker::slotItemCompleted); + connect(_accountState->account().data(), &Account::capabilitiesChanged, this, &Folder::slotCapabilitiesChanged); + // Potentially upgrade suffix vfs to windows vfs ENFORCE(_vfs); if (_definition.virtualFilesMode == Vfs::WithSuffix @@ -615,6 +617,39 @@ void Folder::slotWatchedPathChanged(const QString &path, ChangeReason reason) scheduleThisFolderSoon(); } +void Folder::slotFilesLockReleased(const QSet &files) +{ + qCDebug(lcFolder) << "Going to unlock office files" << files; + + for (const auto &file : files) { + const auto fileRecordPath = fileFromLocalPath(file); + SyncJournalFileRecord rec; + const auto canUnlockFile = journalDb()->getFileRecord(fileRecordPath, &rec) + && rec.isValid() + && rec._lockstate._locked + && rec._lockstate._lockOwnerType == static_cast(SyncFileItem::LockOwnerType::UserLock) + && rec._lockstate._lockOwnerId == _accountState->account()->davUser(); + + if (!canUnlockFile) { + qCDebug(lcFolder) << "Skipping 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) << "Unlocking an office file" << remoteFilePath; + _officeFileLockReleaseUnlockSuccess = connect(_accountState->account().data(), &Account::lockFileSuccess, this, [this, remoteFilePath]() { + disconnect(_officeFileLockReleaseUnlockSuccess); + qCDebug(lcFolder) << "Unlocking an office file succeeded" << remoteFilePath; + startSync(); + }); + _officeFileLockReleaseUnlockFailure = connect(_accountState->account().data(), &Account::lockFileError, this, [this, remoteFilePath](const QString &message) { + disconnect(_officeFileLockReleaseUnlockFailure); + qCWarning(lcFolder) << "Failed to unlock a file:" << remoteFilePath << message; + }); + _accountState->account()->setLockFileState(remoteFilePath, journalDb(), SyncFileItem::LockStatus::UnlockedItem); + } +} + void Folder::implicitlyHydrateFile(const QString &relativepath) { qCInfo(lcFolder) << "Implicitly hydrate virtual file:" << relativepath; @@ -1257,6 +1292,13 @@ void Folder::slotHydrationDone() emit syncStateChange(); } +void Folder::slotCapabilitiesChanged() +{ + if (_accountState->account()->capabilities().filesLockAvailable()) { + connect(_folderWatcher.data(), &FolderWatcher::filesLockReleased, this, &Folder::slotFilesLockReleased, Qt::UniqueConnection); + } +} + void Folder::scheduleThisFolderSoon() { if (!_scheduleSelfTimer.isActive()) { @@ -1297,6 +1339,9 @@ void Folder::registerFolderWatcher() this, &Folder::slotNextSyncFullLocalDiscovery); connect(_folderWatcher.data(), &FolderWatcher::becameUnreliable, this, &Folder::slotWatcherUnreliable); + if (_accountState->account()->capabilities().filesLockAvailable()) { + connect(_folderWatcher.data(), &FolderWatcher::filesLockReleased, this, &Folder::slotFilesLockReleased); + } _folderWatcher->init(path()); _folderWatcher->startNotificatonTest(path() + QLatin1String(".nextcloudsync.log")); } diff --git a/src/gui/folder.h b/src/gui/folder.h index 2d0a8edc4..6f26eb5e5 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -342,6 +342,11 @@ public slots: */ void slotWatchedPathChanged(const QString &path, OCC::Folder::ChangeReason reason); + /* + * Triggered when lock files were removed + */ + void slotFilesLockReleased(const QSet &files); + /** * Mark a virtual file as being requested for download, and start a sync. * @@ -428,6 +433,8 @@ private slots: /** Unblocks normal sync operation */ void slotHydrationDone(); + void slotCapabilitiesChanged(); + private: void connectSyncRoot(); @@ -533,6 +540,9 @@ private: * The vfs mode instance (created by plugin) to use. Never null. */ QSharedPointer _vfs; + + QMetaObject::Connection _officeFileLockReleaseUnlockSuccess; + QMetaObject::Connection _officeFileLockReleaseUnlockFailure; }; } diff --git a/src/gui/folderwatcher.cpp b/src/gui/folderwatcher.cpp index 04d10776b..00ba227b7 100644 --- a/src/gui/folderwatcher.cpp +++ b/src/gui/folderwatcher.cpp @@ -15,6 +15,10 @@ // event masks #include "folderwatcher.h" +#include "accountstate.h" +#include "account.h" +#include "capabilities.h" + #include #include @@ -35,6 +39,11 @@ #include "folder.h" #include "filesystem.h" +namespace +{ +const char *lockFilePatterns[] = {".~lock.", "~$"}; +} + namespace OCC { Q_LOGGING_CATEGORY(lcFolderWatcher, "nextcloud.gui.folderwatcher", QtInfoMsg) @@ -43,6 +52,10 @@ FolderWatcher::FolderWatcher(Folder *folder) : QObject(folder) , _folder(folder) { + if (_folder && _folder->accountState() && _folder->accountState()->account()) { + connect(_folder->accountState()->account().data(), &Account::capabilitiesChanged, this, &FolderWatcher::folderAccountCapabilitiesChanged); + folderAccountCapabilitiesChanged(); + } } FolderWatcher::~FolderWatcher() = default; @@ -166,20 +179,36 @@ void FolderWatcher::changeDetected(const QStringList &paths) _timer.restart(); QSet changedPaths; + QSet unlockedFiles; - // ------- handle ignores: for (int i = 0; i < paths.size(); ++i) { QString path = paths[i]; if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(path, _testNotificationPath)) { _testNotificationPath.clear(); } + + if (_shouldWatchForFileUnlocking) { + const auto unlockedFilePath = possiblyAddUnlockedFilePath(path); + if (!unlockedFilePath.isEmpty()) { + unlockedFiles.insert(unlockedFilePath); + } + + qCDebug(lcFolderWatcher) << "Unlocked files:" << unlockedFiles.values(); + } + + // ------- handle ignores: if (pathIsIgnored(path)) { continue; } changedPaths.insert(path); } + + if (!unlockedFiles.isEmpty()) { + emit filesLockReleased(unlockedFiles); + } + if (changedPaths.isEmpty()) { return; } @@ -190,4 +219,79 @@ void FolderWatcher::changeDetected(const QStringList &paths) } } +void FolderWatcher::folderAccountCapabilitiesChanged() +{ + _shouldWatchForFileUnlocking = _folder->accountState()->account()->capabilities().filesLockAvailable(); +} + +QString FolderWatcher::possiblyAddUnlockedFilePath(const QString &path) +{ + qCDebug(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() || QFileInfo::exists(path)) { + return {}; + } + + 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('.')); + + if (lockFilePathWithoutPrefixSplit.size() < 2) { + return {}; + } + + auto extensionSanitized = lockFilePathWithoutPrefixSplit.takeLast().toStdString(); + // remove possible non-alphabetical characters at the end of the extension + extensionSanitized.erase( + std::remove_if(extensionSanitized.begin(), extensionSanitized.end(), [](const auto &ch) { + return !std::isalnum(ch); + }), + extensionSanitized.end() + ); + + lockFilePathWithoutPrefixSplit.push_back(QString::fromStdString(extensionSanitized)); + auto unlockedFilePath = lockFilePathWithoutPrefixSplit.join(QLatin1Char('.')); + + if (!QFile::exists(unlockedFilePath)) { + unlockedFilePath.clear(); + qCDebug(lcFolderWatcher) << "Assumed unlocked file path" << unlockedFilePath << "does not exist. Going to try to find matching file"; + auto splitFilePath = unlockedFilePath.split(QLatin1Char('/')); + if (splitFilePath.size() > 1) { + const auto lockFileName = 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 for a matching file + unlockedFilePath = findMatchingUnlockedFileInDir(splitFilePath.join(QLatin1Char('/')), lockFileName); + } + } + + if (unlockedFilePath.isEmpty() || !QFile::exists(unlockedFilePath)) { + return {}; + } + return unlockedFilePath; +} + +QString FolderWatcher::findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName) +{ + QString foundFilePath; + const QDir dir(dirPath); + for (const auto &candidateUnlockedFileInfo : dir.entryInfoList(QDir::Files)) { + if (candidateUnlockedFileInfo.fileName().contains(lockFileName)) { + foundFilePath = candidateUnlockedFileInfo.absoluteFilePath(); + break; + } + } + return foundFilePath; +} + } // namespace OCC diff --git a/src/gui/folderwatcher.h b/src/gui/folderwatcher.h index 957f8a035..3403fabbd 100644 --- a/src/gui/folderwatcher.h +++ b/src/gui/folderwatcher.h @@ -88,6 +88,11 @@ signals: * of the contained files is changed. */ void pathChanged(const QString &path); + /* + * Emitted when lock files were removed + */ + void filesLockReleased(const QSet &files); + /** * Emitted if some notifications were lost. * @@ -108,6 +113,7 @@ protected slots: // called from the implementations to indicate a change in path void changeDetected(const QString &path); void changeDetected(const QStringList &paths); + void folderAccountCapabilitiesChanged(); private slots: void startNotificationTestWhenReady(); @@ -122,8 +128,13 @@ private: Folder *_folder; bool _isReliable = true; + bool _shouldWatchForFileUnlocking = false; + void appendSubPaths(QDir dir, QStringList& subPaths); + QString possiblyAddUnlockedFilePath(const QString &path); + QString findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName); + /** Path of the expected test notification */ QString _testNotificationPath;