RotationAnimator {
target: contentImage
- running: false
+ running: root.running
onRunningChanged: contentImage.rotation = 0
from: 0
to: 360
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
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);
void trySetupClientStatusReporting();
- void reportClientStatus(const ClientStatusReporting::Status status);
+ void reportClientStatus(const ClientStatusReportingStatus status) const;
[[nodiscard]] std::shared_ptr<UserStatusConnector> userStatusConnector() const;
PushNotifications *_pushNotifications = nullptr;
- QScopedPointer<ClientStatusReporting> _clientStatusReporting;
+ std::unique_ptr<ClientStatusReporting> _clientStatusReporting;
std::shared_ptr<UserStatusConnector> _userStatusConnector;
void ClientSideEncryption::failedToInitialize(const AccountPtr &account)
{
forgetSensitiveData(account);
+ account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
Q_EMIT initializationFinished();
}
if (metadataKeys.isEmpty()) {
qCDebug(lcCse()) << "Could not migrate. No metadata keys found!";
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
}
if (_metadataKey.isEmpty()) {
qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!";
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
}
} else {
_metadataKey.clear();
_files.clear();
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
}
}
if (decryptResult.isEmpty())
{
qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key";
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return {};
}
return QByteArray::fromBase64(decryptResult);
if (decryptResult.isEmpty())
{
qCDebug(lcCse()) << "ERROR. Could not decrypt";
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return {};
}
if (_metadataKey.isEmpty()) {
qCDebug(lcCse) << "Metadata generation failed! Empty metadata key!";
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return {};
}
const auto version = _account->capabilities().clientSideEncryptionVersion();
QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKey);
if (encryptedEncrypted.isEmpty()) {
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
qCDebug(lcCse) << "Metadata generation failed!";
}
QJsonObject file;
if (decryptedKey.isEmpty() || decryptedAuthenticationTag.isEmpty() || decryptedInitializationVector.isEmpty()) {
qCDebug(lcCseMetadata) << "failed to decrypt filedrop entry" << it.key();
+ _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
continue;
}
#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;
}
#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;
};
--- /dev/null
+/*
+ * 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 {};
+}
+}
--- /dev/null
+/*
+ * 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);
+}
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+/*
+ * 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;
+};
+}
--- /dev/null
+/*
+ * 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
+}
--- /dev/null
+/*
+ * 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;
+};
+}
+++ /dev/null
-/*
- * 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;
-}
-}
quint64 _numOccurences = 1;
quint64 _lastOccurence = 0;
- [[nodiscard]] bool isValid() const;
+ [[nodiscard]] inline bool isValid() const
+ {
+ return _status >= 0 && !_name.isEmpty() && _lastOccurence > 0;
+ }
};
}
// 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");
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
{
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);
}
}
}
}
_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()) {
}
_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;
}
namespace OCC {
-
-
inline QByteArray getEtagFromReply(QNetworkReply *reply)
{
QByteArray ocEtag = parseEtag(reply->rawHeader("OC-ETag"));
return ret;
}
-inline QPair<QByteArray, QByteArray> getExceptionFromReply(QNetworkReply *reply)
+inline QPair<QByteArray, QByteArray> getExceptionFromReply(QNetworkReply * const reply)
{
Q_ASSERT(reply);
if (!reply) {
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;
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
}
_journal->setConflictRecord(record);
- account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict);
+ account()->reportClientStatus(ClientStatusReportingStatus::DownloadError_Conflict);
}
}
}
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.")
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;
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());
}
}
*/
#include "account.h"
#include "accountstate.h"
-#include "clientstatusreporting.h"
+#include "clientstatusreportingcommon.h"
+#include "clientstatusreportingdatabase.h"
+#include "clientstatusreportingnetwork.h"
#include "syncenginetestutils.h"
#include <QSignalSpy>
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();
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{
{
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();
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();
void testNothingReportedAndNothingSent()
{
- QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs);
+ QTest::qWait(OCC::ClientStatusReportingNetwork::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReportingNetwork::repordSendIntervalMs);
QVERIFY(bodyReceivedAndParsed.isEmpty());
}