Unlock Office files when they are closed.

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2023-04-07 21:09:06 +02:00
parent 4cc85d6dac
commit da6a7d3dac
4 changed files with 171 additions and 1 deletions

View 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"));
}

View 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;
};
}

View file

@ -15,6 +15,10 @@
// event masks
#include "folderwatcher.h"
#include "accountstate.h"
#include "account.h"
#include "capabilities.h"
#include <cstdint>
#include <QFileInfo>
@ -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<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

View 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;