From 459ec9c9d7e1ae8a0a95364ef706a1e93fb38b3e Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 7 Apr 2022 10:44:03 +0200 Subject: [PATCH] introduce new jobs to handle lock/unlock of files close #4382 Signed-off-by: Matthieu Gallien --- src/libsync/CMakeLists.txt | 2 + src/libsync/lockfilejobs.cpp | 223 +++++++++++++++++++++++++++++++++++ src/libsync/lockfilejobs.h | 61 ++++++++++ 3 files changed, 286 insertions(+) create mode 100644 src/libsync/lockfilejobs.cpp create mode 100644 src/libsync/lockfilejobs.h diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 0ee2016bbe20a..4d1aacd7b1d85 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -110,6 +110,8 @@ set(libsync_SRCS userstatusconnector.cpp ocsprofileconnector.h ocsprofileconnector.cpp + lockfilejobs.h + lockfilejobs.cpp creds/dummycredentials.h creds/dummycredentials.cpp creds/abstractcredentials.h diff --git a/src/libsync/lockfilejobs.cpp b/src/libsync/lockfilejobs.cpp new file mode 100644 index 0000000000000..2d3dfcb52062f --- /dev/null +++ b/src/libsync/lockfilejobs.cpp @@ -0,0 +1,223 @@ +/* + * Copyright (C) by Matthieu Gallien + * + * 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. + */ + +#include "lockfilejobs.h" + +#include "account.h" +#include "common/syncjournaldb.h" +#include "filesystem.h" + +#include +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcLockFileJob, "nextcloud.sync.networkjob.lockfile", QtInfoMsg) + +LockFileJob::LockFileJob(const AccountPtr account, + SyncJournalDb* const journal, + const QString &path, + const SyncFileItem::LockStatus requestedLockState, + QObject *parent) + : AbstractNetworkJob(account, path, parent) + , _journal(journal) + , _requestedLockState(requestedLockState) +{ +} + +void LockFileJob::start() +{ + qCInfo(lcLockFileJob()) << "start" << path() << _requestedLockState; + + QNetworkRequest request; + request.setRawHeader("X-User-Lock", "1"); + + QByteArray verb; + switch(_requestedLockState) + { + case SyncFileItem::LockStatus::LockedItem: + verb = "LOCK"; + break; + case SyncFileItem::LockStatus::UnlockedItem: + verb = "UNLOCK"; + break; + } + sendRequest(verb, makeDavUrl(path()), request); + + AbstractNetworkJob::start(); +} + +bool LockFileJob::finished() +{ + if (reply()->error() != QNetworkReply::NoError) { + qCInfo(lcLockFileJob()) << "finished with error" << reply()->error() << reply()->errorString(); + const auto httpErrorCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (httpErrorCode == LOCKED_HTTP_ERROR_CODE) { + const auto record = handleReply(); + if (static_cast(record._lockOwnerType) == SyncFileItem::LockOwnerType::UserLock) { + Q_EMIT finishedWithError(httpErrorCode, {}, record._lockOwnerDisplayName); + } else { + Q_EMIT finishedWithError(httpErrorCode, {}, record._lockEditorApp); + } + } else if (httpErrorCode == PRECONDITION_FAILED_ERROR_CODE) { + const auto record = handleReply(); + if (_requestedLockState == SyncFileItem::LockStatus::UnlockedItem && !record._locked) { + Q_EMIT finishedWithoutError(); + } else { + Q_EMIT finishedWithError(httpErrorCode, reply()->errorString(), {}); + } + } else { + Q_EMIT finishedWithError(httpErrorCode, reply()->errorString(), {}); + } + } else { + qCInfo(lcLockFileJob()) << "success" << path(); + handleReply(); + Q_EMIT finishedWithoutError(); + } + return true; +} + +void LockFileJob::setFileRecordLocked(SyncJournalFileRecord &record) const +{ + record._locked = (_lockStatus == SyncFileItem::LockStatus::LockedItem); + record._lockOwnerType = static_cast(_lockOwnerType); + record._lockOwnerDisplayName = _userDisplayName; + record._lockOwnerId = _userId; + record._lockEditorApp = _editorName; + record._lockTime = _lockTime; + record._lockTimeout = _lockTimeout; +} + +void LockFileJob::resetState() +{ + _lockStatus = SyncFileItem::LockStatus::UnlockedItem; + _lockOwnerType = SyncFileItem::LockOwnerType::UserLock; + _userDisplayName.clear(); + _editorName.clear(); + _userId.clear(); + _lockTime = 0; + _lockTimeout = 0; +} + +SyncJournalFileRecord LockFileJob::handleReply() +{ + const auto xml = reply()->readAll(); + + QXmlStreamReader reader(xml); + + resetState(); + + while (!reader.atEnd()) { + const auto type = reader.readNext(); + const auto name = reader.name().toString(); + + switch (type) { + case QXmlStreamReader::TokenType::NoToken: + case QXmlStreamReader::TokenType::Invalid: + case QXmlStreamReader::TokenType::DTD: + case QXmlStreamReader::TokenType::EntityReference: + case QXmlStreamReader::TokenType::ProcessingInstruction: + case QXmlStreamReader::TokenType::Comment: + case QXmlStreamReader::TokenType::StartDocument: + case QXmlStreamReader::TokenType::Characters: + case QXmlStreamReader::TokenType::EndDocument: + case QXmlStreamReader::TokenType::EndElement: + break; + case QXmlStreamReader::TokenType::StartElement: + decodeStartElement(name, reader); + break; + } + } + + SyncJournalFileRecord record; + + if (_lockStatus == SyncFileItem::LockStatus::LockedItem) { + if (_lockOwnerType == SyncFileItem::LockOwnerType::UserLock && _userDisplayName.isEmpty()) { + return record; + } + + if (_lockOwnerType == SyncFileItem::LockOwnerType::AppLock && _editorName.isEmpty()) { + return record; + } + + if (_userId.isEmpty()) { + return record; + } + + if (_lockTime <= 0) { + return record; + } + + if (_lockTimeout <= 0) { + return record; + } + } + + const auto relativePath = path().mid(1); + if (_journal->getFileRecord(relativePath, &record) && record.isValid()) { + setFileRecordLocked(record); + if (_lockOwnerType != SyncFileItem::LockOwnerType::UserLock || + _userId != account()->davUser()) { + FileSystem::setFileReadOnly(relativePath, true); + } + _journal->setFileRecord(record); + _journal->commit("lock file job"); + } + + return record; +} + +void LockFileJob::decodeStartElement(const QString &name, + QXmlStreamReader &reader) +{ + if (name == QStringLiteral("lock")) { + const auto valueText = reader.readElementText(); + if (!valueText.isEmpty()) { + bool isValid = false; + const auto convertedValue = valueText.toInt(&isValid); + if (isValid) { + _lockStatus = static_cast(convertedValue); + } + } + } else if (name == QStringLiteral("lock-owner-type")) { + const auto valueText = reader.readElementText(); + bool isValid = false; + const auto convertedValue = valueText.toInt(&isValid); + if (isValid) { + _lockOwnerType = static_cast(convertedValue); + } + } else if (name == QStringLiteral("lock-owner-displayname")) { + _userDisplayName = reader.readElementText(); + } else if (name == QStringLiteral("lock-owner")) { + _userId = reader.readElementText(); + } else if (name == QStringLiteral("lock-time")) { + const auto valueText = reader.readElementText(); + bool isValid = false; + const auto convertedValue = valueText.toLongLong(&isValid); + if (isValid) { + _lockTime = convertedValue; + } + } else if (name == QStringLiteral("lock-timeout")) { + const auto valueText = reader.readElementText(); + bool isValid = false; + const auto convertedValue = valueText.toLongLong(&isValid); + if (isValid) { + _lockTimeout = convertedValue; + } + } else if (name == QStringLiteral("lock-owner-editor")) { + _editorName = reader.readElementText(); + } +} + +} diff --git a/src/libsync/lockfilejobs.h b/src/libsync/lockfilejobs.h new file mode 100644 index 0000000000000..523a1e9b31613 --- /dev/null +++ b/src/libsync/lockfilejobs.h @@ -0,0 +1,61 @@ +#ifndef LOCKFILEJOBS_H +#define LOCKFILEJOBS_H + +#include "abstractnetworkjob.h" + +#include "syncfileitem.h" + +class QXmlStreamReader; + +namespace OCC { + +class SyncJournalDb; + +class OWNCLOUDSYNC_EXPORT LockFileJob : public AbstractNetworkJob +{ + Q_OBJECT + +public: + static constexpr auto LOCKED_HTTP_ERROR_CODE = 423; + static constexpr auto PRECONDITION_FAILED_ERROR_CODE = 412; + + explicit LockFileJob(const AccountPtr account, + SyncJournalDb* const journal, + const QString &path, + const SyncFileItem::LockStatus requestedLockState, + QObject *parent = nullptr); + void start() override; + +signals: + void finishedWithError(int httpErrorCode, + const QString &errorString, + const QString &lockOwnerName); + void finishedWithoutError(); + +private: + bool finished() override; + + void setFileRecordLocked(SyncJournalFileRecord &record) const; + + SyncJournalFileRecord handleReply(); + + void resetState(); + + void decodeStartElement(const QString &name, + QXmlStreamReader &reader); + + SyncJournalDb* _journal = nullptr; + SyncFileItem::LockStatus _requestedLockState = SyncFileItem::LockStatus::LockedItem; + + SyncFileItem::LockStatus _lockStatus = SyncFileItem::LockStatus::UnlockedItem; + SyncFileItem::LockOwnerType _lockOwnerType = SyncFileItem::LockOwnerType::UserLock; + QString _userDisplayName; + QString _editorName; + QString _userId; + qint64 _lockTime = 0; + qint64 _lockTimeout = 0; +}; + +} + +#endif // LOCKFILEJOBS_H