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
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;
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()) {
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"));
}
*/
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.
*
/** Unblocks normal sync operation */
void slotHydrationDone();
+ void slotCapabilitiesChanged();
+
private:
void connectSyncRoot();
* The vfs mode instance (created by plugin) to use. Never null.
*/
QSharedPointer<Vfs> _vfs;
+
+ QMetaObject::Connection _officeFileLockReleaseUnlockSuccess;
+ QMetaObject::Connection _officeFileLockReleaseUnlockFailure;
};
}
// 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)
: 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;
_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;
}
}
}
+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
* 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.
*
// 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();
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;