/* * Copyright (C) by Klaas Freitag * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ // event masks #include "folderwatcher.h" #include "accountstate.h" #include "account.h" #include "capabilities.h" #include #include #include #include #include #include #include #if defined(Q_OS_WIN) #include "folderwatcher_win.h" #elif defined(Q_OS_MAC) #include "folderwatcher_mac.h" #elif defined(Q_OS_UNIX) #include "folderwatcher_linux.h" #endif #include "folder.h" #include "filesystem.h" namespace { const char *lockFilePatterns[] = {".~lock.", "~$"}; 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 { Q_LOGGING_CATEGORY(lcFolderWatcher, "nextcloud.gui.folderwatcher", QtInfoMsg) 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; void FolderWatcher::init(const QString &root) { _d.reset(new FolderWatcherPrivate(this, root)); _timer.start(); } bool FolderWatcher::pathIsIgnored(const QString &path) const { return path.isEmpty(); } bool FolderWatcher::isReliable() const { return _isReliable; } void FolderWatcher::appendSubPaths(QDir dir, QStringList& subPaths) { QStringList newSubPaths = dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs | QDir::Files); for (int i = 0; i < newSubPaths.size(); i++) { QString path = dir.path() + "/" + newSubPaths[i]; QFileInfo fileInfo(path); subPaths.append(path); if (fileInfo.isDir()) { QDir dir(path); appendSubPaths(dir, subPaths); } } } void FolderWatcher::startNotificatonTest(const QString &path) { #ifdef Q_OS_MAC // Testing the folder watcher on OSX is harder because the watcher // automatically discards changes that were performed by our process. // It would still be useful to test but the OSX implementation // is deferred until later. return; #endif Q_ASSERT(_testNotificationPath.isEmpty()); _testNotificationPath = path; // Don't do the local file modification immediately: // wait for FolderWatchPrivate::_ready startNotificationTestWhenReady(); } void FolderWatcher::startNotificationTestWhenReady() { if (!_d->_ready) { QTimer::singleShot(1000, this, &FolderWatcher::startNotificationTestWhenReady); return; } auto path = _testNotificationPath; if (QFile::exists(path)) { auto mtime = FileSystem::getModTime(path); FileSystem::setModTime(path, mtime + 1); } else { QFile f(path); f.open(QIODevice::WriteOnly | QIODevice::Append); } FileSystem::setFileHidden(path, true); QTimer::singleShot(5000, this, [this]() { if (!_testNotificationPath.isEmpty()) emit becameUnreliable(tr("The watcher did not receive a test notification.")); _testNotificationPath.clear(); }); } int FolderWatcher::testLinuxWatchCount() const { #ifdef Q_OS_LINUX return _d->testWatchCount(); #else return -1; #endif } void FolderWatcher::changeDetected(const QString &path) { QFileInfo fileInfo(path); QStringList paths(path); if (fileInfo.isDir()) { QDir dir(path); appendSubPaths(dir, paths); } changeDetected(paths); } void FolderWatcher::changeDetected(const QStringList &paths) { // TODO: this shortcut doesn't look very reliable: // - why is the timeout only 1 second? // - what if there is more than one file being updated frequently? // - why do we skip the file altogether instead of e.g. reducing the upload frequency? // Check if the same path was reported within the last second. const auto pathsSet = paths.toSet(); if (pathsSet == _lastPaths && _timer.elapsed() < 1000) { // the same path was reported within the last second. Skip. return; } _lastPaths = pathsSet; _timer.restart(); QSet changedPaths; QSet unlockedFiles; QSet lockedFiles; for (const auto &path : paths) { if (!_testNotificationPath.isEmpty() && Utility::fileNamesEqual(path, _testNotificationPath)) { _testNotificationPath.clear(); } const auto checkResult = checkIfFileIsLockOrUnlock(path); if (_shouldWatchForFileUnlocking) { if (checkResult.type == FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty()) { unlockedFiles.insert(checkResult.path); } qCDebug(lcFolderWatcher) << "Unlocked files:" << unlockedFiles.values(); } if (checkResult.type == FileLockingInfo::Type::Locked && !checkResult.path.isEmpty()) { lockedFiles.insert(checkResult.path); } qCDebug(lcFolderWatcher) << "Locked files:" << lockedFiles.values(); // ------- handle ignores: if (pathIsIgnored(path)) { continue; } changedPaths.insert(path); } if (!unlockedFiles.isEmpty()) { emit filesLockReleased(unlockedFiles); } if (!lockedFiles.isEmpty()) { emit filesLockImposed(lockedFiles); } if (changedPaths.isEmpty()) { return; } qCInfo(lcFolderWatcher) << "Detected changes in paths:" << changedPaths; for (const auto &path : changedPaths) { emit pathChanged(path); } } void FolderWatcher::folderAccountCapabilitiesChanged() { _shouldWatchForFileUnlocking = _folder->accountState()->account()->capabilities().filesLockAvailable(); } FolderWatcher::FileLockingInfo FolderWatcher::checkIfFileIsLockOrUnlock(const QString &path) const { FileLockingInfo result; const auto lockFilePatternFound = filePathLockFilePatternMatch(path); if (lockFilePatternFound.isEmpty()) { return result; } const auto lockFilePathWithoutPrefix = QString(path).replace(lockFilePatternFound, 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