From dfacf7af03bcbda02fc09feab0709bd592920fb0 Mon Sep 17 00:00:00 2001 From: alex-z Date: Thu, 30 Nov 2023 17:34:09 +0100 Subject: [PATCH] Added error reporting for E2EE issues. Signed-off-by: alex-z --- src/gui/tray/NCBusyIndicator.qml | 2 +- src/libsync/CMakeLists.txt | 7 +- src/libsync/account.cpp | 10 +- src/libsync/account.h | 4 +- src/libsync/clientsideencryption.cpp | 9 + src/libsync/clientstatusreporting.cpp | 408 +----------------- src/libsync/clientstatusreporting.h | 83 +--- src/libsync/clientstatusreportingcommon.cpp | 63 +++ src/libsync/clientstatusreportingcommon.h | 38 ++ src/libsync/clientstatusreportingdatabase.cpp | 280 ++++++++++++ src/libsync/clientstatusreportingdatabase.h | 68 +++ src/libsync/clientstatusreportingnetwork.cpp | 198 +++++++++ src/libsync/clientstatusreportingnetwork.h | 68 +++ src/libsync/clientstatusreportingrecord.cpp | 24 -- src/libsync/clientstatusreportingrecord.h | 5 +- src/libsync/discovery.cpp | 4 +- src/libsync/networkjobs.h | 7 + src/libsync/owncloudpropagator.cpp | 27 +- src/libsync/owncloudpropagator_p.h | 4 +- src/libsync/propagatedownload.cpp | 4 +- src/libsync/syncengine.cpp | 6 +- src/libsync/vfs/cfapi/vfs_cfapi.cpp | 2 +- test/testclientstatusreporting.cpp | 60 +-- 23 files changed, 835 insertions(+), 546 deletions(-) create mode 100644 src/libsync/clientstatusreportingcommon.cpp create mode 100644 src/libsync/clientstatusreportingcommon.h create mode 100644 src/libsync/clientstatusreportingdatabase.cpp create mode 100644 src/libsync/clientstatusreportingdatabase.h create mode 100644 src/libsync/clientstatusreportingnetwork.cpp create mode 100644 src/libsync/clientstatusreportingnetwork.h delete mode 100644 src/libsync/clientstatusreportingrecord.cpp diff --git a/src/gui/tray/NCBusyIndicator.qml b/src/gui/tray/NCBusyIndicator.qml index ddead28d4b07d..973d3dee1ed62 100644 --- a/src/gui/tray/NCBusyIndicator.qml +++ b/src/gui/tray/NCBusyIndicator.qml @@ -42,7 +42,7 @@ BusyIndicator { RotationAnimator { target: contentImage - running: false + running: root.running onRunningChanged: contentImage.rotation = 0 from: 0 to: 360 diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index bd7931a8670d3..cd124bd3b653b 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -26,8 +26,13 @@ set(libsync_SRCS clientproxy.cpp clientstatusreporting.h clientstatusreporting.cpp + clientstatusreportingcommon.h + clientstatusreportingcommon.cpp + clientstatusreportingdatabase.h + clientstatusreportingdatabase.cpp + clientstatusreportingnetwork.h + clientstatusreportingnetwork.cpp clientstatusreportingrecord.h - clientstatusreportingrecord.cpp cookiejar.h cookiejar.cpp discovery.h diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 1d27a74ffaca2..bea76c53b9069 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -286,19 +286,17 @@ void Account::setPushNotificationsReconnectInterval(int interval) void Account::trySetupClientStatusReporting() { - if (_capabilities.isClientStatusReportingEnabled()) { - if (!_clientStatusReporting) { - _clientStatusReporting.reset(new ClientStatusReporting(this)); - } + if (!_capabilities.isClientStatusReportingEnabled()) { + _clientStatusReporting.reset(); return; } if (!_clientStatusReporting) { - _clientStatusReporting.reset(); + _clientStatusReporting = std::make_unique(this); } } -void Account::reportClientStatus(const ClientStatusReporting::Status status) +void Account::reportClientStatus(const ClientStatusReportingStatus status) const { if (_clientStatusReporting) { _clientStatusReporting->reportClientStatus(status); diff --git a/src/libsync/account.h b/src/libsync/account.h index 1c51bb7a0c6b5..e118b53fb2f25 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -308,7 +308,7 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject void trySetupClientStatusReporting(); - void reportClientStatus(const ClientStatusReporting::Status status); + void reportClientStatus(const ClientStatusReportingStatus status) const; [[nodiscard]] std::shared_ptr userStatusConnector() const; @@ -444,7 +444,7 @@ private slots: PushNotifications *_pushNotifications = nullptr; - QScopedPointer _clientStatusReporting; + std::unique_ptr _clientStatusReporting; std::shared_ptr _userStatusConnector; diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 3278c450dfeba..dd9263b024d62 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1262,6 +1262,7 @@ bool ClientSideEncryption::sensitiveDataRemaining() const void ClientSideEncryption::failedToInitialize(const AccountPtr &account) { forgetSensitiveData(account); + account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); Q_EMIT initializationFinished(); } @@ -1775,6 +1776,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) if (metadataKeys.isEmpty()) { qCDebug(lcCse()) << "Could not migrate. No metadata keys found!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } @@ -1787,6 +1789,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) if (_metadataKey.isEmpty()) { qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } @@ -1861,6 +1864,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } else { _metadataKey.clear(); _files.clear(); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } } @@ -1899,6 +1903,7 @@ QByteArray FolderMetadata::decryptData(const QByteArray &data) const if (decryptResult.isEmpty()) { qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } return QByteArray::fromBase64(decryptResult); @@ -1916,6 +1921,7 @@ QByteArray FolderMetadata::decryptDataUsingKey(const QByteArray &data, if (decryptResult.isEmpty()) { qCDebug(lcCse()) << "ERROR. Could not decrypt"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } @@ -1979,6 +1985,7 @@ QByteArray FolderMetadata::encryptedMetadata() const { if (_metadataKey.isEmpty()) { qCDebug(lcCse) << "Metadata generation failed! Empty metadata key!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } const auto version = _account->capabilities().clientSideEncryptionVersion(); @@ -2000,6 +2007,7 @@ QByteArray FolderMetadata::encryptedMetadata() const { QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKey); if (encryptedEncrypted.isEmpty()) { + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); qCDebug(lcCse) << "Metadata generation failed!"; } QJsonObject file; @@ -2087,6 +2095,7 @@ bool FolderMetadata::moveFromFileDropToFiles() if (decryptedKey.isEmpty() || decryptedAuthenticationTag.isEmpty() || decryptedInitializationVector.isEmpty()) { qCDebug(lcCseMetadata) << "failed to decrypt filedrop entry" << it.key(); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); continue; } diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index 617405047c5ec..682ff206e1c31 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -14,427 +14,59 @@ #include "clientstatusreporting.h" #include "account.h" +#include "clientstatusreportingdatabase.h" +#include "clientstatusreportingnetwork.h" #include "clientstatusreportingrecord.h" -#include -#include "common/c_jhash.h" -#include - -namespace -{ -constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; -constexpr auto statusNamesHash = "statusNamesHash"; - -constexpr auto statusReportCategoryE2eErrors = "e2e_errors"; -constexpr auto statusReportCategoryProblems = "problems"; -constexpr auto statusReportCategorySyncConflicts = "sync_conflicts"; -constexpr auto statusReportCategoryVirus = "virus_detected"; -} namespace OCC { Q_LOGGING_CATEGORY(lcClientStatusReporting, "nextcloud.sync.clientstatusreporting", QtInfoMsg) -ClientStatusReporting::ClientStatusReporting(Account *account, QObject *parent) - : QObject(parent) - , _account(account) -{ - init(); -} - -ClientStatusReporting::~ClientStatusReporting() +ClientStatusReporting::ClientStatusReporting(Account *account) { - if (_database.isOpen()) { - _database.close(); + for (int i = 0; i < static_cast(ClientStatusReportingStatus::Count); ++i) { + const auto statusString = clientStatusstatusStringFromNumber(static_cast(i)); + _statusStrings[i] = statusString; } -} -void ClientStatusReporting::init() -{ - Q_ASSERT(!_isInitialized); - if (_isInitialized) { - qCDebug(lcClientStatusReporting) << "Double call to init"; + if (_statusStrings.size() < static_cast(ClientStatusReportingStatus::Count)) { return; } - for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) { - const auto statusString = statusStringFromNumber(static_cast(i)); - _statusNamesAndHashes[i] = {statusString, c_jhash64((uint8_t *)statusString.data(), statusString.size(), 0)}; - } - - const auto dbPath = makeDbPath(); - _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); - _database.setDatabaseName(dbPath); - - if (!_database.open()) { - qCDebug(lcClientStatusReporting) << "Could not setup client reporting, database connection error."; + _database = QSharedPointer::create(account); + if (!_database->isInitialized()) { return; } - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral( - "CREATE TABLE IF NOT EXISTS clientstatusreporting(" - "name VARCHAR(4096) PRIMARY KEY," - "status INTEGER(8)," - "count INTEGER," - "lastOccurrence INTEGER(8))")); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not setup client clientstatusreporting table:" << query.lastError().text(); + _reporter = std::make_unique(account, _database); + if (!_reporter->isInitialized()) { return; } - if (!query.prepare(QStringLiteral("CREATE INDEX IF NOT EXISTS name ON clientstatusreporting(name);")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not create index on clientstatusreporting table:" << query.lastError().text(); - return; - } - - if (!query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not setup client keyvalue table:" << query.lastError().text(); - return; - } - - // prevent issues in case enum gets changed in future, hash its value and clean the db in case there was a change - QByteArray statusNamesContatenated; - for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) { - statusNamesContatenated += statusStringFromNumber(static_cast(i)); - } - statusNamesContatenated += QByteArray::number(ClientStatusReporting::Status::Count); - const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex(); - const auto statusNamesHashFromDb = getStatusNamesHash(); - - if (statusNamesHashCurrent != statusNamesHashFromDb) { - deleteClientStatusReportingRecords(); - setStatusNamesHash(statusNamesHashCurrent); - } - // - - _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval); - connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer); - _clientStatusReportingSendTimer.start(); - _isInitialized = true; } -QVector ClientStatusReporting::getClientStatusReportingRecords() const -{ - QVector records; - - QMutexLocker locker(&_mutex); - - QSqlQuery query; - if (!query.prepare(QStringLiteral("SELECT * FROM clientstatusreporting")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not get records from clientstatusreporting:" << query.lastError().text(); - return records; - } +ClientStatusReporting::~ClientStatusReporting() = default; - while (query.next()) { - ClientStatusReportingRecord record; - record._status = query.value(query.record().indexOf(QStringLiteral("status"))).toLongLong(); - record._name = query.value(query.record().indexOf(QStringLiteral("name"))).toByteArray(); - record._numOccurences = query.value(query.record().indexOf(QStringLiteral("count"))).toLongLong(); - record._lastOccurence = query.value(query.record().indexOf(QStringLiteral("lastOccurrence"))).toLongLong(); - records.push_back(record); - } - return records; -} - -void ClientStatusReporting::deleteClientStatusReportingRecords() const -{ - QSqlQuery query; - if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not delete records from clientstatusreporting:" << query.lastError().text(); - } -} - -Result ClientStatusReporting::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const -{ - Q_ASSERT(record.isValid()); - if (!record.isValid()) { - qCDebug(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord"; - return {QStringLiteral("Invalid parameter")}; - } - - const auto recordCopy = record; - - QMutexLocker locker(&_mutex); - - QSqlQuery query; - - const auto prepareResult = query.prepare( - QStringLiteral("INSERT OR REPLACE INTO clientstatusreporting (name, status, count, lastOccurrence) VALUES(:name, :status, :count, :lastOccurrence) ON CONFLICT(name) " - "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;")); - query.bindValue(QStringLiteral(":name"), recordCopy._name); - query.bindValue(QStringLiteral(":status"), recordCopy._status); - query.bindValue(QStringLiteral(":count"), 1); - query.bindValue(QStringLiteral(":lastOccurrence"), recordCopy._lastOccurence); - - if (!prepareResult || !query.exec()) { - const auto errorMessage = query.lastError().text(); - qCDebug(lcClientStatusReporting) << "Could not report client status:" << errorMessage; - return errorMessage; - } - - return {}; -} - -void ClientStatusReporting::reportClientStatus(const Status status) const +void ClientStatusReporting::reportClientStatus(const ClientStatusReportingStatus status) const { if (!_isInitialized) { - qCDebug(lcClientStatusReporting) << "Could not report status. Status reporting is not initialized"; return; } - Q_ASSERT(status >= 0 && status < Count); - if (status < 0 || status >= Status::Count) { - qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << status; + + Q_ASSERT(static_cast(status) >= 0 && static_cast(status) < static_cast(ClientStatusReportingStatus::Count)); + if (static_cast(status) < 0 || static_cast(status) >= static_cast(ClientStatusReportingStatus::Count)) { + qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << static_cast(status); return; } ClientStatusReportingRecord record; - record._name = _statusNamesAndHashes[status].first; - record._status = status; + record._name = _statusStrings[static_cast(status)]; + record._status = static_cast(status); record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); - const auto result = setClientStatusReportingRecord(record); + const auto result = _database->setClientStatusReportingRecord(record); if (!result.isValid()) { qCDebug(lcClientStatusReporting) << "Could not report client status:" << result.error(); } } - -void ClientStatusReporting::sendReportToServer() -{ - if (!_isInitialized) { - qCWarning(lcClientStatusReporting) << "Could not send report to server. Status reporting is not initialized"; - return; - } - - const auto lastSentReportTime = getLastSentReportTimestamp(); - if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) { - return; - } - - const auto report = prepareReport(); - if (report.isEmpty()) { - qCDebug(lcClientStatusReporting) << "Failed to generate report. Report is empty."; - return; - } - - const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); - clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); - clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); - connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) { - if (statusCode == 0 || statusCode == 200 || statusCode == 201 || statusCode == 204) { - const auto metaFromJson = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("meta")).toObject(); - const auto codeFromJson = metaFromJson.value(QStringLiteral("statuscode")).toInt(); - if (codeFromJson == 0 || codeFromJson == 200 || codeFromJson == 201 || codeFromJson == 204) { - reportToServerSentSuccessfully(); - return; - } - qCDebug(lcClientStatusReporting) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson; - } - }); - clientStatusReportingJob->start(); -} - -void ClientStatusReporting::reportToServerSentSuccessfully() -{ - deleteClientStatusReportingRecords(); - setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()); -} - -QString ClientStatusReporting::makeDbPath() const -{ - if (!dbPathForTesting.isEmpty()) { - return dbPathForTesting; - } - const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString()); - const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); - - return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); -} - -quint64 ClientStatusReporting::getLastSentReportTimestamp() const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); - query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; - return 0; - } - if (!query.next()) { - qCDebug(lcClientStatusReporting) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text(); - return 0; - } - return query.value(query.record().indexOf(QStringLiteral("value"))).toULongLong(); -} - -void ClientStatusReporting::setStatusNamesHash(const QByteArray &hash) const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); - query.bindValue(QStringLiteral(":key"), statusNamesHash); - query.bindValue(QStringLiteral(":value"), hash); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not set status names hash."; - return; - } -} - -QByteArray ClientStatusReporting::getStatusNamesHash() const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); - query.bindValue(QStringLiteral(":key"), statusNamesHash); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not get status names hash. No such record:" << statusNamesHash; - return {}; - } - if (!query.next()) { - qCDebug(lcClientStatusReporting) << "Could not get status names hash:" << query.lastError().text(); - return {}; - } - return query.value(query.record().indexOf(QStringLiteral("value"))).toByteArray(); -} - -QVariantMap ClientStatusReporting::prepareReport() const -{ - const auto records = getClientStatusReportingRecords(); - if (records.isEmpty()) { - return {}; - } - - QVariantMap report; - report[statusReportCategorySyncConflicts] = QVariantMap{}; - report[statusReportCategoryProblems] = QVariantMap{}; - report[statusReportCategoryVirus] = QVariantMap{}; - report[statusReportCategoryE2eErrors] = QVariantMap{}; - - QVariantMap e2eeErrors; - QVariantMap problems; - QVariantMap syncConflicts; - QVariantMap virusDetectedErrors; - - for (const auto &record : records) { - const auto categoryKey = classifyStatus(static_cast(record._status)); - - if (categoryKey.isEmpty()) { - qCDebug(lcClientStatusReporting) << "Could not classify status:"; - continue; - } - - if (categoryKey == statusReportCategoryE2eErrors) { - const auto initialCount = e2eeErrors[QStringLiteral("count")].toInt(); - e2eeErrors[QStringLiteral("count")] = initialCount + record._numOccurences; - e2eeErrors[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = e2eeErrors; - } else if (categoryKey == statusReportCategoryProblems) { - problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; - report[categoryKey] = problems; - } else if (categoryKey == statusReportCategorySyncConflicts) { - const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); - syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; - syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = syncConflicts; - } else if (categoryKey == statusReportCategoryVirus) { - const auto initialCount = virusDetectedErrors[QStringLiteral("count")].toInt(); - virusDetectedErrors[QStringLiteral("count")] = initialCount + record._numOccurences; - virusDetectedErrors[QStringLiteral("oldest")] = record._lastOccurence; - report[categoryKey] = virusDetectedErrors; - } - } - return report; -} - -void ClientStatusReporting::setLastSentReportTimestamp(const quint64 timestamp) const -{ - QMutexLocker locker(&_mutex); - QSqlQuery query; - const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); - query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); - query.bindValue(QStringLiteral(":value"), timestamp); - if (!prepareResult || !query.exec()) { - qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; - return; - } -} - -QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) -{ - Q_ASSERT(status >= 0 && status < Count); - if (status < 0 || status >= Status::Count) { - qCDebug(lcClientStatusReporting) << "Invalid status:" << status; - return {}; - } - - switch (status) { - case DownloadError_Cannot_Create_File: - return QByteArrayLiteral("DownloadResult.CANNOT_CREATE_FILE"); - case DownloadError_Conflict: - return QByteArrayLiteral("DownloadResult.CONFLICT"); - case DownloadError_ConflictCaseClash: - return QByteArrayLiteral("DownloadResult.CONFLICT_CASECLASH"); - case DownloadError_ConflictInvalidCharacters: - return QByteArrayLiteral("DownloadResult.CONFLICT_INVALID_CHARACTERS"); - case DownloadError_No_Free_Space: - return QByteArrayLiteral("DownloadResult.NO_FREE_SPACE"); - case DownloadError_ServerError: - return QByteArrayLiteral("DownloadResult.SERVER_ERROR"); - case DownloadError_Virtual_File_Hydration_Failure: - return QByteArrayLiteral("DownloadResult.VIRTUAL_FILE_HYDRATION_FAILURE"); - case E2EeError_GeneralError: - return QByteArrayLiteral("E2EeError.General"); - case UploadError_Conflict: - return QByteArrayLiteral("UploadResult.CONFLICT_CASECLASH"); - case UploadError_ConflictInvalidCharacters: - return QByteArrayLiteral("UploadResult.CONFLICT_INVALID_CHARACTERS"); - case UploadError_No_Free_Space: - return QByteArrayLiteral("UploadResult.NO_FREE_SPACE"); - case UploadError_No_Write_Permissions: - return QByteArrayLiteral("UploadResult.NO_WRITE_PERMISSIONS"); - case UploadError_ServerError: - return QByteArrayLiteral("UploadResult.SERVER_ERROR"); - case UploadError_Virus_Detected: - return QByteArrayLiteral("UploadResult.VIRUS_DETECTED"); - case Count: - return {}; - }; - return {}; -} - -QByteArray ClientStatusReporting::classifyStatus(const Status status) -{ - Q_ASSERT(status >= 0 && status < Count); - if (status < 0 || status >= Status::Count) { - qCDebug(lcClientStatusReporting) << "Invalid status:" << status; - return {}; - } - - switch (status) { - case DownloadError_Conflict: - case DownloadError_ConflictCaseClash: - case DownloadError_ConflictInvalidCharacters: - case UploadError_Conflict: - case UploadError_ConflictInvalidCharacters: - return statusReportCategorySyncConflicts; - case DownloadError_Cannot_Create_File: - case DownloadError_No_Free_Space: - case DownloadError_ServerError: - case DownloadError_Virtual_File_Hydration_Failure: - case UploadError_No_Free_Space: - case UploadError_No_Write_Permissions: - case UploadError_ServerError: - return statusReportCategoryProblems; - case UploadError_Virus_Detected: - return statusReportCategoryVirus; - case E2EeError_GeneralError: - return statusReportCategoryE2eErrors; - case Count: - return {}; - }; - return {}; -} -int ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes -quint64 ClientStatusReporting::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours -QString ClientStatusReporting::dbPathForTesting; } diff --git a/src/libsync/clientstatusreporting.h b/src/libsync/clientstatusreporting.h index 70b7508c028b0..54aaf57853363 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -15,95 +15,38 @@ #include "owncloudlib.h" #include +#include "clientstatusreportingcommon.h" + +#include -#include #include #include -#include -#include -#include -#include -#include -#include +#include namespace OCC { class Account; +class ClientStatusReportingDatabase; +class ClientStatusReportingNetwork; struct ClientStatusReportingRecord; -class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject +class OWNCLOUDSYNC_EXPORT ClientStatusReporting { - Q_OBJECT public: - enum Status { - DownloadError_Cannot_Create_File = 0, - DownloadError_Conflict, - DownloadError_ConflictCaseClash, - DownloadError_ConflictInvalidCharacters, - DownloadError_No_Free_Space, - DownloadError_ServerError, - DownloadError_Virtual_File_Hydration_Failure, - E2EeError_GeneralError, - UploadError_Conflict, - UploadError_ConflictInvalidCharacters, - UploadError_No_Free_Space, - UploadError_No_Write_Permissions, - UploadError_ServerError, - UploadError_Virus_Detected, - Count, - }; - - explicit ClientStatusReporting(Account *account, QObject *parent = nullptr); - ~ClientStatusReporting() override; - - static QByteArray statusStringFromNumber(const Status status); + explicit ClientStatusReporting(Account *account); + ~ClientStatusReporting(); private: - void init(); // reporting must happen via Account - void reportClientStatus(const Status status) const; - - [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const; - [[nodiscard]] QVector getClientStatusReportingRecords() const; - void deleteClientStatusReportingRecords() const; - - void setLastSentReportTimestamp(const quint64 timestamp) const; - [[nodiscard]] quint64 getLastSentReportTimestamp() const; - - void setStatusNamesHash(const QByteArray &hash) const; - [[nodiscard]] QByteArray getStatusNamesHash() const; - - [[nodiscard]] QVariantMap prepareReport() const; - void reportToServerSentSuccessfully(); - - [[nodiscard]] QString makeDbPath() const; - -private slots: - void sendReportToServer(); - -private: - static QByteArray classifyStatus(const Status status); - -public: - static int clientStatusReportingTrySendTimerInterval; - static quint64 repordSendIntervalMs; - // this must be set in unit tests on init - static QString dbPathForTesting; - -private: - - Account *_account = nullptr; - - QSqlDatabase _database; + void reportClientStatus(const ClientStatusReportingStatus status) const; bool _isInitialized = false; - QTimer _clientStatusReportingSendTimer; + QHash _statusStrings; - QHash> _statusNamesAndHashes; + QSharedPointer _database; - // inspired by SyncJournalDb - mutable QRecursiveMutex _mutex; + std::unique_ptr _reporter; friend class Account; }; diff --git a/src/libsync/clientstatusreportingcommon.cpp b/src/libsync/clientstatusreportingcommon.cpp new file mode 100644 index 0000000000000..fabbf2b0fbbaf --- /dev/null +++ b/src/libsync/clientstatusreportingcommon.cpp @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreportingcommon.h" +#include + +namespace OCC { +Q_LOGGING_CATEGORY(lcClientStatusReportingCommon, "nextcloud.sync.clientstatusreportingcommon", QtInfoMsg) + +QByteArray clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status) +{ + Q_ASSERT(static_cast(status) >= 0 && static_cast(status) < static_cast(ClientStatusReportingStatus::Count)); + if (static_cast(status) < 0 || static_cast(status) >= static_cast(ClientStatusReportingStatus::Count)) { + qCDebug(lcClientStatusReportingCommon) << "Invalid status:" << static_cast(status); + return {}; + } + + switch (status) { + case ClientStatusReportingStatus::DownloadError_Cannot_Create_File: + return QByteArrayLiteral("DownloadResult.CANNOT_CREATE_FILE"); + case ClientStatusReportingStatus::DownloadError_Conflict: + return QByteArrayLiteral("DownloadResult.CONFLICT"); + case ClientStatusReportingStatus::DownloadError_ConflictCaseClash: + return QByteArrayLiteral("DownloadResult.CONFLICT_CASECLASH"); + case ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters: + return QByteArrayLiteral("DownloadResult.CONFLICT_INVALID_CHARACTERS"); + case ClientStatusReportingStatus::DownloadError_No_Free_Space: + return QByteArrayLiteral("DownloadResult.NO_FREE_SPACE"); + case ClientStatusReportingStatus::DownloadError_ServerError: + return QByteArrayLiteral("DownloadResult.SERVER_ERROR"); + case ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure: + return QByteArrayLiteral("DownloadResult.VIRTUAL_FILE_HYDRATION_FAILURE"); + case ClientStatusReportingStatus::E2EeError_GeneralError: + return QByteArrayLiteral("E2EeError.General"); + case ClientStatusReportingStatus::UploadError_Conflict: + return QByteArrayLiteral("UploadResult.CONFLICT_CASECLASH"); + case ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters: + return QByteArrayLiteral("UploadResult.CONFLICT_INVALID_CHARACTERS"); + case ClientStatusReportingStatus::UploadError_No_Free_Space: + return QByteArrayLiteral("UploadResult.NO_FREE_SPACE"); + case ClientStatusReportingStatus::UploadError_No_Write_Permissions: + return QByteArrayLiteral("UploadResult.NO_WRITE_PERMISSIONS"); + case ClientStatusReportingStatus::UploadError_ServerError: + return QByteArrayLiteral("UploadResult.SERVER_ERROR"); + case ClientStatusReportingStatus::UploadError_Virus_Detected: + return QByteArrayLiteral("UploadResult.VIRUS_DETECTED"); + case ClientStatusReportingStatus::Count: + return {}; + }; + return {}; +} +} diff --git a/src/libsync/clientstatusreportingcommon.h b/src/libsync/clientstatusreportingcommon.h new file mode 100644 index 0000000000000..4e08ca1efa6fd --- /dev/null +++ b/src/libsync/clientstatusreportingcommon.h @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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. + */ +#pragma once + +#include "owncloudlib.h" +#include + +namespace OCC { +enum class ClientStatusReportingStatus { + DownloadError_Cannot_Create_File = 0, + DownloadError_Conflict, + DownloadError_ConflictCaseClash, + DownloadError_ConflictInvalidCharacters, + DownloadError_No_Free_Space, + DownloadError_ServerError, + DownloadError_Virtual_File_Hydration_Failure, + E2EeError_GeneralError, + UploadError_Conflict, + UploadError_ConflictInvalidCharacters, + UploadError_No_Free_Space, + UploadError_No_Write_Permissions, + UploadError_ServerError, + UploadError_Virus_Detected, + Count, +}; +QByteArray OWNCLOUDSYNC_EXPORT clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status); +} diff --git a/src/libsync/clientstatusreportingdatabase.cpp b/src/libsync/clientstatusreportingdatabase.cpp new file mode 100644 index 0000000000000..169df5e13fffe --- /dev/null +++ b/src/libsync/clientstatusreportingdatabase.cpp @@ -0,0 +1,280 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreportingdatabase.h" + +#include "account.h" +#include + +#include +#include +#include + +namespace +{ +constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; +constexpr auto statusNamesHash = "statusNamesHash"; +} + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcClientStatusReportingDatabase, "nextcloud.sync.clientstatusreportingdatabase", QtInfoMsg) + +ClientStatusReportingDatabase::ClientStatusReportingDatabase(const Account *account) +{ + const auto dbPath = makeDbPath(account); + _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); + _database.setDatabaseName(dbPath); + + if (!_database.open()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not setup client reporting, database connection error."; + return; + } + + QSqlQuery query; + const auto prepareResult = + query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS clientstatusreporting(" + "name VARCHAR(4096) PRIMARY KEY," + "status INTEGER(8)," + "count INTEGER," + "lastOccurrence INTEGER(8))")); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not setup client clientstatusreporting table:" << query.lastError().text(); + return; + } + + if (!query.prepare(QStringLiteral("CREATE TABLE IF NOT EXISTS keyvalue(key VARCHAR(4096), value VARCHAR(4096), PRIMARY KEY(key))")) || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not setup client keyvalue table:" << query.lastError().text(); + return; + } + + if (!updateStatusNamesHash()) { + return; + } + + _isInitialized = true; +} + +ClientStatusReportingDatabase::~ClientStatusReportingDatabase() +{ + if (_database.isOpen()) { + _database.close(); + } +} + +QVector ClientStatusReportingDatabase::getClientStatusReportingRecords() const +{ + QVector records; + + QMutexLocker locker(&_mutex); + + QSqlQuery query; + if (!query.prepare(QStringLiteral("SELECT * FROM clientstatusreporting")) || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get records from clientstatusreporting:" << query.lastError().text(); + return records; + } + + while (query.next()) { + ClientStatusReportingRecord record; + record._status = query.value(query.record().indexOf(QStringLiteral("status"))).toLongLong(); + record._name = query.value(query.record().indexOf(QStringLiteral("name"))).toByteArray(); + record._numOccurences = query.value(query.record().indexOf(QStringLiteral("count"))).toLongLong(); + record._lastOccurence = query.value(query.record().indexOf(QStringLiteral("lastOccurrence"))).toLongLong(); + records.push_back(record); + } + return records; +} + +Result ClientStatusReportingDatabase::deleteClientStatusReportingRecords() const +{ + QSqlQuery query; + if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReportingDatabase) << "Could not delete records from clientstatusreporting:" << errorMessage; + return errorMessage; + } + return {}; +} + +Result ClientStatusReportingDatabase::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const +{ + Q_ASSERT(record.isValid()); + if (!record.isValid()) { + qCDebug(lcClientStatusReportingDatabase) << "Failed to set ClientStatusReportingRecord"; + return {QStringLiteral("Invalid parameter")}; + } + + const auto recordCopy = record; + + QMutexLocker locker(&_mutex); + + QSqlQuery query; + + const auto prepareResult = query.prepare( + QStringLiteral("INSERT OR REPLACE INTO clientstatusreporting (name, status, count, lastOccurrence) VALUES(:name, :status, :count, :lastOccurrence) ON CONFLICT(name) " + "DO UPDATE SET count = count + 1, lastOccurrence = :lastOccurrence;")); + query.bindValue(QStringLiteral(":name"), recordCopy._name); + query.bindValue(QStringLiteral(":status"), recordCopy._status); + query.bindValue(QStringLiteral(":count"), 1); + query.bindValue(QStringLiteral(":lastOccurrence"), recordCopy._lastOccurence); + + if (!prepareResult || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReportingDatabase) << "Could not report client status:" << errorMessage; + return errorMessage; + } + + return {}; +} + +QString ClientStatusReportingDatabase::makeDbPath(const Account *account) const +{ + if (!dbPathForTesting.isEmpty()) { + return dbPathForTesting; + } + const auto databaseId = QStringLiteral("%1@%2").arg(account->davUser(), account->url().toString()); + const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); + + return ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); +} + +bool ClientStatusReportingDatabase::updateStatusNamesHash() const +{ + QByteArray statusNamesContatenated; + for (int i = 0; i < static_cast(ClientStatusReportingStatus::Count); ++i) { + statusNamesContatenated += clientStatusstatusStringFromNumber(static_cast(i)); + } + statusNamesContatenated += QByteArray::number(static_cast(ClientStatusReportingStatus::Count)); + const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex(); + const auto statusNamesHashFromDb = getStatusNamesHash(); + + if (statusNamesHashCurrent != statusNamesHashFromDb) { + auto result = deleteClientStatusReportingRecords(); + if (!result.isValid()) { + return false; + } + + result = setStatusNamesHash(statusNamesHashCurrent); + if (!result.isValid()) { + return false; + } + } + return true; +} + +QVector ClientStatusReportingDatabase::getTableColumns(const QString &table) const +{ + QVector columns; + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("PRAGMA table_info('%1');").arg(table)); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could get table columns" << query.lastError().text(); + return columns; + } + while (query.next()) { + columns.append(query.value(1).toByteArray()); + } + return columns; +} + +bool ClientStatusReportingDatabase::addColumn(const QString &tableName, const QString &columnName, const QString &dataType, const bool withIndex) const +{ + const auto columns = getTableColumns(tableName); + const auto latin1ColumnName = columnName.toLatin1(); + if (columns.indexOf(latin1ColumnName) == -1) { + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("ALTER TABLE %1 ADD COLUMN %2 %3;").arg(tableName, columnName, dataType)); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << QStringLiteral("Failed to update table %1 structure: add %2 column").arg(tableName, columnName) << query.lastError().text(); + return false; + } + + if (withIndex) { + const auto prepareResult = query.prepare(QStringLiteral("CREATE INDEX %1_%2 ON %1(%2);").arg(tableName, columnName)); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << QStringLiteral("Failed to update table %1 structure: create index %2 column").arg(tableName, columnName) << query.lastError().text(); + return false; + } + } + } + return true; +} + +quint64 ClientStatusReportingDatabase::getLastSentReportTimestamp() const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); + query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; + return 0; + } + if (!query.next()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get last sent report timestamp from keyvalue table:" << query.lastError().text(); + return 0; + } + return query.value(query.record().indexOf(QStringLiteral("value"))).toULongLong(); +} + +Result ClientStatusReportingDatabase::setStatusNamesHash(const QByteArray &hash) const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); + query.bindValue(QStringLiteral(":key"), statusNamesHash); + query.bindValue(QStringLiteral(":value"), hash); + if (!prepareResult || !query.exec()) { + const auto errorMessage = query.lastError().text(); + qCDebug(lcClientStatusReportingDatabase) << "Could not set status names hash." << errorMessage; + return errorMessage; + } + return {}; +} + +QByteArray ClientStatusReportingDatabase::getStatusNamesHash() const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("SELECT value FROM keyvalue WHERE key = (:key)")); + query.bindValue(QStringLiteral(":key"), statusNamesHash); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get status names hash. No such record:" << statusNamesHash; + return {}; + } + if (!query.next()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not get status names hash:" << query.lastError().text(); + return {}; + } + return query.value(query.record().indexOf(QStringLiteral("value"))).toByteArray(); +} + +bool ClientStatusReportingDatabase::isInitialized() const +{ + return _isInitialized; +} + +void ClientStatusReportingDatabase::setLastSentReportTimestamp(const quint64 timestamp) const +{ + QMutexLocker locker(&_mutex); + QSqlQuery query; + const auto prepareResult = query.prepare(QStringLiteral("INSERT OR REPLACE INTO keyvalue (key, value) VALUES(:key, :value);")); + query.bindValue(QStringLiteral(":key"), lastSentReportTimestamp); + query.bindValue(QStringLiteral(":value"), timestamp); + if (!prepareResult || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; + return; + } +} +QString ClientStatusReportingDatabase::dbPathForTesting; +} diff --git a/src/libsync/clientstatusreportingdatabase.h b/src/libsync/clientstatusreportingdatabase.h new file mode 100644 index 0000000000000..d3ef3c7161de2 --- /dev/null +++ b/src/libsync/clientstatusreportingdatabase.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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. + */ +#pragma once + +#include "owncloudlib.h" +#include +#include "clientstatusreportingcommon.h" +#include "clientstatusreportingrecord.h" + +#include +#include +#include +#include +#include +#include + +namespace OCC { + +class Account; + +class OWNCLOUDSYNC_EXPORT ClientStatusReportingDatabase +{ +public: + explicit ClientStatusReportingDatabase(const Account *account); + ~ClientStatusReportingDatabase(); + + [[nodiscard]] Result setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const; + [[nodiscard]] QVector getClientStatusReportingRecords() const; + [[nodiscard]] Result deleteClientStatusReportingRecords() const; + + void setLastSentReportTimestamp(const quint64 timestamp) const; + [[nodiscard]] quint64 getLastSentReportTimestamp() const; + + [[nodiscard]] Result setStatusNamesHash(const QByteArray &hash) const; + [[nodiscard]] QByteArray getStatusNamesHash() const; + + [[nodiscard]] bool isInitialized() const; + +private: + [[nodiscard]] QString makeDbPath(const Account *account) const; + [[nodiscard]] bool updateStatusNamesHash() const; + [[nodiscard]] QVector getTableColumns(const QString &table) const; + [[nodiscard]]bool addColumn(const QString &tableName, const QString &columnName, const QString &dataType, const bool withIndex = false) const; + +public: + // this must be set in unit tests on init + static QString dbPathForTesting; + +private: + QSqlDatabase _database; + + bool _isInitialized = false; + + // inspired by SyncJournalDb + mutable QRecursiveMutex _mutex; +}; +} diff --git a/src/libsync/clientstatusreportingnetwork.cpp b/src/libsync/clientstatusreportingnetwork.cpp new file mode 100644 index 0000000000000..bee15f37bb564 --- /dev/null +++ b/src/libsync/clientstatusreportingnetwork.cpp @@ -0,0 +1,198 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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 "clientstatusreportingnetwork.h" + +#include "account.h" +#include "clientstatusreportingdatabase.h" +#include "clientstatusreportingrecord.h" +#include + +namespace +{ +constexpr auto statusReportCategoryE2eErrors = "e2e_errors"; +constexpr auto statusReportCategoryProblems = "problems"; +constexpr auto statusReportCategorySyncConflicts = "sync_conflicts"; +constexpr auto statusReportCategoryVirus = "virus_detected"; +} + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcClientStatusReportingNetwork, "nextcloud.sync.clientstatusreportingnetwork", QtInfoMsg) + +ClientStatusReportingNetwork::ClientStatusReportingNetwork(Account *account, const QSharedPointer database, QObject *parent) + : QObject(parent) + , _account(account) + , _database(database) +{ + init(); +} + +ClientStatusReportingNetwork::~ClientStatusReportingNetwork() +{ +} + +void ClientStatusReportingNetwork::init() +{ + Q_ASSERT(!_isInitialized); + if (_isInitialized) { + return; + } + + _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval); + connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReportingNetwork::sendReportToServer); + _clientStatusReportingSendTimer.start(); + + _isInitialized = true; +} + +bool ClientStatusReportingNetwork::isInitialized() const +{ + return _isInitialized; +} + +void ClientStatusReportingNetwork::sendReportToServer() +{ + if (!_isInitialized) { + qCWarning(lcClientStatusReportingNetwork) << "Could not send report to server. Status reporting is not initialized"; + return; + } + + const auto lastSentReportTime = _database->getLastSentReportTimestamp(); + if (QDateTime::currentDateTimeUtc().toMSecsSinceEpoch() - lastSentReportTime < repordSendIntervalMs) { + return; + } + + const auto report = prepareReport(); + if (report.isEmpty()) { + return; + } + + if (!_account) { + return; + } + + const auto clientStatusReportingJob = new JsonApiJob(_account->sharedFromThis(), QStringLiteral("ocs/v2.php/apps/security_guard/diagnostics")); + clientStatusReportingJob->setBody(QJsonDocument::fromVariant(report)); + clientStatusReportingJob->setVerb(SimpleApiJob::Verb::Put); + connect(clientStatusReportingJob, &JsonApiJob::jsonReceived, [this](const QJsonDocument &json, int statusCode) { + const auto isSuccess = statusCode == HttpErrorCodeNone || statusCode == HttpErrorCodeSuccess || statusCode == HttpErrorCodeSuccessCreated + || statusCode == HttpErrorCodeSuccessNoContent; + if (isSuccess) { + const auto metaFromJson = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("meta")).toObject(); + const auto codeFromJson = metaFromJson.value(QStringLiteral("statuscode")).toInt(); + if (codeFromJson == HttpErrorCodeNone || codeFromJson == HttpErrorCodeSuccess || codeFromJson == HttpErrorCodeSuccessCreated + || codeFromJson == HttpErrorCodeSuccessNoContent) { + reportToServerSentSuccessfully(); + return; + } + qCDebug(lcClientStatusReportingNetwork) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson; + } + }); + clientStatusReportingJob->start(); +} + +void ClientStatusReportingNetwork::reportToServerSentSuccessfully() +{ + qCInfo(lcClientStatusReportingNetwork) << "Report sent successfully"; + if (!_database->deleteClientStatusReportingRecords()) { + qCDebug(lcClientStatusReportingNetwork) << "Could not delete records after sending the report"; + } + _database->setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch()); +} + +QVariantMap ClientStatusReportingNetwork::prepareReport() const +{ + const auto records = _database->getClientStatusReportingRecords(); + if (records.isEmpty()) { + return {}; + } + + QVariantMap report; + report[statusReportCategorySyncConflicts] = QVariantMap{}; + report[statusReportCategoryProblems] = QVariantMap{}; + report[statusReportCategoryVirus] = QVariantMap{}; + report[statusReportCategoryE2eErrors] = QVariantMap{}; + + QVariantMap e2eeErrors; + QVariantMap problems; + QVariantMap syncConflicts; + QVariantMap virusDetectedErrors; + + for (const auto &record : records) { + const auto categoryKey = classifyStatus(static_cast(record._status)); + + if (categoryKey.isEmpty()) { + qCDebug(lcClientStatusReportingNetwork) << "Could not classify status:"; + continue; + } + + if (categoryKey == statusReportCategoryE2eErrors) { + const auto initialCount = e2eeErrors[QStringLiteral("count")].toInt(); + e2eeErrors[QStringLiteral("count")] = initialCount + record._numOccurences; + e2eeErrors[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = e2eeErrors; + } else if (categoryKey == statusReportCategoryProblems) { + problems[record._name] = QVariantMap{{QStringLiteral("count"), record._numOccurences}, {QStringLiteral("oldest"), record._lastOccurence}}; + report[categoryKey] = problems; + } else if (categoryKey == statusReportCategorySyncConflicts) { + const auto initialCount = syncConflicts[QStringLiteral("count")].toInt(); + syncConflicts[QStringLiteral("count")] = initialCount + record._numOccurences; + syncConflicts[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = syncConflicts; + } else if (categoryKey == statusReportCategoryVirus) { + const auto initialCount = virusDetectedErrors[QStringLiteral("count")].toInt(); + virusDetectedErrors[QStringLiteral("count")] = initialCount + record._numOccurences; + virusDetectedErrors[QStringLiteral("oldest")] = record._lastOccurence; + report[categoryKey] = virusDetectedErrors; + } + } + return report; +} + +QByteArray ClientStatusReportingNetwork::classifyStatus(const ClientStatusReportingStatus status) +{ + Q_ASSERT(static_cast(status) >= 0 && static_cast(status) < static_cast(ClientStatusReportingStatus::Count)); + if (static_cast(status) < 0 || static_cast(status) >= static_cast(ClientStatusReportingStatus::Count)) { + qCDebug(lcClientStatusReportingNetwork) << "Invalid status:" << static_cast(status); + return {}; + } + + switch (status) { + case ClientStatusReportingStatus::DownloadError_Conflict: + case ClientStatusReportingStatus::DownloadError_ConflictCaseClash: + case ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters: + case ClientStatusReportingStatus::UploadError_Conflict: + case ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters: + return statusReportCategorySyncConflicts; + case ClientStatusReportingStatus::DownloadError_Cannot_Create_File: + case ClientStatusReportingStatus::DownloadError_No_Free_Space: + case ClientStatusReportingStatus::DownloadError_ServerError: + case ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure: + case ClientStatusReportingStatus::UploadError_No_Free_Space: + case ClientStatusReportingStatus::UploadError_No_Write_Permissions: + case ClientStatusReportingStatus::UploadError_ServerError: + return statusReportCategoryProblems; + case ClientStatusReportingStatus::UploadError_Virus_Detected: + return statusReportCategoryVirus; + case ClientStatusReportingStatus::E2EeError_GeneralError: + return statusReportCategoryE2eErrors; + case ClientStatusReportingStatus::Count: + return {}; + }; + return {}; +} + +int ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes +quint64 ClientStatusReportingNetwork::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours +} diff --git a/src/libsync/clientstatusreportingnetwork.h b/src/libsync/clientstatusreportingnetwork.h new file mode 100644 index 0000000000000..0b4d10c65c520 --- /dev/null +++ b/src/libsync/clientstatusreportingnetwork.h @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 by Oleksandr Zolotov + * + * 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. + */ +#pragma once + +#include "owncloudlib.h" +#include +#include "clientstatusreportingcommon.h" + +#include +#include +#include +#include +#include +#include + +namespace OCC { + +class Account; +class ClientStatusReportingDatabase; +struct ClientStatusReportingRecord; + +class OWNCLOUDSYNC_EXPORT ClientStatusReportingNetwork : public QObject +{ + Q_OBJECT +public: + explicit ClientStatusReportingNetwork(Account *account, const QSharedPointer database, QObject *parent = nullptr); + ~ClientStatusReportingNetwork() override; + +private: + void init(); + + [[nodiscard]] QVariantMap prepareReport() const; + void reportToServerSentSuccessfully(); + +private slots: + void sendReportToServer(); + +public: + [[nodiscard]] bool isInitialized() const; + + static QByteArray classifyStatus(const ClientStatusReportingStatus status); + + static int clientStatusReportingTrySendTimerInterval; + static quint64 repordSendIntervalMs; + // this must be set in unit tests on init + static QString dbPathForTesting; + +private: + Account *_account = nullptr; + + QSharedPointer _database; + + bool _isInitialized = false; + + QTimer _clientStatusReportingSendTimer; +}; +} diff --git a/src/libsync/clientstatusreportingrecord.cpp b/src/libsync/clientstatusreportingrecord.cpp deleted file mode 100644 index 4d795de64b3eb..0000000000000 --- a/src/libsync/clientstatusreportingrecord.cpp +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (C) 2023 by Oleksandr Zolotov - * - * 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 "clientstatusreportingrecord.h" - -namespace OCC -{ - -bool ClientStatusReportingRecord::isValid() const -{ - return _status >= 0 && !_name.isEmpty() && _lastOccurence > 0; -} -} diff --git a/src/libsync/clientstatusreportingrecord.h b/src/libsync/clientstatusreportingrecord.h index 6595177ec0995..94f190a0ee210 100644 --- a/src/libsync/clientstatusreportingrecord.h +++ b/src/libsync/clientstatusreportingrecord.h @@ -30,6 +30,9 @@ struct OWNCLOUDSYNC_EXPORT ClientStatusReportingRecord { quint64 _numOccurences = 1; quint64 _lastOccurence = 0; - [[nodiscard]] bool isValid() const; + [[nodiscard]] inline bool isValid() const + { + return _status >= 0 && !_name.isEmpty() && _lastOccurence > 0; + } }; } diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index c316dd518aa40..dc56ec602b85e 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -1709,13 +1709,13 @@ bool ProcessDirectoryJob::checkPermissions(const OCC::SyncFileItemPtr &item) // No permissions set return true; } else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) { - _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions); + _discoveryData->_account->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Write_Permissions); qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder"); return false; } else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) { - _discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions); + _discoveryData->_account->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Write_Permissions); qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_errorString = tr("Not allowed because you don't have permission to add files in that folder"); diff --git a/src/libsync/networkjobs.h b/src/libsync/networkjobs.h index 4fc397a7a4129..b299bdb0aa387 100644 --- a/src/libsync/networkjobs.h +++ b/src/libsync/networkjobs.h @@ -30,6 +30,13 @@ class QDomDocument; namespace OCC { +constexpr auto HttpErrorCodeNone = 0; +constexpr auto HttpErrorCodeSuccess = 200; +constexpr auto HttpErrorCodeSuccessCreated = 201; +constexpr auto HttpErrorCodeSuccessNoContent = 204; +constexpr auto HttpErrorCodeBadRequest = 400; +constexpr auto HttpErrorCodeUnsupportedMediaType = 415; + struct HttpError { int code; // HTTP error code diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 39b42f7518589..118bf86d55630 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -342,33 +342,34 @@ void PropagateItemJob::reportClientStatuses() { if (_item->_status == SyncFileItem::Status::Conflict) { if (_item->_direction == SyncFileItem::Direction::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_Conflict); } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict); } } else if (_item->_status == SyncFileItem::Status::FileNameClash) { if (_item->_direction == SyncFileItem::Direction::Up) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters); } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters); } } else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters); } else if (_item->_status == SyncFileItem::Status::FileNameInvalid) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); - } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200 && _item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) { + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters); + } else if (_item->_httpErrorCode != HttpErrorCodeNone && _item->_httpErrorCode != HttpErrorCodeSuccess && _item->_httpErrorCode != HttpErrorCodeSuccessCreated + && _item->_httpErrorCode != HttpErrorCodeSuccessNoContent) { if (_item->_direction == SyncFileItem::Up) { - const auto isCodeBadReqOrUnsupportedMediaType = (_item->_httpErrorCode == 400 || _item->_httpErrorCode == 415); + const auto isCodeBadReqOrUnsupportedMediaType = (_item->_httpErrorCode == HttpErrorCodeBadRequest || _item->_httpErrorCode == HttpErrorCodeUnsupportedMediaType); const auto isExceptionInfoPresent = !_item->_errorExceptionName.isEmpty() && !_item->_errorExceptionMessage.isEmpty(); if (isCodeBadReqOrUnsupportedMediaType && isExceptionInfoPresent && _item->_errorExceptionName.contains(QStringLiteral("UnsupportedMediaType")) && _item->_errorExceptionMessage.contains(QStringLiteral("virus"), Qt::CaseInsensitive)) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Virus_Detected); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_Virus_Detected); } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::UploadError_ServerError); } } else { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ServerError); } } } @@ -954,7 +955,7 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item, } _journal->setConflictRecord(conflictRecord); - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict); // Create a new upload job if the new conflict file should be uploaded if (account()->capabilities().uploadConflictFiles()) { @@ -1027,7 +1028,7 @@ OCC::Optional OwncloudPropagator::createCaseClashConflict(const SyncFil } _journal->setCaseConflictRecord(conflictRecord); - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictCaseClash); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictCaseClash); // Need a new sync to detect the created copy of the conflicting file _anotherSyncNeeded = true; diff --git a/src/libsync/owncloudpropagator_p.h b/src/libsync/owncloudpropagator_p.h index 7d0c121bb4af6..beb150648db87 100644 --- a/src/libsync/owncloudpropagator_p.h +++ b/src/libsync/owncloudpropagator_p.h @@ -47,8 +47,6 @@ inline bool fileIsStillChanging(const OCC::SyncFileItem &item) } namespace OCC { - - inline QByteArray getEtagFromReply(QNetworkReply *reply) { QByteArray ocEtag = parseEtag(reply->rawHeader("OC-ETag")); @@ -63,7 +61,7 @@ inline QByteArray getEtagFromReply(QNetworkReply *reply) return ret; } -inline QPair getExceptionFromReply(QNetworkReply *reply) +inline QPair getExceptionFromReply(QNetworkReply * const reply) { Q_ASSERT(reply); if (!reply) { diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index b78adaae1c46c..afdf33d491b42 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -675,7 +675,7 @@ void PropagateDownloadFile::startDownload() if (_tmpFile.exists()) FileSystem::setFileReadOnly(_tmpFile.fileName(), false); if (!_tmpFile.open(QIODevice::Append | QIODevice::Unbuffered)) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Cannot_Create_File); qCWarning(lcPropagateDownload) << "could not open temporary file" << _tmpFile.fileName(); done(SyncFileItem::NormalError, _tmpFile.errorString(), ErrorCategory::GenericError); return; @@ -1260,7 +1260,7 @@ void PropagateDownloadFile::downloadFinished() emit propagator()->touchedFile(filename); // The fileChanged() check is done above to generate better error messages. if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) { - propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Cannot_Create_File); qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(filename); // If the file is locked, we want to retry this sync when it // becomes available again, otherwise try again directly diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index ed2e09d028dd9..523f3506a193b 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -314,7 +314,7 @@ void SyncEngine::conflictRecordMaintenance() } _journal->setConflictRecord(record); - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict); } } } @@ -1262,7 +1262,7 @@ void SyncEngine::slotSummaryError(const QString &message) void SyncEngine::slotInsufficientLocalStorage() { - account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space); + account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_No_Free_Space); slotSummaryError( tr("Disk space is low: Downloads that would reduce free space " "below %1 were skipped.") @@ -1271,7 +1271,7 @@ void SyncEngine::slotInsufficientLocalStorage() void SyncEngine::slotInsufficientRemoteStorage() { - account()->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Free_Space); + account()->reportClientStatus(ClientStatusReportingStatus::UploadError_No_Free_Space); auto msg = tr("There is insufficient space available on the server for some uploads."); if (_uniqueErrors.contains(msg)) return; diff --git a/src/libsync/vfs/cfapi/vfs_cfapi.cpp b/src/libsync/vfs/cfapi/vfs_cfapi.cpp index 4a5a958eeabc5..935cd8605fe4f 100644 --- a/src/libsync/vfs/cfapi/vfs_cfapi.cpp +++ b/src/libsync/vfs/cfapi/vfs_cfapi.cpp @@ -464,7 +464,7 @@ void VfsCfApi::onHydrationJobFinished(HydrationJob *job) qCInfo(lcCfApi) << "Hydration job finished" << job->requestId() << job->folderPath() << job->status(); emit hydrationRequestFinished(job->requestId()); if (!job->errorString().isEmpty()) { - params().account->reportClientStatus(ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure); + params().account->reportClientStatus(ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure); emit failureHydrating(job->errorCode(), job->statusCode(), job->errorString(), job->folderPath()); } } diff --git a/test/testclientstatusreporting.cpp b/test/testclientstatusreporting.cpp index 5686baaa7391c..b018f20336666 100644 --- a/test/testclientstatusreporting.cpp +++ b/test/testclientstatusreporting.cpp @@ -13,7 +13,9 @@ */ #include "account.h" #include "accountstate.h" -#include "clientstatusreporting.h" +#include "clientstatusreportingcommon.h" +#include "clientstatusreportingdatabase.h" +#include "clientstatusreportingnetwork.h" #include "syncenginetestutils.h" #include @@ -39,8 +41,8 @@ class TestClientStatusReporting : public QObject private slots: void initTestCase() { - OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000; - OCC::ClientStatusReporting::repordSendIntervalMs = 2000; + OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval = 1000; + OCC::ClientStatusReportingNetwork::repordSendIntervalMs = 2000; fakeQnam.reset(new FakeQNAM({})); account = OCC::Account::create().get(); @@ -53,7 +55,7 @@ private slots: const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5); dbFilePath = QDir::tempPath() + QStringLiteral("/.tests_userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex())); QFile(dbFilePath).remove(); - OCC::ClientStatusReporting::dbPathForTesting = dbFilePath; + OCC::ClientStatusReportingDatabase::dbPathForTesting = dbFilePath; QVariantMap capabilities; capabilities[QStringLiteral("security_guard")] = QVariantMap{ @@ -74,42 +76,42 @@ private slots: { for (int i = 0; i < 2; ++i) { // 5 conflicts - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Conflict); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Conflict); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ConflictCaseClash); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Conflict); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_ConflictInvalidCharacters); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_Conflict); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ConflictCaseClash); // 4 problems - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_ServerError); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_ServerError); - account->reportClientStatus(OCC::ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure); - // 3 occurances of UploadError_No_Write_Permissions - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions); - - // 3 occurances of UploadError_Virus_Detected - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); - account->reportClientStatus(OCC::ClientStatusReporting::Status::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_Virtual_File_Hydration_Failure); + // 3 occurances of case ClientStatusReportingStatus::UploadError_No_Write_Permissions + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions); + + // 3 occurances of case ClientStatusReportingStatus::UploadError_Virus_Detected + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_Virus_Detected); // 2 occurances of E2EeError_GeneralError - account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); - account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); - QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs); + account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + QTest::qWait(OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReportingNetwork::repordSendIntervalMs); QVERIFY(!bodyReceivedAndParsed.isEmpty()); // we must have 2 e2ee errors const auto virusDetectedErrorsReceived = bodyReceivedAndParsed.value("virus_detected").toMap(); QVERIFY(!virusDetectedErrorsReceived.isEmpty()); - QVERIFY(virusDetectedErrorsReceived.size(), 3); + QCOMPARE(virusDetectedErrorsReceived.value("count"), 3); // we must have 2 e2ee errors const auto e2eeErrorsReceived = bodyReceivedAndParsed.value("e2e_errors").toMap(); QVERIFY(!e2eeErrorsReceived.isEmpty()); - QVERIFY(e2eeErrorsReceived.size(), 2); + QCOMPARE(e2eeErrorsReceived.value("count"), 2); // we must have 5 conflicts const auto conflictsReceived = bodyReceivedAndParsed.value("sync_conflicts").toMap(); @@ -120,8 +122,8 @@ private slots: const auto problemsReceived = bodyReceivedAndParsed.value("problems").toMap(); QVERIFY(!problemsReceived.isEmpty()); QCOMPARE(problemsReceived.size(), 4); - const auto problemsNoWritePermissions = problemsReceived.value(OCC::ClientStatusReporting::statusStringFromNumber(OCC::ClientStatusReporting::Status::UploadError_No_Write_Permissions)).toMap(); - // among those, 3 occurances of UploadError_No_Write_Permissions + const auto problemsNoWritePermissions = problemsReceived.value(OCC::clientStatusstatusStringFromNumber(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions)).toMap(); + // among those, 3 occurances of case ClientStatusReportingStatus::UploadError_No_Write_Permissions QCOMPARE(problemsNoWritePermissions.value("count"), 3); bodyReceivedAndParsed.clear(); @@ -130,7 +132,7 @@ private slots: void testNothingReportedAndNothingSent() { - QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs); + QTest::qWait(OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReportingNetwork::repordSendIntervalMs); QVERIFY(bodyReceivedAndParsed.isEmpty()); }