diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 7b42535f22d8f..c26eb4a90b9f4 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1598,6 +1598,7 @@ void Folder::registerFolderWatcher() connect(_folderWatcher.data(), &FolderWatcher::filesLockImposed, this, &Folder::slotFilesLockImposed, Qt::UniqueConnection); _folderWatcher->init(path()); _folderWatcher->startNotificatonTest(path() + QLatin1String(".nextcloudsync.log")); + connect(_engine.data(), &SyncEngine::lockFileDetected, _folderWatcher.data(), &FolderWatcher::slotLockFileDetectedExternally); } void Folder::disconnectFolderWatcher() diff --git a/src/gui/folderwatcher.cpp b/src/gui/folderwatcher.cpp index 07436463a81ab..03ab9a32c8175 100644 --- a/src/gui/folderwatcher.cpp +++ b/src/gui/folderwatcher.cpp @@ -41,34 +41,7 @@ namespace { -const std::array lockFilePatterns = {{".~lock.", "~$"}}; - constexpr auto lockChangeDebouncingTimerIntervalMs = 500; - -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 { @@ -185,6 +158,12 @@ int FolderWatcher::testLinuxWatchCount() const #endif } +void FolderWatcher::slotLockFileDetectedExternally(const QString &lockFile) +{ + qCInfo(lcFolderWatcher) << "Lock file detected externally, probably a newly-uploaded office file: " << lockFile; + changeDetected(lockFile); +} + void FolderWatcher::changeDetected(const QString &path) { QFileInfo fileInfo(path); @@ -220,17 +199,17 @@ void FolderWatcher::changeDetected(const QStringList &paths) _testNotificationPath.clear(); } - const auto lockFileNamePattern = filePathLockFilePatternMatch(path); - const auto checkResult = lockFileTargetFilePath(path,lockFileNamePattern); + const auto lockFileNamePattern = FileSystem::filePathLockFilePatternMatch(path); + const auto checkResult = FileSystem::lockFileTargetFilePath(path, lockFileNamePattern); if (_shouldWatchForFileUnlocking) { // Lock file has been deleted, file now unlocked - if (checkResult.type == FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty()) { + if (checkResult.type == FileSystem::FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty()) { _lockedFiles.remove(checkResult.path); _unlockedFiles.insert(checkResult.path); } } - if (checkResult.type == FileLockingInfo::Type::Locked && !checkResult.path.isEmpty()) { + if (checkResult.type == FileSystem::FileLockingInfo::Type::Locked && !checkResult.path.isEmpty()) { _unlockedFiles.remove(checkResult.path); _lockedFiles.insert(checkResult.path); } @@ -272,62 +251,4 @@ void FolderWatcher::folderAccountCapabilitiesChanged() _shouldWatchForFileUnlocking = _folder->accountState()->account()->capabilities().filesLockAvailable(); } -FolderWatcher::FileLockingInfo FolderWatcher::lockFileTargetFilePath(const QString &path, const QString &lockFileNamePattern) const -{ - FileLockingInfo result; - - if (lockFileNamePattern.isEmpty()) { - return result; - } - - const auto lockFilePathWithoutPrefix = QString(path).replace(lockFileNamePattern, QStringLiteral("")); - auto lockFilePathWithoutPrefixSplit = lockFilePathWithoutPrefix.split(QLatin1Char('.')); - - if (lockFilePathWithoutPrefixSplit.size() < 2) { - return result; - } - - 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)); - const auto lockFilePathWithoutPrefixNew = lockFilePathWithoutPrefixSplit.join(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 - // for a matching file - result.path = findMatchingUnlockedFileInDir(splitFilePath.join(QLatin1Char('/')), lockFileNameWithoutPrefix); - } - - if (result.path.isEmpty() || !QFile::exists(result.path)) { - result.path.clear(); - return result; - } - result.type = QFile::exists(path) ? FileLockingInfo::Type::Locked : FileLockingInfo::Type::Unlocked; - return result; -} - -QString FolderWatcher::findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName) const -{ - QString foundFilePath; - const QDir dir(dirPath); - const auto entryList = dir.entryInfoList(QDir::Files); - for (const auto &candidateUnlockedFileInfo : entryList) { - 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 c172dc2d994be..7591b62627edb 100644 --- a/src/gui/folderwatcher.h +++ b/src/gui/folderwatcher.h @@ -50,12 +50,6 @@ class FolderWatcher : public QObject { Q_OBJECT - struct FileLockingInfo { - enum class Type { Unset = -1, Locked, Unlocked }; - QString path; - Type type = Type::Unset; - }; - public: // Construct, connect signals, call init() explicit FolderWatcher(Folder *folder = nullptr); @@ -86,6 +80,8 @@ class FolderWatcher : public QObject /// For testing linux behavior only [[nodiscard]] int testLinuxWatchCount() const; + void slotLockFileDetectedExternally(const QString &lockFile); + signals: /** Emitted when one of the watched directories or one * of the contained files is changed. */ @@ -101,8 +97,6 @@ class FolderWatcher : public QObject */ void filesLockImposed(const QSet &files); - void lockFilesFound(const QSet &files); - void lockedFilesFound(const QSet &files); /** @@ -145,11 +139,6 @@ private slots: void appendSubPaths(QDir dir, QStringList& subPaths); - [[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 ignored by the FolderWatcher. */ [[nodiscard]] bool pathIsIgnored(const QString &path) const; diff --git a/src/libsync/filesystem.cpp b/src/libsync/filesystem.cpp index 942b635ffeaec..cc3f59e47c2e5 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -25,12 +25,125 @@ #include #include +#include +#include + #ifdef Q_OS_WIN #include #include #endif +namespace +{ +constexpr std::array lockFilePatterns = {{".~lock.", "~$"}}; +constexpr std::array officeFileExtensions = {"doc", "docx", "xls", "xlsx", "ppt", "pptx", "odt", "odp"}; +// iterates through the dirPath to find the matching fileName +QString findMatchingUnlockedFileInDir(const QString &dirPath, const QString &fileName) +{ + QString foundFilePath; + const QDir dir(dirPath); + const auto entryList = dir.entryInfoList(QDir::Files); + for (const auto &candidateUnlockedFileInfo : entryList) { + if (candidateUnlockedFileInfo.fileName() == fileName) { + foundFilePath = candidateUnlockedFileInfo.absoluteFilePath(); + break; + } + } + return foundFilePath; +} +} + namespace OCC { + +QString FileSystem::filePathLockFilePatternMatch(const QString &path) +{ + qCDebug(OCC::lcFileSystem) << "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()) { + qCDebug(OCC::lcFileSystem) << "Found a lock file with prefix:" << lockFilePatternFound << "in path:" << path; + } + + return lockFilePatternFound; +} + +bool FileSystem::isMatchingOfficeFileExtension(const QString &path) +{ + const auto pathSplit = path.split(QLatin1Char('.')); + const auto extension = pathSplit.size() > 1 ? pathSplit.last().toStdString() : std::string{}; + return std::find(std::cbegin(officeFileExtensions), std::cend(officeFileExtensions), extension) != std::cend(officeFileExtensions); +} + +FileSystem::FileLockingInfo FileSystem::lockFileTargetFilePath(const QString &lockFilePath, const QString &lockFileNamePattern) +{ + FileLockingInfo result; + + if (lockFileNamePattern.isEmpty()) { + return result; + } + + const auto lockFilePathWithoutPrefix = QString(lockFilePath).replace(lockFileNamePattern, QStringLiteral("")); + auto lockFilePathWithoutPrefixSplit = lockFilePathWithoutPrefix.split(QLatin1Char('.')); + + if (lockFilePathWithoutPrefixSplit.size() < 2) { + return result; + } + + 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)); + const auto lockFilePathWithoutPrefixNew = lockFilePathWithoutPrefixSplit.join(QLatin1Char('.')); + + qCDebug(lcFileSystem) << "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 for a matching file + result.path = findMatchingUnlockedFileInDir(splitFilePath.join(QLatin1Char('/')), lockFileNameWithoutPrefix); + } + + if (result.path.isEmpty() || !QFile::exists(result.path)) { + result.path.clear(); + return result; + } + result.type = QFile::exists(lockFilePath) ? FileLockingInfo::Type::Locked : FileLockingInfo::Type::Unlocked; + return result; +} + +QStringList FileSystem::findAllLockFilesInDir(const QString &dirPath) +{ + QStringList results; + const QDir dir(dirPath); + const auto entryList = dir.entryInfoList(QDir::Files | QDir::Hidden | QDir::NoDotAndDotDot); + for (const auto &candidateLockFile : entryList) { + const auto filePath = candidateLockFile.filePath(); + const auto isLockFile = !filePathLockFilePatternMatch(filePath).isEmpty(); + if (isLockFile) { + results.push_back(filePath); + } + } + + return results; +} bool FileSystem::fileEquals(const QString &fn1, const QString &fn2) { diff --git a/src/libsync/filesystem.h b/src/libsync/filesystem.h index 0a1b68a29a58f..7028512fe84b3 100644 --- a/src/libsync/filesystem.h +++ b/src/libsync/filesystem.h @@ -20,6 +20,7 @@ #include "common/filesystembase.h" #include +#include #include #include @@ -42,6 +43,20 @@ class SyncJournal; * @brief This file contains file system helper */ namespace FileSystem { + struct OWNCLOUDSYNC_EXPORT FileLockingInfo { + enum class Type { Unset = -1, Locked, Unlocked }; + QString path; + Type type = Type::Unset; + }; + + // match file path with lock pattern + QString OWNCLOUDSYNC_EXPORT filePathLockFilePatternMatch(const QString &path); + // check if it is an office file (by extension), ONLY call it for files + bool OWNCLOUDSYNC_EXPORT isMatchingOfficeFileExtension(const QString &path); + // finds and fetches FileLockingInfo for the corresponding file that we are locking/unlocking + FileLockingInfo OWNCLOUDSYNC_EXPORT lockFileTargetFilePath(const QString &lockFilePath, const QString &lockFileNamePattern); + // lists all files matching a lockfile pattern in dirPath + QStringList OWNCLOUDSYNC_EXPORT findAllLockFilesInDir(const QString &dirPath); /** * @brief compare two files with given filename and return true if they have the same content diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index e25ea716d2255..7026fd3ce3ec6 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -932,6 +932,33 @@ void SyncEngine::slotCleanPollsJobAborted(const QString &error, const ErrorCateg finalize(false); } +void SyncEngine::detectFileLock(const SyncFileItemPtr &item) +{ + const auto isNewlyUploadedFile = !item->isDirectory() && + item->_instruction == CSYNC_INSTRUCTION_NEW && + item->_direction == SyncFileItem::Up && item->_status == SyncFileItem::Success; + + if (isNewlyUploadedFile && item->_locked != SyncFileItem::LockStatus::LockedItem && _account->capabilities().filesLockAvailable() && + FileSystem::isMatchingOfficeFileExtension(item->_file)) { + { + SyncJournalFileRecord rec; + if (!_journal->getFileRecord(item->_file, &rec) || !rec.isValid()) { + qCWarning(lcEngine) << "Newly-created office file just uploaded but not in sync journal. Not going to lock it." << item->_file; + return; + } + } + const auto localFilePath = _propagator->fullLocalPath(item->_file); + const auto allMatchingLockFiles = FileSystem::findAllLockFilesInDir(QFileInfo(localFilePath).absolutePath()); + for (const auto &lockFilePath : allMatchingLockFiles) { + const auto checkResult = FileSystem::lockFileTargetFilePath(lockFilePath, FileSystem::filePathLockFilePatternMatch(lockFilePath)); + if (checkResult.type == FileSystem::FileLockingInfo::Type::Locked && checkResult.path == localFilePath) { + qCInfo(lcEngine) << "Newly-created office file lock detected. Let FolderWatcher take it from here..." << item->_file; + emit lockFileDetected(lockFilePath); + } + } + } +} + void SyncEngine::setNetworkLimits(int upload, int download) { _uploadLimit = upload; @@ -954,6 +981,8 @@ void SyncEngine::slotItemCompleted(const SyncFileItemPtr &item, const ErrorCateg emit transmissionProgress(*_progressInfo); emit itemCompleted(item, category); + + detectFileLock(item); } void SyncEngine::slotPropagationFinished(OCC::SyncFileItem::Status status) diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h index 2a60cba2dcd32..af234725cf2d8 100644 --- a/src/libsync/syncengine.h +++ b/src/libsync/syncengine.h @@ -195,6 +195,8 @@ public slots: */ void seenLockedFile(const QString &fileName); + void lockFileDetected(const QString &lockFile); + private slots: void slotFolderDiscovered(bool local, const QString &folder); void slotRootEtagReceived(const QByteArray &, const QDateTime &time); @@ -215,6 +217,7 @@ private slots: void slotPropagationFinished(SyncFileItem::Status status); void slotProgress(const OCC::SyncFileItem &item, qint64 current); void slotCleanPollsJobAborted(const QString &error, const OCC::ErrorCategory category); + void detectFileLock(const OCC::SyncFileItemPtr &item); /** Records that a file was touched by a job. */ void slotAddTouchedFile(const QString &fn);