Skip to content

Commit

Permalink
Detect office files for locking on new upload. Notify FolderWatcher.
Browse files Browse the repository at this point in the history
Signed-off-by: alex-z <blackslayer4@gmail.com>
  • Loading branch information
allexzander authored and mgallien committed Apr 23, 2024
1 parent 907eda4 commit f33f396
Show file tree
Hide file tree
Showing 9 changed files with 329 additions and 102 deletions.
1 change: 1 addition & 0 deletions src/gui/folder.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
109 changes: 20 additions & 89 deletions src/gui/folderwatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,34 +41,7 @@

namespace
{
const std::array<const char *, 2> 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 {
Expand Down Expand Up @@ -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

Check warning on line 172 in src/gui/folderwatcher.cpp

View workflow job for this annotation

GitHub Actions / build

src/gui/folderwatcher.cpp:172:20 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
return _lockChangeDebouncingTimer.interval();
}

void FolderWatcher::changeDetected(const QString &path)

Check warning on line 177 in src/gui/folderwatcher.cpp

View workflow job for this annotation

GitHub Actions / build

src/gui/folderwatcher.cpp:177:21 [readability-convert-member-functions-to-static]

method 'changeDetected' can be made static
{
QFileInfo fileInfo(path);
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
18 changes: 5 additions & 13 deletions src/gui/folderwatcher.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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. */
Expand All @@ -101,8 +100,6 @@ class FolderWatcher : public QObject
*/
void filesLockImposed(const QSet<QString> &files);

void lockFilesFound(const QSet<QString> &files);

void lockedFilesFound(const QSet<QString> &files);

/**
Expand Down Expand Up @@ -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;

Expand Down
120 changes: 120 additions & 0 deletions src/libsync/filesystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,132 @@
#include <QDirIterator>
#include <QCoreApplication>

#include <array>
#include <string_view>

#ifdef Q_OS_WIN
#include <securitybaseapi.h>
#include <sddl.h>
#endif

namespace
{
constexpr std::array<const char *, 2> lockFilePatterns = {{".~lock.", "~$"}};
constexpr std::array<std::string_view, 8> 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)

Check warning on line 41 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:41:9 [modernize-use-trailing-return-type]

use a trailing return type for this function

Check warning on line 41 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:41:39 [bugprone-easily-swappable-parameters]

2 adjacent parameters of 'findMatchingUnlockedFileInDir' of similar type ('const int &') are easily swapped by mistake
{
QString foundFilePath;

Check warning on line 43 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:43:13 [cppcoreguidelines-init-variables]

variable 'foundFilePath' is not initialized
const QDir dir(dirPath);

Check warning on line 44 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:44:16 [cppcoreguidelines-init-variables]

variable 'dir' is not initialized
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)

Check warning on line 65 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:65:21 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
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;

Check warning on line 73 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:73:13 [cppcoreguidelines-init-variables]

variable 'lockFilePatternFound' is not initialized
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)

Check warning on line 88 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:88:18 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
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)

Check warning on line 95 in src/libsync/filesystem.cpp

View workflow job for this annotation

GitHub Actions / build

src/libsync/filesystem.cpp:95:41 [modernize-use-trailing-return-type]

use a trailing return type for this function
{
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)
{
Expand Down
15 changes: 15 additions & 0 deletions src/libsync/filesystem.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "common/filesystembase.h"

#include <QString>
#include <QStringList>

#include <ctime>
#include <functional>
Expand All @@ -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
Expand Down
Loading

0 comments on commit f33f396

Please sign in to comment.