Added error reporting for E2EE issues.
authoralex-z <blackslayer4@gmail.com>
Thu, 30 Nov 2023 16:34:09 +0000 (17:34 +0100)
committerallexzander <allexzander@users.noreply.github.com>
Mon, 11 Dec 2023 17:31:39 +0000 (18:31 +0100)
Signed-off-by: alex-z <blackslayer4@gmail.com>
23 files changed:
src/gui/tray/NCBusyIndicator.qml
src/libsync/CMakeLists.txt
src/libsync/account.cpp
src/libsync/account.h
src/libsync/clientsideencryption.cpp
src/libsync/clientstatusreporting.cpp
src/libsync/clientstatusreporting.h
src/libsync/clientstatusreportingcommon.cpp [new file with mode: 0644]
src/libsync/clientstatusreportingcommon.h [new file with mode: 0644]
src/libsync/clientstatusreportingdatabase.cpp [new file with mode: 0644]
src/libsync/clientstatusreportingdatabase.h [new file with mode: 0644]
src/libsync/clientstatusreportingnetwork.cpp [new file with mode: 0644]
src/libsync/clientstatusreportingnetwork.h [new file with mode: 0644]
src/libsync/clientstatusreportingrecord.cpp [deleted file]
src/libsync/clientstatusreportingrecord.h
src/libsync/discovery.cpp
src/libsync/networkjobs.h
src/libsync/owncloudpropagator.cpp
src/libsync/owncloudpropagator_p.h
src/libsync/propagatedownload.cpp
src/libsync/syncengine.cpp
src/libsync/vfs/cfapi/vfs_cfapi.cpp
test/testclientstatusreporting.cpp

index ddead28d4b07d66257cbfd05e45ce64f59addde5..973d3dee1ed628b93e928b1adc42202fb341fdb5 100644 (file)
@@ -42,7 +42,7 @@ BusyIndicator {
 
         RotationAnimator {
             target: contentImage
-            running: false
+            running: root.running
             onRunningChanged: contentImage.rotation = 0
             from: 0
             to: 360
index bd7931a8670d31e29d0fdb30153ed199e6da0b12..cd124bd3b653b5d4d8e688443cef0cb7f0b567ea 100644 (file)
@@ -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
index 1d27a74ffaca2d1481b1be92a5a9cc3c28e13ff1..bea76c53b9069f01b3cbbb5b9de9d55c6eddd22b 100644 (file)
@@ -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<ClientStatusReporting>(this);
     }
 }
 
-void Account::reportClientStatus(const ClientStatusReporting::Status status)
+void Account::reportClientStatus(const ClientStatusReportingStatus status) const
 {
     if (_clientStatusReporting) {
         _clientStatusReporting->reportClientStatus(status);
index 1c51bb7a0c6b5d608e10ef0f97490447303a9e08..e118b53fb2f25213d66ab055412c697fe656fa13 100644 (file)
@@ -308,7 +308,7 @@ public:
 
     void trySetupClientStatusReporting();
 
-    void reportClientStatus(const ClientStatusReporting::Status status);
+    void reportClientStatus(const ClientStatusReportingStatus status) const;
 
     [[nodiscard]] std::shared_ptr<UserStatusConnector> userStatusConnector() const;
 
@@ -444,7 +444,7 @@ private:
 
     PushNotifications *_pushNotifications = nullptr;
 
-    QScopedPointer<ClientStatusReporting> _clientStatusReporting;
+    std::unique_ptr<ClientStatusReporting> _clientStatusReporting;
 
     std::shared_ptr<UserStatusConnector> _userStatusConnector;
 
index 3278c450dfebaf4e25ee6886fdaa8c717b6d2228..dd9263b024d62f5c0f282bcdc295709689b4dc5b 100644 (file)
@@ -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;
         }
 
index 617405047c5ecb7d2025c98f3106ff78c4a78a21..682ff206e1c3194ffbf0b9797f1e92b12ddb7f0d 100644 (file)
 #include "clientstatusreporting.h"
 
 #include "account.h"
+#include "clientstatusreportingdatabase.h"
+#include "clientstatusreportingnetwork.h"
 #include "clientstatusreportingrecord.h"
-#include <configfile.h>
-#include "common/c_jhash.h"
-#include <networkjobs.h>
-
-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<int>(ClientStatusReportingStatus::Count); ++i) {
+        const auto statusString = clientStatusstatusStringFromNumber(static_cast<ClientStatusReportingStatus>(i));
+        _statusStrings[i] = statusString;
     }
-}
 
-void ClientStatusReporting::init()
-{
-    Q_ASSERT(!_isInitialized);
-    if (_isInitialized) {
-        qCDebug(lcClientStatusReporting) << "Double call to init";
+    if (_statusStrings.size() < static_cast<int>(ClientStatusReportingStatus::Count)) {
         return;
     }
 
-    for (int i = 0; i < ClientStatusReporting::Status::Count; ++i) {
-        const auto statusString = statusStringFromNumber(static_cast<Status>(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<ClientStatusReportingDatabase>::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<ClientStatusReportingNetwork>(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<Status>(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<ClientStatusReportingRecord> ClientStatusReporting::getClientStatusReportingRecords() const
-{
-    QVector<ClientStatusReportingRecord> 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<void, QString> 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<int>(status) >= 0 && static_cast<int>(status) < static_cast<int>(ClientStatusReportingStatus::Count));
+    if (static_cast<int>(status) < 0 || static_cast<int>(status) >= static_cast<int>(ClientStatusReportingStatus::Count)) {
+        qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << static_cast<int>(status);
         return;
     }
 
     ClientStatusReportingRecord record;
-    record._name = _statusNamesAndHashes[status].first;
-    record._status = status;
+    record._name = _statusStrings[static_cast<int>(status)];
+    record._status = static_cast<int>(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<Status>(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;
 }
index 70b7508c028b032f35a99411183c7ffd4f6aba6b..54aaf57853363aaf333545d6188531846d326647 100644 (file)
 
 #include "owncloudlib.h"
 #include <common/result.h>
+#include "clientstatusreportingcommon.h"
+
+#include <memory>
 
-#include <QtGlobal>
 #include <QByteArray>
 #include <QHash>
-#include <QObject>
-#include <QPair>
-#include <QRecursiveMutex>
-#include <QString>
-#include <QTimer>
-#include <QtSql>
+#include <QSharedPointer>
 
 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<void, QString> setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const;
-    [[nodiscard]] QVector<ClientStatusReportingRecord> 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<int, QByteArray> _statusStrings;
 
-    QHash<int, QPair<QByteArray, quint64>> _statusNamesAndHashes;
+    QSharedPointer<ClientStatusReportingDatabase> _database;
 
-    // inspired by SyncJournalDb
-    mutable QRecursiveMutex _mutex;
+    std::unique_ptr<ClientStatusReportingNetwork> _reporter;
 
     friend class Account;
 };
diff --git a/src/libsync/clientstatusreportingcommon.cpp b/src/libsync/clientstatusreportingcommon.cpp
new file mode 100644 (file)
index 0000000..fabbf2b
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * 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 <QLoggingCategory>
+
+namespace OCC {
+Q_LOGGING_CATEGORY(lcClientStatusReportingCommon, "nextcloud.sync.clientstatusreportingcommon", QtInfoMsg)
+
+QByteArray clientStatusstatusStringFromNumber(const ClientStatusReportingStatus status)
+{
+    Q_ASSERT(static_cast<int>(status) >= 0 && static_cast<int>(status) < static_cast<int>(ClientStatusReportingStatus::Count));
+    if (static_cast<int>(status) < 0 || static_cast<int>(status) >= static_cast<int>(ClientStatusReportingStatus::Count)) {
+        qCDebug(lcClientStatusReportingCommon) << "Invalid status:" << static_cast<int>(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 (file)
index 0000000..4e08ca1
--- /dev/null
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * 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 <QtCore/qbytearray.h>
+
+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 (file)
index 0000000..169df5e
--- /dev/null
@@ -0,0 +1,280 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * 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 <configfile.h>
+
+#include <QSqlError>
+#include <QSqlRecord>
+#include <QSqlQuery>
+
+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<ClientStatusReportingRecord> ClientStatusReportingDatabase::getClientStatusReportingRecords() const
+{
+    QVector<ClientStatusReportingRecord> 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<void, QString> 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<void, QString> 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<int>(ClientStatusReportingStatus::Count); ++i) {
+        statusNamesContatenated += clientStatusstatusStringFromNumber(static_cast<ClientStatusReportingStatus>(i));
+    }
+    statusNamesContatenated += QByteArray::number(static_cast<int>(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<QByteArray> ClientStatusReportingDatabase::getTableColumns(const QString &table) const
+{
+    QVector<QByteArray> 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<void, QString> 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 (file)
index 0000000..d3ef3c7
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * 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 <common/result.h>
+#include "clientstatusreportingcommon.h"
+#include "clientstatusreportingrecord.h"
+
+#include <QtGlobal>
+#include <QByteArray>
+#include <QMutex>
+#include <QString>
+#include <QVector>
+#include <QSqlDatabase>
+
+namespace OCC {
+
+class Account;
+
+class OWNCLOUDSYNC_EXPORT ClientStatusReportingDatabase
+{
+public:
+    explicit ClientStatusReportingDatabase(const Account *account);
+    ~ClientStatusReportingDatabase();
+
+    [[nodiscard]] Result<void, QString> setClientStatusReportingRecord(const ClientStatusReportingRecord &record) const;
+    [[nodiscard]] QVector<ClientStatusReportingRecord> getClientStatusReportingRecords() const;
+    [[nodiscard]] Result<void, QString> deleteClientStatusReportingRecords() const;
+
+    void setLastSentReportTimestamp(const quint64 timestamp) const;
+    [[nodiscard]] quint64 getLastSentReportTimestamp() const;
+
+    [[nodiscard]] Result<void, QString> 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<QByteArray> 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 (file)
index 0000000..bee15f3
--- /dev/null
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * 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 <networkjobs.h>
+
+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<ClientStatusReportingDatabase> 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<ClientStatusReportingStatus>(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<int>(status) >= 0 && static_cast<int>(status) < static_cast<int>(ClientStatusReportingStatus::Count));
+    if (static_cast<int>(status) < 0 || static_cast<int>(status) >= static_cast<int>(ClientStatusReportingStatus::Count)) {
+        qCDebug(lcClientStatusReportingNetwork) << "Invalid status:" << static_cast<int>(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 (file)
index 0000000..0b4d10c
--- /dev/null
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
+ *
+ * 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 <common/result.h>
+#include "clientstatusreportingcommon.h"
+
+#include <QtGlobal>
+#include <QByteArray>
+#include <QObject>
+#include <QSharedPointer>
+#include <QString>
+#include <QTimer>
+
+namespace OCC {
+
+class Account;
+class ClientStatusReportingDatabase;
+struct ClientStatusReportingRecord;
+
+class OWNCLOUDSYNC_EXPORT ClientStatusReportingNetwork : public QObject
+{
+    Q_OBJECT
+public:
+    explicit ClientStatusReportingNetwork(Account *account, const QSharedPointer<ClientStatusReportingDatabase> 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<ClientStatusReportingDatabase> _database;
+
+    bool _isInitialized = false;
+
+    QTimer _clientStatusReportingSendTimer;
+};
+}
diff --git a/src/libsync/clientstatusreportingrecord.cpp b/src/libsync/clientstatusreportingrecord.cpp
deleted file mode 100644 (file)
index 4d795de..0000000
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
- *
- * 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;
-}
-}
index 6595177ec09957b3accf5be6abe0e028ca15ab7b..94f190a0ee2102b4f0d9b06601d8ea711ff37ae1 100644 (file)
@@ -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;
+    }
 };
 }
index c316dd518aa40d8efa3617c1c78ca2f548eaceaf..dc56ec602b85ef2e11565154a7e4ef95d9c90863 100644 (file)
@@ -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");
index 4fc397a7a412948ad43378e83195d31c98f9e54b..b299bdb0aa3879956f1b6668bd40ddc0446dbec5 100644 (file)
@@ -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
index 39b42f751858939a60f1dca14110fa9ff780e656..118bf86d556308b3b1e9b9f00830b76f181294c4 100644 (file)
@@ -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<QString> 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;
index 7d0c121bb4af65d19ee660af9daf75227291b3ca..beb150648db87591a6c0a535f09fcc96c8c66c49 100644 (file)
@@ -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<QByteArray, QByteArray> getExceptionFromReply(QNetworkReply *reply)
+inline QPair<QByteArray, QByteArray> getExceptionFromReply(QNetworkReply * const reply)
 {
     Q_ASSERT(reply);
     if (!reply) {
index b78adaae1c46c39e6e262b9bb3cf7c0106249452..afdf33d491b429d6a9cb28063be1aab45e65423a 100644 (file)
@@ -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
index ed2e09d028dd9455d71d6703c10a76bcafc719f7..523f3506a193ba3a8c4baeedcb733a8e24db5981 100644 (file)
@@ -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;
index 4a5a958eeabc5195e475e02c4b9ffed209c87ece..935cd8605fe4fb70cb8ddeb01f7959837146ab8a 100644 (file)
@@ -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());
     }
 }
index 5686baaa7391c0bfe078cb25780f684d0d031ddb..b018f203366664532d056fc0aec94888b4194887 100644 (file)
@@ -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 <QSignalSpy>
@@ -39,8 +41,8 @@ public:
 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());
     }