diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 0e0f85b65dda2..cd124bd3b653b 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -26,6 +26,12 @@ set(libsync_SRCS clientproxy.cpp clientstatusreporting.h clientstatusreporting.cpp + clientstatusreportingcommon.h + clientstatusreportingcommon.cpp + clientstatusreportingdatabase.h + clientstatusreportingdatabase.cpp + clientstatusreportingnetwork.h + clientstatusreportingnetwork.cpp clientstatusreportingrecord.h cookiejar.h cookiejar.cpp diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 7ceb196bdf0b2..bea76c53b9069 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -296,7 +296,7 @@ void Account::trySetupClientStatusReporting() } } -void Account::reportClientStatus(const ClientStatusReporting::Status status) const +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 005115adc0ff1..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) const; + void reportClientStatus(const ClientStatusReportingStatus status) const; [[nodiscard]] std::shared_ptr userStatusConnector() const; diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 71d19aed12e02..dd9263b024d62 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1262,7 +1262,7 @@ bool ClientSideEncryption::sensitiveDataRemaining() const void ClientSideEncryption::failedToInitialize(const AccountPtr &account) { forgetSensitiveData(account); - account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); Q_EMIT initializationFinished(); } @@ -1776,7 +1776,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) if (metadataKeys.isEmpty()) { qCDebug(lcCse()) << "Could not migrate. No metadata keys found!"; - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } @@ -1789,7 +1789,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) if (_metadataKey.isEmpty()) { qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } @@ -1864,7 +1864,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } else { _metadataKey.clear(); _files.clear(); - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } } @@ -1903,7 +1903,7 @@ QByteArray FolderMetadata::decryptData(const QByteArray &data) const if (decryptResult.isEmpty()) { qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key"; - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } return QByteArray::fromBase64(decryptResult); @@ -1921,7 +1921,7 @@ QByteArray FolderMetadata::decryptDataUsingKey(const QByteArray &data, if (decryptResult.isEmpty()) { qCDebug(lcCse()) << "ERROR. Could not decrypt"; - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } @@ -1985,7 +1985,7 @@ QByteArray FolderMetadata::encryptedMetadata() const { if (_metadataKey.isEmpty()) { qCDebug(lcCse) << "Metadata generation failed! Empty metadata key!"; - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return {}; } const auto version = _account->capabilities().clientSideEncryptionVersion(); @@ -2007,7 +2007,7 @@ QByteArray FolderMetadata::encryptedMetadata() const { QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKey); if (encryptedEncrypted.isEmpty()) { - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); qCDebug(lcCse) << "Metadata generation failed!"; } QJsonObject file; @@ -2095,7 +2095,7 @@ bool FolderMetadata::moveFromFileDropToFiles() if (decryptedKey.isEmpty() || decryptedAuthenticationTag.isEmpty() || decryptedInitializationVector.isEmpty()) { qCDebug(lcCseMetadata) << "failed to decrypt filedrop entry" << it.key(); - _account->reportClientStatus(OCC::ClientStatusReporting::Status::E2EeError_GeneralError); + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); continue; } diff --git a/src/libsync/clientstatusreporting.cpp b/src/libsync/clientstatusreporting.cpp index 1930ce167f69b..0566995737282 100644 --- a/src/libsync/clientstatusreporting.cpp +++ b/src/libsync/clientstatusreporting.cpp @@ -14,423 +14,53 @@ #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() -{ - if (_database.isOpen()) { - _database.close(); - } -} - -void ClientStatusReporting::init() -{ - Q_ASSERT(!_isInitialized); - if (_isInitialized) { - 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."; - 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(); - 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; - } - - updateStatusNamesHash(); - - _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval); - connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer); - _clientStatusReportingSendTimer.start(); - - _isInitialized = true; -} - -QVector ClientStatusReporting::getClientStatusReportingRecords() const +ClientStatusReporting::ClientStatusReporting(Account *account) + : _account(account) { - 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; + for (int i = 0; i < ClientStatusReportingStatus::Count; ++i) { + const auto statusString = clientStatusstatusStringFromNumber(static_cast(i)); + _statusStrings[i] = statusString; } - 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; -} + _database = QSharedPointer::create(account); + _reporter = std::make_unique(account, _database); -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(); - } + _isInitialized = _database->isInitialized() && _reporter->isInitialized(); } -Result ClientStatusReporting::setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const +ClientStatusReporting::~ClientStatusReporting() { - 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 {}; + // the sole purpose of this desrtuctor is to make unique_ptr work with forward declaration, but let's clearn the initialized flag too + _isInitialized = false; } -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) { + if (status < 0 || status >= ClientStatusReportingStatus::Count) { qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << status; return; } ClientStatusReportingRecord record; - record._name = _statusNamesAndHashes[status].first; + record._name = _statusStrings[status]; record._status = 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()) { - 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())); -} - -void ClientStatusReporting::updateStatusNamesHash() -{ - 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); - } -} - -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 38198e18af893..669c4dc4fa1d4 100644 --- a/src/libsync/clientstatusreporting.h +++ b/src/libsync/clientstatusreporting.h @@ -15,101 +15,40 @@ #include "owncloudlib.h" #include +#include "clientstatusreportingcommon.h" + +#include -#include #include #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, - }; - Q_ENUM(Status); - - 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; - - void updateStatusNamesHash(); - -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: + void reportClientStatus(const ClientStatusReportingStatus status) const; Account *_account = nullptr; - QSqlDatabase _database; - 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..62e48aa2d6669 --- /dev/null +++ b/src/libsync/clientstatusreportingcommon.cpp @@ -0,0 +1,62 @@ +/* + * 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 "clientstatusreportingcommon.h" +#include + +namespace OCC { +QByteArray clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status) +{ + Q_ASSERT(status >= 0 && status < Count); + if (status < 0 || status >= ClientStatusReportingStatus::Count) { + qDebug() << "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 {}; +} +} diff --git a/src/libsync/clientstatusreportingcommon.h b/src/libsync/clientstatusreportingcommon.h new file mode 100644 index 0000000000000..db18cd1580fa0 --- /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 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..489617730c8db --- /dev/null +++ b/src/libsync/clientstatusreportingdatabase.cpp @@ -0,0 +1,223 @@ +/* + * 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 + +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; + } + + updateStatusNamesHash(); + + _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; +} + +void ClientStatusReportingDatabase::deleteClientStatusReportingRecords() const +{ + QSqlQuery query; + if (!query.prepare(QStringLiteral("DELETE FROM clientstatusreporting")) || !query.exec()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not delete records from clientstatusreporting:" << query.lastError().text(); + } +} + +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())); +} + +void ClientStatusReportingDatabase::updateStatusNamesHash() +{ + QByteArray statusNamesContatenated; + for (int i = 0; i < ClientStatusReportingStatus::Count; ++i) { + statusNamesContatenated += clientStatusstatusStringFromNumber(static_cast(i)); + } + statusNamesContatenated += QByteArray::number(ClientStatusReportingStatus::Count); + const auto statusNamesHashCurrent = QCryptographicHash::hash(statusNamesContatenated, QCryptographicHash::Md5).toHex(); + const auto statusNamesHashFromDb = getStatusNamesHash(); + + if (statusNamesHashCurrent != statusNamesHashFromDb) { + deleteClientStatusReportingRecords(); + setStatusNamesHash(statusNamesHashCurrent); + } +} + +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(); +} + +void 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()) { + qCDebug(lcClientStatusReportingDatabase) << "Could not set status names hash."; + 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..fd1347c1a7079 --- /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 +#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; + void deleteClientStatusReportingRecords() const; + + void setLastSentReportTimestamp(const quint64 timestamp) const; + [[nodiscard]] quint64 getLastSentReportTimestamp() const; + + void setStatusNamesHash(const QByteArray &hash) const; + [[nodiscard]] QByteArray getStatusNamesHash() const; + + [[nodiscard]] bool isInitialized() const; + +private: + [[nodiscard]] QString makeDbPath(const Account *account) const; + void updateStatusNamesHash(); + +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..cc871fe8f8784 --- /dev/null +++ b/src/libsync/clientstatusreportingnetwork.cpp @@ -0,0 +1,188 @@ +/* + * 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; + } + + 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(lcClientStatusReportingNetwork) << "Received error when sending client report statusCode:" << statusCode << "codeFromJson:" << codeFromJson; + } + }); + clientStatusReportingJob->start(); +} + +void ClientStatusReportingNetwork::reportToServerSentSuccessfully() +{ + _database->deleteClientStatusReportingRecords(); + _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(status >= 0 && status < Count); + if (status < 0 || status >= ClientStatusReportingStatus::Count) { + qCDebug(lcClientStatusReportingNetwork) << "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 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..e41bc5859ff0f --- /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/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/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 39b42f7518589..e2628d48a1b99 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -342,20 +342,20 @@ 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); + propagator()->account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_ConflictInvalidCharacters); } else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200 && _item->_httpErrorCode != 201 && _item->_httpErrorCode != 204) { if (_item->_direction == SyncFileItem::Up) { const auto isCodeBadReqOrUnsupportedMediaType = (_item->_httpErrorCode == 400 || _item->_httpErrorCode == 415); @@ -363,12 +363,12 @@ void PropagateItemJob::reportClientStatuses() 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 +954,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 +1027,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/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 c796b7a5a75c0..e0f625ae00876 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,30 +76,30 @@ 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); + account->reportClientStatus(OCC::ClientStatusReportingStatus::UploadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::DownloadError_ServerError); + account->reportClientStatus(OCC::ClientStatusReportingStatus::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); + 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 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_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()); @@ -120,7 +122,7 @@ 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(); + const auto problemsNoWritePermissions = problemsReceived.value(OCC::clientStatusstatusStringFromNumber(OCC::ClientStatusReportingStatus::UploadError_No_Write_Permissions)).toMap(); // among those, 3 occurances of UploadError_No_Write_Permissions QCOMPARE(problemsNoWritePermissions.value("count"), 3); @@ -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()); }