Generate client status report records when errors happen.

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2023-11-27 18:47:17 +01:00 коммит произвёл allexzander
Родитель 5b54b13a97
Коммит 8e38739d94
10 изменённых файлов: 283 добавлений и 125 удалений

Просмотреть файл

@ -22,10 +22,6 @@
namespace namespace
{ {
constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime"; constexpr auto lastSentReportTimestamp = "lastClientStatusReportSentTime";
//constexpr auto repordSendIntervalMs = 24 * 60 * 60 * 1000;
//constexpr int clientStatusReportingSendTimerInterval = 1000 * 60 * 2;
constexpr auto repordSendIntervalMs = 2000;
constexpr int clientStatusReportingSendTimerInterval = 5000;
} }
namespace OCC namespace OCC
@ -39,6 +35,13 @@ ClientStatusReporting::ClientStatusReporting(Account *account, QObject *parent)
init(); init();
} }
ClientStatusReporting::~ClientStatusReporting()
{
if (_database.isOpen()) {
_database.close();
}
}
void ClientStatusReporting::init() void ClientStatusReporting::init()
{ {
if (_isInitialized) { if (_isInitialized) {
@ -51,10 +54,7 @@ void ClientStatusReporting::init()
_statusNamesAndHashes[i] = {statusString, SyncJournalDb::getPHash(statusString)}; _statusNamesAndHashes[i] = {statusString, SyncJournalDb::getPHash(statusString)};
} }
const auto databaseId = QStringLiteral("%1@%2").arg(_account->davUser(), _account->url().toString()); const auto dbPath = makeDbPath();
const auto databaseIdHash = QCryptographicHash::hash(databaseId.toUtf8(), QCryptographicHash::Md5);
const QString dbPath = ConfigFile().configPath() + QStringLiteral(".userdata_%1.db").arg(QString::fromLatin1(databaseIdHash.left(6).toHex()));
_database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE")); _database = QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"));
_database.setDatabaseName(dbPath); _database.setDatabaseName(dbPath);
@ -82,25 +82,11 @@ void ClientStatusReporting::init()
return; return;
} }
_clientStatusReportingSendTimer.setInterval(clientStatusReportingSendTimerInterval); _clientStatusReportingSendTimer.setInterval(clientStatusReportingTrySendTimerInterval);
connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer); connect(&_clientStatusReportingSendTimer, &QTimer::timeout, this, &ClientStatusReporting::sendReportToServer);
_clientStatusReportingSendTimer.start(); _clientStatusReportingSendTimer.start();
_isInitialized = true; _isInitialized = true;
reportClientStatus(Status::DownloadError_ConflictCaseClash);
reportClientStatus(Status::DownloadError_ConflictInvalidCharacters);
reportClientStatus(Status::UploadError_ServerError);
reportClientStatus(Status::UploadError_ServerError);
setLastSentReportTimestamp(QDateTime::currentDateTime().toMSecsSinceEpoch());
auto records = getClientStatusReportingRecords();
// auto resDelete = deleteClientStatusReportingRecords();
records = getClientStatusReportingRecords();
auto res = getLastSentReportTimestamp();
} }
QVector<ClientStatusReportingRecord> ClientStatusReporting::getClientStatusReportingRecords() const QVector<ClientStatusReportingRecord> ClientStatusReporting::getClientStatusReportingRecords() const
@ -147,7 +133,7 @@ Result<void, QString> ClientStatusReporting::setClientStatusReportingRecord(cons
{ {
Q_ASSERT(record.isValid()); Q_ASSERT(record.isValid());
if (!record.isValid()) { if (!record.isValid()) {
qCWarning(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord"; qCDebug(lcClientStatusReporting) << "Failed to set ClientStatusReportingRecord";
return {QStringLiteral("Invalid parameter")}; return {QStringLiteral("Invalid parameter")};
} }
@ -183,7 +169,7 @@ void ClientStatusReporting::reportClientStatus(const Status status)
} }
Q_ASSERT(status >= 0 && status < Count); Q_ASSERT(status >= 0 && status < Count);
if (status < 0 || status >= Status::Count) { if (status < 0 || status >= Status::Count) {
qCWarning(lcClientStatusReporting) << "Trying to report invalid status:" << status; qCDebug(lcClientStatusReporting) << "Trying to report invalid status:" << status;
return; return;
} }
@ -194,7 +180,7 @@ void ClientStatusReporting::reportClientStatus(const Status status)
record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch(); record._lastOccurence = QDateTime::currentDateTimeUtc().toMSecsSinceEpoch();
const auto result = setClientStatusReportingRecord(record); const auto result = setClientStatusReportingRecord(record);
if (!result.isValid()) { if (!result.isValid()) {
qCWarning(lcClientStatusReporting) << "Could not report client status:" << result.error(); qCDebug(lcClientStatusReporting) << "Could not report client status:" << result.error();
} }
} }
@ -210,17 +196,79 @@ void ClientStatusReporting::sendReportToServer()
return; return;
} }
const auto records = getClientStatusReportingRecords(); const auto report = prepareReport();
if (records.isEmpty()) { if (report.isEmpty()) {
qCDebug(lcClientStatusReporting) << "Failed to generate report. Report is empty.";
return; return;
} }
QVariantMap report; 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("ocs").toObject().value("meta").toObject();
const auto codeFromJson = metaFromJson.value("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()
{
if (!deleteClientStatusReportingRecords()) {
qCDebug(lcClientStatusReporting) << "Error deleting client status report.";
}
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()));
}
qulonglong ClientStatusReporting::getLastSentReportTimestamp() const
{
QMutexLocker locker(&_mutex);
QSqlQuery query;
const auto prepareResult = query.prepare("SELECT value FROM keyvalue WHERE key = (:key)");
query.bindValue(":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;
}
int valueIndex = query.record().indexOf("value");
return query.value(valueIndex).toULongLong();
}
QVariantMap ClientStatusReporting::prepareReport() const
{
const auto records = getClientStatusReportingRecords();
if (records.isEmpty()) {
return {};
}
QVariantMap report;
report[QStringLiteral("sync_conflicts")] = QVariantMap{}; report[QStringLiteral("sync_conflicts")] = QVariantMap{};
report[QStringLiteral("problems")] = QVariantMap{}; report[QStringLiteral("problems")] = QVariantMap{};
report[QStringLiteral("virus_detected")] = QVariantMap{}; report[QStringLiteral("virus_detected")] = QVariantMap{};
report[QStringLiteral ("e2e_errors")] = QVariantMap{}; report[QStringLiteral("e2e_errors")] = QVariantMap{};
QVariantMap syncConflicts; QVariantMap syncConflicts;
QVariantMap problems; QVariantMap problems;
@ -243,53 +291,10 @@ void ClientStatusReporting::sendReportToServer()
report[categoryKey] = problems; report[categoryKey] = problems;
} }
} }
return report;
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 == 200 || statusCode == 204) {
const auto data = json.object().value("ocs").toObject().value("data").toObject();
const auto dataMap = data.toVariantMap();
slotSendReportToserverFinished();
}
});
clientStatusReportingJob->start();
} }
void ClientStatusReporting::slotSendReportToserverFinished() void ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestamp)
{
if (!deleteClientStatusReportingRecords()) {
qCWarning(lcClientStatusReporting) << "Error deleting client status report.";
}
setLastSentReportTimestamp(QDateTime::currentDateTimeUtc().toMSecsSinceEpoch());
}
qulonglong ClientStatusReporting::getLastSentReportTimestamp() const
{
QMutexLocker locker(&_mutex);
QSqlQuery query;
const auto prepareResult = query.prepare("SELECT value FROM keyvalue WHERE key = (:key)");
query.bindValue(":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;
}
int valueIndex = query.record().indexOf("value");
return query.value(valueIndex).toULongLong();
}
bool ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestamp)
{ {
QMutexLocker locker(&_mutex); QMutexLocker locker(&_mutex);
QSqlQuery query; QSqlQuery query;
@ -298,17 +303,15 @@ bool ClientStatusReporting::setLastSentReportTimestamp(const qulonglong timestam
query.bindValue(":value", timestamp); query.bindValue(":value", timestamp);
if (!prepareResult || !query.exec()) { if (!prepareResult || !query.exec()) {
qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp; qCDebug(lcClientStatusReporting) << "Could not set last sent report timestamp from keyvalue table. No such record:" << lastSentReportTimestamp;
return false; return;
} }
return true;
} }
QByteArray ClientStatusReporting::statusStringFromNumber(const Status status) QByteArray ClientStatusReporting::statusStringFromNumber(const Status status)
{ {
Q_ASSERT(status >= 0 && status < Count); Q_ASSERT(status >= 0 && status < Count);
if (status < 0 || status >= Status::Count) { if (status < 0 || status >= Status::Count) {
qCWarning(lcClientStatusReporting) << "Invalid status:" << status; qCDebug(lcClientStatusReporting) << "Invalid status:" << status;
return {}; return {};
} }
@ -328,8 +331,6 @@ QByteArray ClientStatusReporting::statusStringFromNumber(const Status status)
case DownloadError_Virtual_File_Hydration_Failure: case DownloadError_Virtual_File_Hydration_Failure:
return QByteArrayLiteral("DownloadError.VIRTUAL_FILE_HYDRATION_FAILURE "); return QByteArrayLiteral("DownloadError.VIRTUAL_FILE_HYDRATION_FAILURE ");
case UploadError_Conflict: case UploadError_Conflict:
return QByteArrayLiteral("UploadError.CONFLICT");
case UploadError_ConflictCaseClash:
return QByteArrayLiteral("UploadError.CONFLICT_CASECLASH"); return QByteArrayLiteral("UploadError.CONFLICT_CASECLASH");
case UploadError_ConflictInvalidCharacters: case UploadError_ConflictInvalidCharacters:
return QByteArrayLiteral("UploadError.CONFLICT_INVALID_CHARACTERS"); return QByteArrayLiteral("UploadError.CONFLICT_INVALID_CHARACTERS");
@ -349,7 +350,7 @@ QString ClientStatusReporting::classifyStatus(const Status status)
{ {
Q_ASSERT(status >= 0 && status < Count); Q_ASSERT(status >= 0 && status < Count);
if (status < 0 || status >= Status::Count) { if (status < 0 || status >= Status::Count) {
qCWarning(lcClientStatusReporting) << "Invalid status:" << status; qCDebug(lcClientStatusReporting) << "Invalid status:" << status;
return {}; return {};
} }
@ -358,7 +359,6 @@ QString ClientStatusReporting::classifyStatus(const Status status)
case DownloadError_ConflictCaseClash: case DownloadError_ConflictCaseClash:
case DownloadError_ConflictInvalidCharacters: case DownloadError_ConflictInvalidCharacters:
case UploadError_Conflict: case UploadError_Conflict:
case UploadError_ConflictCaseClash:
case UploadError_ConflictInvalidCharacters: case UploadError_ConflictInvalidCharacters:
return QStringLiteral("sync_conflicts"); return QStringLiteral("sync_conflicts");
case DownloadError_Cannot_Create_File: case DownloadError_Cannot_Create_File:
@ -374,4 +374,7 @@ QString ClientStatusReporting::classifyStatus(const Status status)
}; };
return {}; return {};
} }
int ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000 * 60 * 2; // check if the time has come, every 2 minutes
int ClientStatusReporting::repordSendIntervalMs = 24 * 60 * 60 * 1000; // once every 24 hours
QString ClientStatusReporting::dbPathForTesting;
} }

Просмотреть файл

@ -26,6 +26,8 @@
#include <QPair> #include <QPair>
#include <QRecursiveMutex> #include <QRecursiveMutex>
class TestClientStatusReporting;
namespace OCC { namespace OCC {
class Account; class Account;
@ -36,24 +38,23 @@ class OWNCLOUDSYNC_EXPORT ClientStatusReporting : public QObject
Q_OBJECT Q_OBJECT
public: public:
enum Status { enum Status {
DownloadError_Cannot_Create_File = 0, DownloadError_Cannot_Create_File = 100,
DownloadError_Conflict, DownloadError_Conflict = 101,
DownloadError_ConflictCaseClash, DownloadError_ConflictCaseClash = 102,
DownloadError_ConflictInvalidCharacters, DownloadError_ConflictInvalidCharacters = 103,
DownloadError_No_Free_Space, DownloadError_No_Free_Space = 104,
DownloadError_ServerError, DownloadError_ServerError = 105,
DownloadError_Virtual_File_Hydration_Failure, DownloadError_Virtual_File_Hydration_Failure = 106,
UploadError_Conflict, UploadError_Conflict = 107,
UploadError_ConflictCaseClash, UploadError_ConflictInvalidCharacters = 108,
UploadError_ConflictInvalidCharacters, UploadError_No_Free_Space = 109,
UploadError_No_Free_Space, UploadError_No_Write_Permissions = 110,
UploadError_No_Write_Permissions, UploadError_ServerError = 111,
UploadError_ServerError, Count = UploadError_ServerError + 1,
Count,
}; };
explicit ClientStatusReporting(Account *account, QObject *parent = nullptr); explicit ClientStatusReporting(Account *account, QObject *parent = nullptr);
~ClientStatusReporting() = default; ~ClientStatusReporting();
private: private:
void init(); void init();
@ -62,22 +63,32 @@ private:
[[nodiscard]] Result<void, QString> setClientStatusReportingRecord(const ClientStatusReportingRecord &record); [[nodiscard]] Result<void, QString> setClientStatusReportingRecord(const ClientStatusReportingRecord &record);
[[nodiscard]] QVector<ClientStatusReportingRecord> getClientStatusReportingRecords() const; [[nodiscard]] QVector<ClientStatusReportingRecord> getClientStatusReportingRecords() const;
[[nodiscard]] bool deleteClientStatusReportingRecords(); [[nodiscard]] bool deleteClientStatusReportingRecords();
[[nodiscard]] bool setLastSentReportTimestamp(const qulonglong timestamp); void setLastSentReportTimestamp(const qulonglong timestamp);
[[nodiscard]] qulonglong getLastSentReportTimestamp() const; [[nodiscard]] qulonglong getLastSentReportTimestamp() const;
[[nodiscard]] QVariantMap prepareReport() const;
void reportToServerSentSuccessfully();
[[nodiscard]] QString makeDbPath() const;
private slots: private slots:
void sendReportToServer(); void sendReportToServer();
void slotSendReportToserverFinished();
private: private:
static QByteArray statusStringFromNumber(const Status status); static QByteArray statusStringFromNumber(const Status status);
static QString classifyStatus(const Status status); static QString classifyStatus(const Status status);
static int clientStatusReportingTrySendTimerInterval;
static int repordSendIntervalMs;
static QString dbPathForTesting;
Account *_account = nullptr; Account *_account = nullptr;
QHash<int, QPair<QByteArray, qint64>> _statusNamesAndHashes; QHash<int, QPair<QByteArray, qint64>> _statusNamesAndHashes;
QSqlDatabase _database; QSqlDatabase _database;
bool _isInitialized = false; bool _isInitialized = false;
QTimer _clientStatusReportingSendTimer; QTimer _clientStatusReportingSendTimer;
mutable QRecursiveMutex _mutex; mutable QRecursiveMutex _mutex;
friend class Account; friend class Account;
friend class TestClientStatusReporting;
}; };
} }

Просмотреть файл

@ -1713,11 +1713,13 @@ bool ProcessDirectoryJob::checkPermissions(const OCC::SyncFileItemPtr &item)
// No permissions set // No permissions set
return true; return true;
} else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) { } else if (item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddSubDirectories)) {
_discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions);
qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_instruction = CSYNC_INSTRUCTION_ERROR;
item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder"); item->_errorString = tr("Not allowed because you don't have permission to add subfolders to that folder");
return false; return false;
} else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) { } else if (!item->isDirectory() && !perms.hasPermission(RemotePermissions::CanAddFile)) {
_discoveryData->_account->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Write_Permissions);
qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file; qCWarning(lcDisco) << "checkForPermission: ERROR" << item->_file;
item->_instruction = CSYNC_INSTRUCTION_ERROR; item->_instruction = CSYNC_INSTRUCTION_ERROR;
item->_errorString = tr("Not allowed because you don't have permission to add files in that folder"); item->_errorString = tr("Not allowed because you don't have permission to add files in that folder");

Просмотреть файл

@ -229,29 +229,7 @@ void PropagateItemJob::done(const SyncFileItem::Status statusArg, const QString
_item->_status = statusArg; _item->_status = statusArg;
if (_item->_status == SyncFileItem::Status::Conflict) { reportClientStatuses();
if (_item->_direction == SyncFileItem::Direction::Up) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict);
} else {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict);
}
} else if (_item->_status == SyncFileItem::Status::FileNameClash) {
if (_item->_direction == SyncFileItem::Direction::Up) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters);
} else {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters);
}
} else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters);
} else if (_item->_status == SyncFileItem::Status::FileNameInvalid) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters);
} else if (_item->_httpErrorCode != 0 && _item->_httpErrorCode != 200) {
if (_item->_direction == SyncFileItem::Up) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError);
} else {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError);
}
}
if (_item->_isRestoration) { if (_item->_isRestoration) {
if (_item->_status == SyncFileItem::Success if (_item->_status == SyncFileItem::Success
@ -360,6 +338,33 @@ bool PropagateItemJob::hasEncryptedAncestor() const
return false; return false;
} }
void PropagateItemJob::reportClientStatuses()
{
if (_item->_status == SyncFileItem::Status::Conflict) {
if (_item->_direction == SyncFileItem::Direction::Up) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_Conflict);
} else {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Conflict);
}
} else if (_item->_status == SyncFileItem::Status::FileNameClash) {
if (_item->_direction == SyncFileItem::Direction::Up) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ConflictInvalidCharacters);
} else {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ConflictInvalidCharacters);
}
} else if (_item->_status == SyncFileItem::Status::FileNameInvalidOnServer) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::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) {
if (_item->_direction == SyncFileItem::Up) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::UploadError_ServerError);
} else {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_ServerError);
}
}
}
// ================================================================================ // ================================================================================
PropagateItemJob *OwncloudPropagator::createJob(const SyncFileItemPtr &item) PropagateItemJob *OwncloudPropagator::createJob(const SyncFileItemPtr &item)

Просмотреть файл

@ -194,6 +194,8 @@ protected slots:
void slotRestoreJobFinished(SyncFileItem::Status status); void slotRestoreJobFinished(SyncFileItem::Status status);
private: private:
void reportClientStatuses();
QScopedPointer<PropagateItemJob> _restoreJob; QScopedPointer<PropagateItemJob> _restoreJob;
JobParallelism _parallelism = FullParallelism; JobParallelism _parallelism = FullParallelism;

Просмотреть файл

@ -675,6 +675,7 @@ void PropagateDownloadFile::startDownload()
if (_tmpFile.exists()) if (_tmpFile.exists())
FileSystem::setFileReadOnly(_tmpFile.fileName(), false); FileSystem::setFileReadOnly(_tmpFile.fileName(), false);
if (!_tmpFile.open(QIODevice::Append | QIODevice::Unbuffered)) { if (!_tmpFile.open(QIODevice::Append | QIODevice::Unbuffered)) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File);
qCWarning(lcPropagateDownload) << "could not open temporary file" << _tmpFile.fileName(); qCWarning(lcPropagateDownload) << "could not open temporary file" << _tmpFile.fileName();
done(SyncFileItem::NormalError, _tmpFile.errorString(), ErrorCategory::GenericError); done(SyncFileItem::NormalError, _tmpFile.errorString(), ErrorCategory::GenericError);
return; return;
@ -1259,6 +1260,7 @@ void PropagateDownloadFile::downloadFinished()
emit propagator()->touchedFile(filename); emit propagator()->touchedFile(filename);
// The fileChanged() check is done above to generate better error messages. // The fileChanged() check is done above to generate better error messages.
if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) { if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) {
propagator()->account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_Cannot_Create_File);
qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(filename); 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 // If the file is locked, we want to retry this sync when it
// becomes available again, otherwise try again directly // becomes available again, otherwise try again directly

Просмотреть файл

@ -552,7 +552,6 @@ void SyncEngine::startSync()
.arg( .arg(
Utility::octetsToString(freeBytes), Utility::octetsToString(freeBytes),
Utility::octetsToString(minFree)), ErrorCategory::GenericError); Utility::octetsToString(minFree)), ErrorCategory::GenericError);
account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space);
finalize(false); finalize(false);
return; return;
} else { } else {
@ -1263,6 +1262,7 @@ void SyncEngine::slotSummaryError(const QString &message)
void SyncEngine::slotInsufficientLocalStorage() void SyncEngine::slotInsufficientLocalStorage()
{ {
account()->reportClientStatus(ClientStatusReporting::Status::DownloadError_No_Free_Space);
slotSummaryError( slotSummaryError(
tr("Disk space is low: Downloads that would reduce free space " tr("Disk space is low: Downloads that would reduce free space "
"below %1 were skipped.") "below %1 were skipped.")
@ -1271,8 +1271,8 @@ void SyncEngine::slotInsufficientLocalStorage()
void SyncEngine::slotInsufficientRemoteStorage() void SyncEngine::slotInsufficientRemoteStorage()
{ {
auto msg = tr("There is insufficient space available on the server for some uploads.");
account()->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Free_Space); account()->reportClientStatus(ClientStatusReporting::Status::UploadError_No_Free_Space);
auto msg = tr("There is insufficient space available on the server for some uploads.");
if (_uniqueErrors.contains(msg)) if (_uniqueErrors.contains(msg))
return; return;

Просмотреть файл

@ -464,6 +464,7 @@ void VfsCfApi::onHydrationJobFinished(HydrationJob *job)
qCInfo(lcCfApi) << "Hydration job finished" << job->requestId() << job->folderPath() << job->status(); qCInfo(lcCfApi) << "Hydration job finished" << job->requestId() << job->folderPath() << job->status();
emit hydrationRequestFinished(job->requestId()); emit hydrationRequestFinished(job->requestId());
if (!job->errorString().isEmpty()) { if (!job->errorString().isEmpty()) {
params().account->reportClientStatus(ClientStatusReporting::Status::DownloadError_Virtual_File_Hydration_Failure);
emit failureHydrating(job->errorCode(), job->statusCode(), job->errorString(), job->folderPath()); emit failureHydrating(job->errorCode(), job->statusCode(), job->errorString(), job->folderPath());
} }
} }

Просмотреть файл

@ -75,6 +75,7 @@ nextcloud_add_test(SecureFileDrop)
nextcloud_add_test(FileTagModel) nextcloud_add_test(FileTagModel)
nextcloud_add_test(SyncConflictsModel) nextcloud_add_test(SyncConflictsModel)
nextcloud_add_test(DateFieldBackend) nextcloud_add_test(DateFieldBackend)
nextcloud_add_test(ClientStatusReporting)
target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync) target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync)
configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY) configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY)

Просмотреть файл

@ -0,0 +1,131 @@
/*
* Copyright (C) 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 "account.h"
#include "accountstate.h"
#include "clientstatusreporting.h"
#include "syncenginetestutils.h"
#include <QSignalSpy>
#include <QTest>
namespace {
static QByteArray fake200Response = R"({"ocs":{"meta":{"status":"success","statuscode":200},"data":[]}})";
}
class TestClientStatusReporting : public QObject
{
Q_OBJECT
public:
TestClientStatusReporting() = default;
QScopedPointer<FakeQNAM> fakeQnam;
OCC::Account *account;
QScopedPointer<OCC::AccountState> accountState;
QString dbFilePath;
QVariantMap bodyReceivedAndParsed;
private slots:
void initTestCase()
{
OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval = 1000;
OCC::ClientStatusReporting::repordSendIntervalMs = 2000;
fakeQnam.reset(new FakeQNAM({}));
account = OCC::Account::create().get();
account->setCredentials(new FakeCredentials{fakeQnam.data()});
account->setUrl(QUrl(("http://example.de")));
accountState.reset(new OCC::AccountState(account->sharedFromThis()));
const auto databaseId = QStringLiteral("%1@%2").arg(account->davUser(), account->url().toString());
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;
QVariantMap capabilities;
capabilities[QStringLiteral("security_guard")] = QVariantMap{
{ QStringLiteral("diagnostics"), true }
};
account->setCapabilities(capabilities);
fakeQnam->setOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
QNetworkReply *reply = nullptr;
const auto reqBody = device->readAll();
bodyReceivedAndParsed = QJsonDocument::fromJson(reqBody).toVariant().toMap();
reply = new FakePayloadReply(op, req, fake200Response, fakeQnam.data());
return reply;
});
}
void testReportAndSendStatuses()
{
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);
// 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);
QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs);
QVERIFY(!bodyReceivedAndParsed.isEmpty());
// we must have "virus_detected" and "e2e_errors" keys present (as required by server)
QVERIFY(bodyReceivedAndParsed.contains("virus_detected"));
QVERIFY(bodyReceivedAndParsed.contains("e2e_errors"));
// we must have 5 conflicts
const auto conflictsReceived = bodyReceivedAndParsed.value("sync_conflicts").toMap();
QVERIFY(!conflictsReceived.isEmpty());
QCOMPARE(conflictsReceived.value("count"), 5);
// we must have 4 problems
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
QCOMPARE(problemsNoWritePermissions.value("count"), 3);
bodyReceivedAndParsed.clear();
}
}
void testNothingReportedAndNothingSent()
{
QTest::qWait(OCC::ClientStatusReporting::clientStatusReportingTrySendTimerInterval + OCC::ClientStatusReporting::repordSendIntervalMs);
QVERIFY(bodyReceivedAndParsed.isEmpty());
}
void cleanupTestCase()
{
accountState.reset(nullptr);
delete account;
QFile(dbFilePath).remove();
}
};
QTEST_MAIN(TestClientStatusReporting)
#include "testclientstatusreporting.moc"