]> dgit.raspbian.org Git - nextcloud-desktop.git/commitdiff
Unlock Office files when they are closed.
authoralex-z <blackslayer4@gmail.com>
Fri, 7 Apr 2023 19:09:06 +0000 (21:09 +0200)
committeralex-z <blackslayer4@gmail.com>
Tue, 25 Apr 2023 10:54:37 +0000 (12:54 +0200)
Signed-off-by: alex-z <blackslayer4@gmail.com>
src/gui/folder.cpp
src/gui/folder.h
src/gui/folderwatcher.cpp
src/gui/folderwatcher.h

index 9db1c3f77dec4a535ad6bc2a8fb2a212f802967d..96b26798f01899df72d9bc1a026d13323ea1b37f 100644 (file)
@@ -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<QString> &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<qint64>(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"));
 }
index 2d0a8edc4bb294e13035d8c3d724dd247c8cd62e..6f26eb5e541e2a5659a972e4da0064847721ee56 100644 (file)
@@ -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<QString> &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> _vfs;
+
+    QMetaObject::Connection _officeFileLockReleaseUnlockSuccess;
+    QMetaObject::Connection _officeFileLockReleaseUnlockFailure;
 };
 }
 
index 04d10776bdb2117c2e572dc9e6a9a610ab190a91..00ba227b7452215c420ebf3586ee29c6b14c4554 100644 (file)
 // event masks
 #include "folderwatcher.h"
 
+#include "accountstate.h"
+#include "account.h"
+#include "capabilities.h"
+
 #include <cstdint>
 
 #include <QFileInfo>
 #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<QString> changedPaths;
+    QSet<QString> 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
index 957f8a0359a24246104c0b223b62ac321acbe188..3403fabbda032dc86c85ee6139c04fda5bd22917 100644 (file)
@@ -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<QString> &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;