diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 6b797d0d6556..d7c5fbac54bc 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1603,6 +1603,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 07436463a81a..93c2334506a8 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,22 @@ 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::setShouldWatchForFileUnlocking(bool shouldWatchForFileUnlocking) +{ + _shouldWatchForFileUnlocking = shouldWatchForFileUnlocking; +} + +int FolderWatcher::lockChangeDebouncingTimout() const +{ + return _lockChangeDebouncingTimer.interval(); +} + void FolderWatcher::changeDetected(const QString &path) { QFileInfo fileInfo(path); @@ -220,17 +209,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 +261,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 c172dc2d994b..17886ee3ccf7 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,11 @@ class FolderWatcher : public QObject /// For testing linux behavior only [[nodiscard]] int testLinuxWatchCount() const; + void slotLockFileDetectedExternally(const QString &lockFile); + + void setShouldWatchForFileUnlocking(bool shouldWatchForFileUnlocking); + [[nodiscard]] int lockChangeDebouncingTimout() const; + signals: /** Emitted when one of the watched directories or one * of the contained files is changed. */ @@ -101,8 +100,6 @@ class FolderWatcher : public QObject */ void filesLockImposed(const QSet &files); - void lockFilesFound(const QSet &files); - void lockedFilesFound(const QSet &files); /** @@ -145,11 +142,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 7c58c7b2bde4..e4a5dbc65ba0 100644 --- a/src/libsync/filesystem.cpp +++ b/src/libsync/filesystem.cpp @@ -25,7 +25,130 @@ #include "vio/csync_vio_local.h" #include "std/c_time.h" +#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 &lockFileName) +{ + QString foundFilePath; + const QDir dir(dirPath); + const auto entryList = dir.entryInfoList(QDir::Files); + for (const auto &candidateUnlockedFileInfo : entryList) { + const auto candidateUnlockFileName = candidateUnlockedFileInfo.fileName(); + const auto lockFilePatternFoundIt = std::find_if(std::cbegin(lockFilePatterns), std::cend(lockFilePatterns), [&candidateUnlockFileName](std::string_view pattern) { + return candidateUnlockFileName.contains(QString::fromStdString(std::string(pattern))); + }); + if (lockFilePatternFoundIt != std::cend(lockFilePatterns)) { + continue; + } + if (candidateUnlockFileName.contains(lockFileName)) { + 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 602f74bb7f2b..10af61461e53 100644 --- a/src/libsync/filesystem.h +++ b/src/libsync/filesystem.h @@ -17,6 +17,8 @@ #include "config.h" #include +#include + #include #include @@ -39,6 +41,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 e25ea716d225..7026fd3ce3ec 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 2a60cba2dcd3..af234725cf2d 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); diff --git a/test/testfolderwatcher.cpp b/test/testfolderwatcher.cpp index d57358d37077..2e614700106a 100644 --- a/test/testfolderwatcher.cpp +++ b/test/testfolderwatcher.cpp @@ -262,6 +262,121 @@ private slots: mkdir(dir); QVERIFY(waitForPathChanged(dir)); } + + void testDetectLockFiles() + { + QStringList listOfOfficeFiles = {QString(_rootPath + "/document.docx"), QString(_rootPath + "/document.odt")}; + std::sort(std::begin(listOfOfficeFiles), std::end(listOfOfficeFiles)); + + const QStringList listOfOfficeLockFiles = {QString(_rootPath + "/.~lock.document.docx#"), QString(_rootPath + "/.~lock.document.odt#")}; + + _watcher->setShouldWatchForFileUnlocking(true); + + // verify that office files for locking got detected by the watcher + QScopedPointer locksImposedSpy(new QSignalSpy(_watcher.data(), &FolderWatcher::filesLockImposed)); + + for (const auto &officeFile : listOfOfficeFiles) { + touch(officeFile); + QVERIFY(waitForPathChanged(officeFile)); + } + for (const auto &officeLockFile : listOfOfficeLockFiles) { + touch(officeLockFile); + QVERIFY(waitForPathChanged(officeLockFile)); + } + + locksImposedSpy->wait(_watcher->lockChangeDebouncingTimout() + 100); + QCOMPARE(locksImposedSpy->count(), 1); + auto lockedOfficeFilesByWatcher = locksImposedSpy->takeFirst().at(0).value>().values(); + std::sort(std::begin(lockedOfficeFilesByWatcher), std::end(lockedOfficeFilesByWatcher)); + QCOMPARE(listOfOfficeFiles.size(), lockedOfficeFilesByWatcher.size()); + + for (int i = 0; i < listOfOfficeFiles.size(); ++i) { + QVERIFY(listOfOfficeFiles.at(i) == lockedOfficeFilesByWatcher.at(i)); + } + + // verify that office files for unlocking got detected by the watcher + QScopedPointer locksReleasedSpy(new QSignalSpy(_watcher.data(), &FolderWatcher::filesLockReleased)); + + for (const auto &officeLockFile : listOfOfficeLockFiles) { + rm(officeLockFile); + QVERIFY(waitForPathChanged(officeLockFile)); + } + + locksReleasedSpy->wait(_watcher->lockChangeDebouncingTimout() + 100); + QCOMPARE(locksReleasedSpy->count(), 1); + auto unLockedOfficeFilesByWatcher = locksReleasedSpy->takeFirst().at(0).value>().values(); + std::sort(std::begin(unLockedOfficeFilesByWatcher), std::end(unLockedOfficeFilesByWatcher)); + QCOMPARE(listOfOfficeFiles.size(), unLockedOfficeFilesByWatcher.size()); + + for (int i = 0; i < listOfOfficeFiles.size(); ++i) { + QVERIFY(listOfOfficeFiles.at(i) == unLockedOfficeFilesByWatcher.at(i)); + } + + // cleanup + for (const auto &officeLockFile : listOfOfficeLockFiles) { + rm(officeLockFile); + } + for (const auto &officeFile : listOfOfficeFiles) { + rm(officeFile); + } + } + void testDetectLockFilesExternally() + { + QStringList listOfOfficeFiles = {QString(_rootPath + "/document.docx"), QString(_rootPath + "/document.odt")}; + std::sort(std::begin(listOfOfficeFiles), std::end(listOfOfficeFiles)); + + const QStringList listOfOfficeLockFiles = {QString(_rootPath + "/.~lock.document.docx#"), QString(_rootPath + "/.~lock.document.odt#")}; + + for (const auto &officeFile : listOfOfficeFiles) { + touch(officeFile); + } + for (const auto &officeLockFile : listOfOfficeLockFiles) { + touch(officeLockFile); + } + + _watcher.reset(new FolderWatcher); + _watcher->init(_rootPath); + _watcher->setShouldWatchForFileUnlocking(true); + _pathChangedSpy.reset(new QSignalSpy(_watcher.data(), &FolderWatcher::pathChanged)); + QScopedPointer locksImposedSpy(new QSignalSpy(_watcher.data(), &FolderWatcher::filesLockImposed)); + QScopedPointer locksReleasedSpy(new QSignalSpy(_watcher.data(), &FolderWatcher::filesLockReleased)); + + for (const auto &officeLockFile : listOfOfficeLockFiles) { + _watcher->slotLockFileDetectedExternally(officeLockFile); + } + + // locked files detected + locksImposedSpy->wait(_watcher->lockChangeDebouncingTimout() + 100); + QCOMPARE(locksImposedSpy->count(), 1); + auto lockedOfficeFilesByWatcher = locksImposedSpy->takeFirst().at(0).value>().values(); + std::sort(std::begin(lockedOfficeFilesByWatcher), std::end(lockedOfficeFilesByWatcher)); + QCOMPARE(listOfOfficeFiles.size(), lockedOfficeFilesByWatcher.size()); + for (int i = 0; i < listOfOfficeFiles.size(); ++i) { + QVERIFY(listOfOfficeFiles.at(i) == lockedOfficeFilesByWatcher.at(i)); + } + + // unlocked files detected + for (const auto &officeLockFile : listOfOfficeLockFiles) { + rm(officeLockFile); + } + locksReleasedSpy->wait(_watcher->lockChangeDebouncingTimout() + 100); + QCOMPARE(locksReleasedSpy->count(), 1); + auto unLockedOfficeFilesByWatcher = locksReleasedSpy->takeFirst().at(0).value>().values(); + std::sort(std::begin(unLockedOfficeFilesByWatcher), std::end(unLockedOfficeFilesByWatcher)); + QCOMPARE(listOfOfficeFiles.size(), unLockedOfficeFilesByWatcher.size()); + + for (int i = 0; i < listOfOfficeFiles.size(); ++i) { + QVERIFY(listOfOfficeFiles.at(i) == unLockedOfficeFilesByWatcher.at(i)); + } + + // cleanup + for (const auto &officeFile : listOfOfficeFiles) { + rm(officeFile); + } + for (const auto &officeLockFile : listOfOfficeLockFiles) { + rm(officeLockFile); + } + } }; #ifdef Q_OS_MAC diff --git a/test/testlockfile.cpp b/test/testlockfile.cpp index 174f9c656abe..bd3680a43ddc 100644 --- a/test/testlockfile.cpp +++ b/test/testlockfile.cpp @@ -750,6 +750,27 @@ private slots: const auto localFileLocked = QFileInfo{fakeFolder.localPath() + u"A/a1"}; QVERIFY(!localFileLocked.isWritable()); } + + void testLockFile_lockFile_detect_newly_uploaded() + { + const auto testFileName = QStringLiteral("document.docx"); + const auto testLockFileName = QStringLiteral(".~lock.document.docx#"); + + const auto testDocumentsDirName = "documents"; + + FakeFolder fakeFolder{FileInfo{}}; + fakeFolder.localModifier().mkdir(testDocumentsDirName); + + fakeFolder.syncEngine().account()->setCapabilities({{"files", QVariantMap{{"locking", QByteArray{"1.0"}}}}}); + QSignalSpy lockFileDetectedNewlyUploadedSpy(&fakeFolder.syncEngine(), &OCC::SyncEngine::lockFileDetected); + + fakeFolder.localModifier().insert(testDocumentsDirName + QString("/") + testLockFileName); + fakeFolder.localModifier().insert(testDocumentsDirName + QString("/") + testFileName); + + QVERIFY(fakeFolder.syncOnce()); + + QCOMPARE(lockFileDetectedNewlyUploadedSpy.count(), 1); + } }; QTEST_GUILESS_MAIN(TestLockFile)