2011-04-06 13:48:02 +04:00
|
|
|
/*
|
2014-01-13 19:16:19 +04:00
|
|
|
* Copyright (C) by Klaas Freitag <freitag@owncloud.com>
|
2011-04-06 13:48:02 +04:00
|
|
|
*
|
|
|
|
* 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
|
2016-10-25 12:00:07 +03:00
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
|
|
* (at your option) any later version.
|
2011-04-06 13:48:02 +04:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
2011-02-17 02:21:45 +03:00
|
|
|
|
2011-03-16 16:50:34 +03:00
|
|
|
// event masks
|
2014-07-11 02:31:24 +04:00
|
|
|
#include "folderwatcher.h"
|
2012-05-21 18:48:49 +04:00
|
|
|
|
2023-04-07 22:09:06 +03:00
|
|
|
#include "accountstate.h"
|
|
|
|
#include "account.h"
|
|
|
|
#include "capabilities.h"
|
|
|
|
|
2014-01-27 15:31:54 +04:00
|
|
|
#if defined(Q_OS_WIN)
|
2014-07-11 02:31:24 +04:00
|
|
|
#include "folderwatcher_win.h"
|
2014-01-27 15:31:54 +04:00
|
|
|
#elif defined(Q_OS_MAC)
|
2014-07-11 02:31:24 +04:00
|
|
|
#include "folderwatcher_mac.h"
|
2014-04-29 13:02:44 +04:00
|
|
|
#elif defined(Q_OS_UNIX)
|
2014-07-11 02:31:24 +04:00
|
|
|
#include "folderwatcher_linux.h"
|
2014-01-27 15:31:54 +04:00
|
|
|
#endif
|
|
|
|
|
2015-10-02 10:40:44 +03:00
|
|
|
#include "folder.h"
|
2019-06-13 10:59:01 +03:00
|
|
|
#include "filesystem.h"
|
2014-01-27 15:31:54 +04:00
|
|
|
|
2023-08-18 11:58:28 +03:00
|
|
|
#include <QFileInfo>
|
|
|
|
#include <QFlags>
|
|
|
|
#include <QDir>
|
|
|
|
#include <QMutexLocker>
|
|
|
|
#include <QStringList>
|
|
|
|
#include <QTimer>
|
|
|
|
|
|
|
|
#include <array>
|
|
|
|
#include <cstdint>
|
|
|
|
|
2023-04-07 22:09:06 +03:00
|
|
|
namespace
|
|
|
|
{
|
2023-08-18 11:58:28 +03:00
|
|
|
const std::array<const char *, 2> lockFilePatterns = {".~lock.", "~$"};
|
2023-08-18 11:43:50 +03:00
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2023-04-07 22:09:06 +03:00
|
|
|
}
|
|
|
|
|
2014-11-10 00:34:07 +03:00
|
|
|
namespace OCC {
|
2011-02-17 02:21:45 +03:00
|
|
|
|
2017-12-28 22:33:10 +03:00
|
|
|
Q_LOGGING_CATEGORY(lcFolderWatcher, "nextcloud.gui.folderwatcher", QtInfoMsg)
|
2017-05-09 15:24:11 +03:00
|
|
|
|
2018-04-24 10:52:15 +03:00
|
|
|
FolderWatcher::FolderWatcher(Folder *folder)
|
2015-10-02 10:40:44 +03:00
|
|
|
: QObject(folder)
|
|
|
|
, _folder(folder)
|
2011-02-17 02:21:45 +03:00
|
|
|
{
|
2023-04-07 22:09:06 +03:00
|
|
|
if (_folder && _folder->accountState() && _folder->accountState()->account()) {
|
|
|
|
connect(_folder->accountState()->account().data(), &Account::capabilitiesChanged, this, &FolderWatcher::folderAccountCapabilitiesChanged);
|
|
|
|
folderAccountCapabilitiesChanged();
|
|
|
|
}
|
2011-03-21 00:18:38 +03:00
|
|
|
}
|
|
|
|
|
2020-05-25 22:33:24 +03:00
|
|
|
FolderWatcher::~FolderWatcher() = default;
|
2014-01-27 15:31:54 +04:00
|
|
|
|
2018-04-24 10:52:15 +03:00
|
|
|
void FolderWatcher::init(const QString &root)
|
|
|
|
{
|
|
|
|
_d.reset(new FolderWatcherPrivate(this, root));
|
|
|
|
_timer.start();
|
|
|
|
}
|
|
|
|
|
2023-07-02 20:05:37 +03:00
|
|
|
bool FolderWatcher::pathIsIgnored(const QString &path) const
|
2014-01-13 18:23:38 +04:00
|
|
|
{
|
2023-07-02 20:05:37 +03:00
|
|
|
return path.isEmpty();
|
2014-01-13 18:23:38 +04:00
|
|
|
}
|
|
|
|
|
2017-10-09 13:06:57 +03:00
|
|
|
bool FolderWatcher::isReliable() const
|
|
|
|
{
|
|
|
|
return _isReliable;
|
|
|
|
}
|
|
|
|
|
2019-05-09 11:05:49 +03:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-13 10:59:01 +03:00
|
|
|
void FolderWatcher::startNotificatonTest(const QString &path)
|
|
|
|
{
|
2019-07-16 15:23:15 +03:00
|
|
|
#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
|
|
|
|
|
2019-06-13 10:59:01 +03:00
|
|
|
Q_ASSERT(_testNotificationPath.isEmpty());
|
|
|
|
_testNotificationPath = path;
|
|
|
|
|
2019-07-11 14:31:54 +03:00
|
|
|
// 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;
|
2019-06-13 10:59:01 +03:00
|
|
|
if (QFile::exists(path)) {
|
|
|
|
auto mtime = FileSystem::getModTime(path);
|
|
|
|
FileSystem::setModTime(path, mtime + 1);
|
|
|
|
} else {
|
|
|
|
QFile f(path);
|
|
|
|
f.open(QIODevice::WriteOnly | QIODevice::Append);
|
|
|
|
}
|
2023-05-24 17:37:45 +03:00
|
|
|
FileSystem::setFileHidden(path, true);
|
2019-06-13 10:59:01 +03:00
|
|
|
|
2019-07-11 14:31:54 +03:00
|
|
|
QTimer::singleShot(5000, this, [this]() {
|
2019-06-13 10:59:01 +03:00
|
|
|
if (!_testNotificationPath.isEmpty())
|
|
|
|
emit becameUnreliable(tr("The watcher did not receive a test notification."));
|
|
|
|
_testNotificationPath.clear();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-07-11 14:31:54 +03:00
|
|
|
|
2019-03-05 15:20:09 +03:00
|
|
|
int FolderWatcher::testLinuxWatchCount() const
|
|
|
|
{
|
|
|
|
#ifdef Q_OS_LINUX
|
|
|
|
return _d->testWatchCount();
|
|
|
|
#else
|
|
|
|
return -1;
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2014-01-11 23:35:58 +04:00
|
|
|
void FolderWatcher::changeDetected(const QString &path)
|
2011-04-05 14:16:24 +04:00
|
|
|
{
|
2019-05-09 11:05:49 +03:00
|
|
|
QFileInfo fileInfo(path);
|
2014-02-18 03:36:41 +04:00
|
|
|
QStringList paths(path);
|
2019-05-09 11:05:49 +03:00
|
|
|
if (fileInfo.isDir()) {
|
|
|
|
QDir dir(path);
|
|
|
|
appendSubPaths(dir, paths);
|
|
|
|
}
|
2014-02-18 03:36:41 +04:00
|
|
|
changeDetected(paths);
|
|
|
|
}
|
|
|
|
|
|
|
|
void FolderWatcher::changeDetected(const QStringList &paths)
|
|
|
|
{
|
|
|
|
// TODO: this shortcut doesn't look very reliable:
|
|
|
|
// - why is the timeout only 1 second?
|
2015-10-05 07:21:19 +03:00
|
|
|
// - 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?
|
2014-02-18 03:36:41 +04:00
|
|
|
|
2014-01-13 18:23:38 +04:00
|
|
|
// Check if the same path was reported within the last second.
|
2023-08-17 13:14:41 +03:00
|
|
|
const auto pathsSet = paths.toSet();
|
2014-02-18 03:36:41 +04:00
|
|
|
if (pathsSet == _lastPaths && _timer.elapsed() < 1000) {
|
2014-01-13 18:23:38 +04:00
|
|
|
// the same path was reported within the last second. Skip.
|
|
|
|
return;
|
|
|
|
}
|
2014-02-18 03:36:41 +04:00
|
|
|
_lastPaths = pathsSet;
|
2014-01-13 18:23:38 +04:00
|
|
|
_timer.restart();
|
|
|
|
|
2014-11-07 12:53:41 +03:00
|
|
|
QSet<QString> changedPaths;
|
2023-04-07 22:09:06 +03:00
|
|
|
QSet<QString> unlockedFiles;
|
2023-08-28 18:07:27 +03:00
|
|
|
QSet<QString> lockedFiles;
|
2014-02-18 03:36:41 +04:00
|
|
|
|
2023-08-17 13:14:41 +03:00
|
|
|
for (const auto &path : paths) {
|
2019-06-13 10:59:01 +03:00
|
|
|
if (!_testNotificationPath.isEmpty()
|
|
|
|
&& Utility::fileNamesEqual(path, _testNotificationPath)) {
|
|
|
|
_testNotificationPath.clear();
|
|
|
|
}
|
2023-04-07 22:09:06 +03:00
|
|
|
|
2023-08-28 18:07:27 +03:00
|
|
|
const auto checkResult = checkIfFileIsLockOrUnlock(path);
|
2023-04-07 22:09:06 +03:00
|
|
|
if (_shouldWatchForFileUnlocking) {
|
2023-08-28 18:07:27 +03:00
|
|
|
if (checkResult.type == FileLockingInfo::Type::Unlocked && !checkResult.path.isEmpty()) {
|
|
|
|
unlockedFiles.insert(checkResult.path);
|
2023-04-07 22:09:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
qCDebug(lcFolderWatcher) << "Unlocked files:" << unlockedFiles.values();
|
|
|
|
}
|
|
|
|
|
2023-08-28 18:07:27 +03:00
|
|
|
if (checkResult.type == FileLockingInfo::Type::Locked && !checkResult.path.isEmpty()) {
|
|
|
|
lockedFiles.insert(checkResult.path);
|
|
|
|
}
|
|
|
|
|
|
|
|
qCDebug(lcFolderWatcher) << "Locked files:" << lockedFiles.values();
|
|
|
|
|
2023-08-17 13:14:41 +03:00
|
|
|
// ------- handle ignores:
|
2014-02-18 03:36:41 +04:00
|
|
|
if (pathIsIgnored(path)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2014-11-07 12:53:41 +03:00
|
|
|
changedPaths.insert(path);
|
2014-02-18 03:36:41 +04:00
|
|
|
}
|
2023-04-07 22:09:06 +03:00
|
|
|
|
|
|
|
if (!unlockedFiles.isEmpty()) {
|
|
|
|
emit filesLockReleased(unlockedFiles);
|
|
|
|
}
|
|
|
|
|
2023-08-28 18:07:27 +03:00
|
|
|
if (!lockedFiles.isEmpty()) {
|
|
|
|
emit filesLockImposed(lockedFiles);
|
|
|
|
}
|
|
|
|
|
2014-11-07 12:53:41 +03:00
|
|
|
if (changedPaths.isEmpty()) {
|
2014-01-13 18:23:38 +04:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2017-03-30 14:46:20 +03:00
|
|
|
qCInfo(lcFolderWatcher) << "Detected changes in paths:" << changedPaths;
|
2023-08-17 13:14:41 +03:00
|
|
|
for (const auto &path : changedPaths) {
|
2014-11-07 12:53:41 +03:00
|
|
|
emit pathChanged(path);
|
2014-02-18 03:36:41 +04:00
|
|
|
}
|
2012-12-05 21:30:40 +04:00
|
|
|
}
|
|
|
|
|
2023-04-07 22:09:06 +03:00
|
|
|
void FolderWatcher::folderAccountCapabilitiesChanged()
|
|
|
|
{
|
|
|
|
_shouldWatchForFileUnlocking = _folder->accountState()->account()->capabilities().filesLockAvailable();
|
|
|
|
}
|
|
|
|
|
2023-08-28 18:07:27 +03:00
|
|
|
FolderWatcher::FileLockingInfo FolderWatcher::checkIfFileIsLockOrUnlock(const QString &path) const
|
2023-04-07 22:09:06 +03:00
|
|
|
{
|
2023-08-28 18:07:27 +03:00
|
|
|
FileLockingInfo result;
|
2023-08-18 11:43:50 +03:00
|
|
|
const auto lockFilePatternFound = filePathLockFilePatternMatch(path);
|
2023-04-07 22:09:06 +03:00
|
|
|
|
2023-08-28 18:07:27 +03:00
|
|
|
if (lockFilePatternFound.isEmpty()) {
|
|
|
|
return result;
|
2023-04-07 22:09:06 +03:00
|
|
|
}
|
|
|
|
|
2023-08-18 11:43:50 +03:00
|
|
|
const auto lockFilePathWithoutPrefix = QString(path).replace(lockFilePatternFound, QStringLiteral(""));
|
|
|
|
auto lockFilePathWithoutPrefixSplit = lockFilePathWithoutPrefix.split(QLatin1Char('.'));
|
2023-04-07 22:09:06 +03:00
|
|
|
|
|
|
|
if (lockFilePathWithoutPrefixSplit.size() < 2) {
|
2023-08-28 18:07:27 +03:00
|
|
|
return result;
|
2023-04-07 22:09:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
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));
|
2023-08-18 11:43:50 +03:00
|
|
|
const auto lockFilePathWithoutPrefixNew = lockFilePathWithoutPrefixSplit.join(QLatin1Char('.'));
|
2023-08-28 18:07:27 +03:00
|
|
|
|
2023-08-18 11:43:50 +03:00
|
|
|
qCDebug(lcFolderWatcher) << "Assumed locked/unlocked file path" << lockFilePathWithoutPrefixNew << "Going to try to find matching file";
|
|
|
|
auto splitFilePath = lockFilePathWithoutPrefixNew.split(QLatin1Char('/'));
|
2023-08-28 18:07:27 +03:00
|
|
|
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);
|
2023-04-07 22:09:06 +03:00
|
|
|
}
|
|
|
|
|
2023-08-28 18:07:27 +03:00
|
|
|
if (result.path.isEmpty() || !QFile::exists(result.path)) {
|
|
|
|
result.path.clear();
|
|
|
|
return result;
|
2023-04-07 22:09:06 +03:00
|
|
|
}
|
2023-08-28 18:07:27 +03:00
|
|
|
result.type = QFile::exists(path) ? FileLockingInfo::Type::Locked : FileLockingInfo::Type::Unlocked;
|
|
|
|
return result;
|
2023-04-07 22:09:06 +03:00
|
|
|
}
|
|
|
|
|
2023-08-28 18:07:27 +03:00
|
|
|
QString FolderWatcher::findMatchingUnlockedFileInDir(const QString &dirPath, const QString &lockFileName) const
|
2023-04-07 22:09:06 +03:00
|
|
|
{
|
|
|
|
QString foundFilePath;
|
|
|
|
const QDir dir(dirPath);
|
2023-08-18 11:43:50 +03:00
|
|
|
const auto entryList = dir.entryInfoList(QDir::Files);
|
|
|
|
for (const auto &candidateUnlockedFileInfo : entryList) {
|
2023-04-07 22:09:06 +03:00
|
|
|
if (candidateUnlockedFileInfo.fileName().contains(lockFileName)) {
|
|
|
|
foundFilePath = candidateUnlockedFileInfo.absoluteFilePath();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return foundFilePath;
|
|
|
|
}
|
|
|
|
|
2014-11-10 00:34:07 +03:00
|
|
|
} // namespace OCC
|