syncthingtray/connector/syncthingconnection_requests.cpp
Martchus 25ae9266a8 Discard obsolete folder errors
* Discard folder errors older than the last "sync" state
* Unfortunately we don't know the the time of the last
  sync from the beginning. To prevent showing obsolete
  errors in this state, discard all errors if the status
  changes to something other than "out-of-syn".
2018-11-02 21:47:27 +01:00

1952 lines
70 KiB
C++

#include "./syncthingconnection.h"
#include "./utils.h"
#ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED
#include "./syncthingconnectionmockhelpers.h"
#endif
#include <c++utilities/conversion/conversionexception.h>
#include <c++utilities/conversion/stringconversion.h>
#if defined(LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS) || defined(LIB_SYNCTHING_CONNECTOR_LOG_API_CALLS)
#include <c++utilities/io/ansiescapecodes.h>
#endif
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonValue>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QStringBuilder>
#include <QTimer>
#include <QUrlQuery>
#include <iostream>
#include <utility>
using namespace std;
using namespace ChronoUtilities;
using namespace ConversionUtilities;
#if defined(LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS) || defined(LIB_SYNCTHING_CONNECTOR_LOG_API_CALLS)
using namespace EscapeCodes;
#endif
namespace Data {
// helper to create QNetworkRequest
/*!
* \brief Prepares a request for the specified \a path and \a query.
*/
QNetworkRequest SyncthingConnection::prepareRequest(const QString &path, const QUrlQuery &query, bool rest)
{
QUrl url(m_syncthingUrl);
url.setPath(rest ? (url.path() % QStringLiteral("/rest/") % path) : (url.path() + path));
url.setUserName(user());
url.setPassword(password());
url.setQuery(query);
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, QByteArray("application/x-www-form-urlencoded"));
request.setRawHeader("X-API-Key", m_apiKey);
return request;
}
/*!
* \brief Requests asynchronously data using the rest API.
*/
QNetworkReply *SyncthingConnection::requestData(const QString &path, const QUrlQuery &query, bool rest)
{
#ifndef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED
auto *const reply = networkAccessManager().get(prepareRequest(path, query, rest));
#ifdef LIB_SYNCTHING_CONNECTOR_LOG_API_CALLS
cerr << Phrases::Info << "GETing: " << reply->url().toString().toStdString() << Phrases::EndFlush;
#endif
reply->ignoreSslErrors(m_expectedSslErrors);
return reply;
#else
return MockedReply::forRequest(QStringLiteral("GET"), path, query, rest);
#endif
}
/*!
* \brief Posts asynchronously data using the rest API.
*/
QNetworkReply *SyncthingConnection::postData(const QString &path, const QUrlQuery &query, const QByteArray &data)
{
auto *const reply = networkAccessManager().post(prepareRequest(path, query), data);
#ifdef LIB_SYNCTHING_CONNECTOR_LOG_API_CALLS
cerr << Phrases::Info << "POSTing: " << reply->url().toString().toStdString() << Phrases::End << data.data() << endl;
#endif
reply->ignoreSslErrors(m_expectedSslErrors);
return reply;
}
// pause/resume devices
/*!
* \brief Requests pausing the devices with the specified IDs.
*
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::pauseDevice(const QStringList &devIds)
{
return pauseResumeDevice(devIds, true);
}
/*!
* \brief Requests pausing all devices.
*
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::pauseAllDevs()
{
return pauseResumeDevice(deviceIds(), true);
}
/*!
* \brief Requests resuming the devices with the specified IDs.
*
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::resumeDevice(const QStringList &devIds)
{
return pauseResumeDevice(devIds, false);
}
/*!
* \brief Requests resuming all devices.
*
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::resumeAllDevs()
{
return pauseResumeDevice(deviceIds(), false);
}
/*!
* \brief Internally used to pause/resume directories.
* \returns Returns whether a request has been made.
* \remarks This might currently result in errors caused by Syncthing not
* handling E notation correctly when using Qt < 5.9:
* https://github.com/syncthing/syncthing/issues/4001
*/
bool SyncthingConnection::pauseResumeDevice(const QStringList &devIds, bool paused)
{
if (devIds.isEmpty()) {
return false;
}
if (!isConnected()) {
emit error(tr("Unable to pause/resume a devices when not connected"), SyncthingErrorCategory::SpecificRequest, QNetworkReply::NoError);
return false;
}
QJsonObject config = m_rawConfig;
if (!setDevicesPaused(config, devIds, paused)) {
return false;
}
QJsonDocument doc;
doc.setObject(config);
QNetworkReply *const reply = postData(QStringLiteral("system/config"), QUrlQuery(), doc.toJson(QJsonDocument::Compact));
reply->setProperty("devIds", devIds);
reply->setProperty("resume", !paused);
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDevPauseResume);
return true;
}
/*!
* \brief Reads results of pauseDevice() and resumeDevice().
*/
void SyncthingConnection::readDevPauseResume()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError: {
const QStringList devIds(reply->property("devIds").toStringList());
const bool resume = reply->property("resume").toBool();
setDevicesPaused(m_rawConfig, devIds, !resume);
if (reply->property("resume").toBool()) {
emit deviceResumeTriggered(devIds);
} else {
emit devicePauseTriggered(devIds);
}
break;
}
default:
emitError(tr("Unable to request device pause/resume: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
// pause/resume directories
/*!
* \brief Pauses the directories with the specified IDs.
* \remarks Calling this method when not connected results in an error because the *current* Syncthing config must
* be available for this call.
* \returns Returns whether a request has been made.
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::pauseDirectories(const QStringList &dirIds)
{
return pauseResumeDirectory(dirIds, true);
}
/*!
* \brief Pauses all directories.
* \remarks Calling this method when not connected results in an error because the *current* Syncthing config must
* be available for this call.
*
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::pauseAllDirs()
{
return pauseResumeDirectory(directoryIds(), true);
}
/*!
* \brief Resumes the directories with the specified IDs.
* \remarks Calling this method when not connected results in an error because the *current* Syncthing config must
* be available for this call.
*
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::resumeDirectories(const QStringList &dirIds)
{
return pauseResumeDirectory(dirIds, false);
}
/*!
* \brief Resumes all directories.
* \remarks Calling this method when not connected results in an error because the *current* Syncthing config must
* be available for this call.
*
* The signal error() is emitted when the request was not successful.
*/
bool SyncthingConnection::resumeAllDirs()
{
return pauseResumeDirectory(directoryIds(), false);
}
/*!
* \brief Internally used to pause/resume directories.
* \returns Returns whether a request has been made.
* \remarks This might currently result in errors caused by Syncthing not
* handling E notation correctly when using Qt < 5.9:
* https://github.com/syncthing/syncthing/issues/4001
*/
bool SyncthingConnection::pauseResumeDirectory(const QStringList &dirIds, bool paused)
{
if (dirIds.isEmpty()) {
return false;
}
if (!isConnected()) {
emit error(tr("Unable to pause/resume a directories when not connected"), SyncthingErrorCategory::SpecificRequest, QNetworkReply::NoError);
return false;
}
QJsonObject config = m_rawConfig;
if (setDirectoriesPaused(config, dirIds, paused)) {
QJsonDocument doc;
doc.setObject(config);
QNetworkReply *const reply = postData(QStringLiteral("system/config"), QUrlQuery(), doc.toJson(QJsonDocument::Compact));
reply->setProperty("dirIds", dirIds);
reply->setProperty("resume", !paused);
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirPauseResume);
return true;
}
return false;
}
void SyncthingConnection::readDirPauseResume()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError: {
const QStringList dirIds(reply->property("dirIds").toStringList());
const bool resume = reply->property("resume").toBool();
setDirectoriesPaused(m_rawConfig, dirIds, !resume);
if (resume) {
emit directoryResumeTriggered(dirIds);
} else {
emit directoryPauseTriggered(dirIds);
}
break;
}
default:
emitError(tr("Unable to request directory pause/resume: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
// rescan directories
/*!
* \brief Requests rescanning all directories.
*
* Note that rescan is only requested for unpaused directories because requesting rescan for
* paused directories only leads to an error.
*
* The signal error() is emitted when the request was not successful.
*/
void SyncthingConnection::rescanAllDirs()
{
for (const SyncthingDir &dir : m_dirs) {
if (!dir.paused) {
rescan(dir.id);
}
}
}
/*!
* \brief Requests rescanning the directory with the specified ID.
*
* The signal error() is emitted when the request was not successful.
*/
void SyncthingConnection::rescan(const QString &dirId, const QString &relpath)
{
if (dirId.isEmpty()) {
emit error(tr("Unable to rescan: No directory ID specified."), SyncthingErrorCategory::SpecificRequest, QNetworkReply::NoError,
QNetworkRequest(), QByteArray());
return;
}
QUrlQuery query;
query.addQueryItem(QStringLiteral("folder"), dirId);
if (!relpath.isEmpty()) {
query.addQueryItem(QStringLiteral("sub"), relpath);
}
QNetworkReply *reply = postData(QStringLiteral("db/scan"), query);
reply->setProperty("dirId", dirId);
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readRescan);
}
/*!
* \brief Reads results of rescan().
*/
void SyncthingConnection::readRescan()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError:
emit rescanTriggered(reply->property("dirId").toString());
break;
default:
emitError(tr("Unable to request rescan: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
// restart/shutdown Syncthing
/*!
* \brief Requests Syncthing to restart.
*
* The signal error() is emitted when the request was not successful.
*/
void SyncthingConnection::restart()
{
QObject::connect(postData(QStringLiteral("system/restart"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readRestart);
}
/*!
* \brief Reads results of restart().
*/
void SyncthingConnection::readRestart()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError:
emit restartTriggered();
break;
default:
emitError(tr("Unable to request restart: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Requests Syncthing to exit and not restart.
*
* The signal error() is emitted when the request was not successful.
*/
void SyncthingConnection::shutdown()
{
QObject::connect(postData(QStringLiteral("system/shutdown"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readShutdown);
}
/*!
* \brief Reads results of shutdown().
*/
void SyncthingConnection::readShutdown()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError:
emit shutdownTriggered();
break;
default:
emitError(tr("Unable to request shutdown: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
// clear errors
/*!
* \brief Requests clearing errors asynchronously.
*
* The signal error() is emitted in the error case.
*/
void SyncthingConnection::requestClearingErrors()
{
QObject::connect(
postData(QStringLiteral("system/error/clear"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readClearingErrors);
}
/*!
* \brief Reads results of requestClearingErrors().
*/
void SyncthingConnection::readClearingErrors()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError:
break;
default:
emitError(tr("Unable to request clearing errors: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
// overall Syncthing config (most importantly settings, directories and devices)
/*!
* \brief Requests the Syncthing configuration asynchronously.
*
* The signal newConfig() is emitted on success; otherwise error() is emitted.
*/
void SyncthingConnection::requestConfig()
{
if (m_configReply) {
return;
}
QObject::connect(
m_configReply = requestData(QStringLiteral("system/config"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readConfig);
}
/*!
* \brief Reads results of requestConfig().
*/
void SyncthingConnection::readConfig()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_configReply) {
m_configReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse Syncthing config: "), jsonError, reply, response);
handleFatalConnectionError();
return;
}
m_rawConfig = replyDoc.object();
m_hasConfig = true;
emit newConfig(m_rawConfig);
if (m_keepPolling) {
concludeReadingConfigAndStatus();
return;
}
readDevs(m_rawConfig.value(QLatin1String("devices")).toArray());
readDirs(m_rawConfig.value(QLatin1String("folders")).toArray());
break;
}
case QNetworkReply::OperationCanceledError:
return;
default:
emitError(tr("Unable to request Syncthing config: "), SyncthingErrorCategory::OverallConnection, reply);
handleFatalConnectionError();
}
}
/*!
* \brief Reads directory results of requestConfig(); called by readConfig().
* \remarks
* - The devs are required to resolve the names of the devices a directory is shared with.
* So when parsing the config, readDevs() should be called first.
* - The own device ID is required to filter it from the devices a directory is shared with.
* So the readStatus() should have been called first.
*/
void SyncthingConnection::readDirs(const QJsonArray &dirs)
{
// store the new dirs in a temporary list which is assigned to m_dirs later
std::vector<SyncthingDir> newDirs;
newDirs.reserve(static_cast<size_t>(dirs.size()));
int dummy;
for (const QJsonValue &dirVal : dirs) {
const QJsonObject dirObj(dirVal.toObject());
SyncthingDir *const dirItem = addDirInfo(newDirs, dirObj.value(QLatin1String("id")).toString());
if (!dirItem) {
continue;
}
dirItem->label = dirObj.value(QLatin1String("label")).toString();
dirItem->path = dirObj.value(QLatin1String("path")).toString();
dirItem->deviceIds.clear();
dirItem->deviceNames.clear();
for (const QJsonValueRef dev : dirObj.value(QLatin1String("devices")).toArray()) {
const QString devId = dev.toObject().value(QLatin1String("deviceID")).toString();
if (devId.isEmpty() || devId == m_myId) {
continue;
}
dirItem->deviceIds << devId;
if (const SyncthingDev *const dev = findDevInfo(devId, dummy)) {
dirItem->deviceNames << dev->name;
}
}
dirItem->assignDirType(dirObj.value(QLatin1String("type")).toString());
dirItem->rescanInterval = dirObj.value(QLatin1String("rescanIntervalS")).toInt(-1);
dirItem->ignorePermissions = dirObj.value(QLatin1String("ignorePerms")).toBool(false);
dirItem->ignoreDelete = dirObj.value(QLatin1String("ignoreDelete")).toBool(false);
dirItem->autoNormalize = dirObj.value(QLatin1String("autoNormalize")).toBool(false);
dirItem->minDiskFreePercentage = dirObj.value(QLatin1String("minDiskFreePct")).toInt(-1);
dirItem->paused = dirObj.value(QLatin1String("paused")).toBool(dirItem->paused);
dirItem->fileSystemWatcherEnabled = dirObj.value(QLatin1String("fsWatcherEnabled")).toBool(false);
dirItem->fileSystemWatcherDelay = dirObj.value(QLatin1String("fsWatcherDelayS")).toDouble(0.0);
}
m_dirs.swap(newDirs);
emit this->newDirs(m_dirs);
}
/*!
* \brief Reads device results of requestConfig(); called by readConfig().
*/
void SyncthingConnection::readDevs(const QJsonArray &devs)
{
// store the new devs in a temporary list which is assigned to m_devs later
vector<SyncthingDev> newDevs;
newDevs.reserve(static_cast<size_t>(devs.size()));
for (const QJsonValue &devVal : devs) {
const QJsonObject devObj(devVal.toObject());
SyncthingDev *const devItem = addDevInfo(newDevs, devObj.value(QLatin1String("deviceID")).toString());
if (!devItem) {
continue;
}
devItem->name = devObj.value(QLatin1String("name")).toString();
devItem->addresses = things(devObj.value(QLatin1String("addresses")).toArray(), [](const QJsonValue &value) { return value.toString(); });
devItem->compression = devObj.value(QLatin1String("compression")).toString();
devItem->certName = devObj.value(QLatin1String("certName")).toString();
devItem->introducer = devObj.value(QLatin1String("introducer")).toBool(false);
devItem->status = devItem->id == m_myId ? SyncthingDevStatus::OwnDevice : SyncthingDevStatus::Unknown;
devItem->paused = devObj.value(QLatin1String("paused")).toBool(devItem->paused);
}
m_devs.swap(newDevs);
emit this->newDevices(m_devs);
}
// status of Syncthing (own ID, startup time)
/*!
* \brief Requests the Syncthing status asynchronously.
*
* The signals myIdChanged() are emitted when those values have changed; error() is emitted in the error case.
*/
void SyncthingConnection::requestStatus()
{
if (m_statusReply) {
return;
}
QObject::connect(
m_statusReply = requestData(QStringLiteral("system/status"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readStatus);
}
/*!
* \brief Reads results of requestStatus().
*/
void SyncthingConnection::readStatus()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_statusReply) {
m_statusReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const auto replyDoc(QJsonDocument::fromJson(response, &jsonError));
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse Syncthing status: "), jsonError, reply, response);
handleFatalConnectionError();
return;
}
const auto replyObj(replyDoc.object());
emitMyIdChanged(replyObj.value(QLatin1String("myID")).toString());
m_startTime = DateTime::fromIsoStringGmt(replyObj.value(QLatin1String("startTime")).toString().toLocal8Bit().data());
m_hasStatus = true;
if (m_keepPolling) {
concludeReadingConfigAndStatus();
}
break;
}
case QNetworkReply::OperationCanceledError:
return;
default:
emitError(tr("Unable to request Syncthing status: "), SyncthingErrorCategory::OverallConnection, reply);
handleFatalConnectionError();
}
}
/*!
* \brief Requests the Syncthing configuration and status asynchronously.
*
* \sa requestConfig() and requestStatus() for emitted signals.
*/
void SyncthingConnection::requestConfigAndStatus()
{
requestConfig();
requestStatus();
}
// further info (connections, errors, ...)
/*!
* \brief Requests current connections asynchronously.
*
* The signal devStatusChanged() is emitted for each device where the connection status has changed; error() is emitted in the error case.
*/
void SyncthingConnection::requestConnections()
{
if (m_connectionsReply) {
return;
}
QObject::connect(m_connectionsReply = requestData(QStringLiteral("system/connections"), QUrlQuery()), &QNetworkReply::finished, this,
&SyncthingConnection::readConnections);
}
/*!
* \brief Reads results of requestConnections().
*/
void SyncthingConnection::readConnections()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_connectionsReply) {
m_connectionsReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse connections: "), jsonError, reply, response);
return;
}
const QJsonObject replyObj(replyDoc.object());
const QJsonObject totalObj(replyObj.value(QLatin1String("total")).toObject());
// read traffic, the conversion to double is neccassary because toInt() doesn't work for high values
const QJsonValue totalIncomingTrafficValue(totalObj.value(QLatin1String("inBytesTotal")));
const QJsonValue totalOutgoingTrafficValue(totalObj.value(QLatin1String("outBytesTotal")));
const uint64 totalIncomingTraffic = totalIncomingTrafficValue.isDouble() ? jsonValueToInt(totalIncomingTrafficValue) : unknownTraffic;
const uint64 totalOutgoingTraffic = totalOutgoingTrafficValue.isDouble() ? jsonValueToInt(totalOutgoingTrafficValue) : unknownTraffic;
double transferTime = 0.0;
const bool hasDelta
= !m_lastConnectionsUpdate.isNull() && ((transferTime = (DateTime::gmtNow() - m_lastConnectionsUpdate).totalSeconds()) != 0.0);
m_totalIncomingRate = (hasDelta && totalIncomingTraffic != unknownTraffic && m_totalIncomingTraffic != unknownTraffic)
? (totalIncomingTraffic - m_totalIncomingTraffic) * 0.008 / transferTime
: 0.0;
m_totalOutgoingRate = (hasDelta && totalOutgoingTraffic != unknownTraffic && m_totalOutgoingTraffic != unknownTraffic)
? (totalOutgoingTraffic - m_totalOutgoingTraffic) * 0.008 / transferTime
: 0.0;
emit trafficChanged(m_totalIncomingTraffic = totalIncomingTraffic, m_totalOutgoingTraffic = totalOutgoingTraffic);
// read connection status
const QJsonObject connectionsObj(replyObj.value(QLatin1String("connections")).toObject());
int index = 0;
for (SyncthingDev &dev : m_devs) {
const QJsonObject connectionObj(connectionsObj.value(dev.id).toObject());
if (connectionObj.isEmpty()) {
++index;
continue;
}
switch (dev.status) {
case SyncthingDevStatus::OwnDevice:
break;
case SyncthingDevStatus::Disconnected:
case SyncthingDevStatus::Unknown:
if (connectionObj.value(QLatin1String("connected")).toBool(false)) {
dev.status = SyncthingDevStatus::Idle;
} else {
dev.status = SyncthingDevStatus::Disconnected;
}
break;
default:
if (!connectionObj.value(QLatin1String("connected")).toBool(false)) {
dev.status = SyncthingDevStatus::Disconnected;
}
}
dev.paused = connectionObj.value(QLatin1String("paused")).toBool(false);
dev.totalIncomingTraffic = jsonValueToInt(connectionObj.value(QLatin1String("inBytesTotal")));
dev.totalOutgoingTraffic = jsonValueToInt(connectionObj.value(QLatin1String("outBytesTotal")));
dev.connectionAddress = connectionObj.value(QLatin1String("address")).toString();
dev.connectionType = connectionObj.value(QLatin1String("type")).toString();
dev.clientVersion = connectionObj.value(QLatin1String("clientVersion")).toString();
emit devStatusChanged(dev, index);
++index;
}
m_lastConnectionsUpdate = DateTime::gmtNow();
// since there seems no event for this data, keep polling
if (m_keepPolling) {
concludeConnection();
if (m_trafficPollTimer.interval()) {
m_trafficPollTimer.start();
}
}
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request connections: "), SyncthingErrorCategory::OverallConnection, reply);
}
}
/*!
* \brief Requests errors asynchronously.
*
* The signal newNotification() is emitted on success; error() is emitted in the error case.
*/
void SyncthingConnection::requestErrors()
{
if (m_errorsReply) {
return;
}
QObject::connect(
m_errorsReply = requestData(QStringLiteral("system/error"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readErrors);
}
/*!
* \brief Reads results of requestErrors().
*/
void SyncthingConnection::readErrors()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_errorsReply) {
m_errorsReply = nullptr;
}
// ignore any errors occured before connecting
if (m_lastErrorTime.isNull()) {
m_lastErrorTime = DateTime::now();
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse errors: "), jsonError, reply, response);
return;
}
for (const QJsonValueRef errorVal : replyDoc.object().value(QLatin1String("errors")).toArray()) {
const QJsonObject errorObj(errorVal.toObject());
if (errorObj.isEmpty()) {
continue;
}
try {
const DateTime when = DateTime::fromIsoStringLocal(errorObj.value(QLatin1String("when")).toString().toLocal8Bit().data());
if (m_lastErrorTime < when) {
emitNotification(m_lastErrorTime = when, errorObj.value(QLatin1String("message")).toString());
}
} catch (const ConversionException &) {
}
}
// since there seems no event for this data, keep polling
if (m_keepPolling) {
concludeConnection();
if (m_errorsPollTimer.interval()) {
m_errorsPollTimer.start();
}
}
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request errors: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Requests statistics (last file, last scan) for all directories asynchronously.
*/
void SyncthingConnection::requestDirStatistics()
{
if (m_dirStatsReply) {
return;
}
QObject::connect(m_dirStatsReply = requestData(QStringLiteral("stats/folder"), QUrlQuery()), &QNetworkReply::finished, this,
&SyncthingConnection::readDirStatistics);
}
/*!
* \brief Reads results of requestDirStatistics().
*/
void SyncthingConnection::readDirStatistics()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_dirStatsReply) {
m_dirStatsReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse directory statistics: "), jsonError, reply, response);
return;
}
const QJsonObject replyObj(replyDoc.object());
int index = 0;
for (SyncthingDir &dirInfo : m_dirs) {
const QJsonObject dirObj(replyObj.value(dirInfo.id).toObject());
if (dirObj.isEmpty()) {
++index;
continue;
}
bool dirModified = false;
try {
dirInfo.lastScanTime = DateTime::fromIsoStringLocal(dirObj.value(QLatin1String("lastScan")).toString().toUtf8().data());
dirModified = true;
} catch (const ConversionException &) {
dirInfo.lastScanTime = DateTime();
}
const QJsonObject lastFileObj(dirObj.value(QLatin1String("lastFile")).toObject());
if (!lastFileObj.isEmpty()) {
dirInfo.lastFileName = lastFileObj.value(QLatin1String("filename")).toString();
dirModified = true;
if (!dirInfo.lastFileName.isEmpty()) {
dirInfo.lastFileDeleted = lastFileObj.value(QLatin1String("deleted")).toBool(false);
try {
dirInfo.lastFileTime = DateTime::fromIsoStringLocal(lastFileObj.value(QLatin1String("at")).toString().toUtf8().data());
if (dirInfo.lastFileTime > m_lastFileTime) {
m_lastFileTime = dirInfo.lastFileTime;
m_lastFileName = dirInfo.lastFileName;
m_lastFileDeleted = dirInfo.lastFileDeleted;
}
} catch (const ConversionException &) {
dirInfo.lastFileTime = DateTime();
}
}
}
if (dirModified) {
emit dirStatusChanged(dirInfo, index);
}
++index;
}
if (m_keepPolling) {
concludeConnection();
}
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request directory statistics: "), SyncthingErrorCategory::OverallConnection, reply);
}
}
/*!
* \brief Requests statistics (global and local status) for \a dirId asynchronously.
*/
void SyncthingConnection::requestDirStatus(const QString &dirId)
{
QUrlQuery query;
query.addQueryItem(QStringLiteral("folder"), dirId);
auto *const reply = requestData(QStringLiteral("db/status"), query);
reply->setProperty("dirId", dirId);
m_otherReplies << reply;
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirStatus);
}
/*!
* \brief Reads data from requestDirStatus().
*/
void SyncthingConnection::readDirStatus()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
m_otherReplies.removeAll(reply);
switch (reply->error()) {
case QNetworkReply::NoError: {
// determine relevant dir
int index;
const QString dirId(reply->property("dirId").toString());
SyncthingDir *const dir = findDirInfo(dirId, index);
if (!dir) {
// discard status for unknown dirs
return;
}
// parse JSON
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse status for directory %1: ").arg(dirId), jsonError, reply, response);
return;
}
if (readDirSummary(DateTime::gmtNow(), replyDoc.object(), *dir, index)) {
recalculateStatus();
}
if (m_keepPolling) {
concludeConnection();
}
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request directory statistics: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Requests completion for \a devId and \a dirId asynchronously.
*/
void SyncthingConnection::requestCompletion(const QString &devId, const QString &dirId)
{
QUrlQuery query;
query.addQueryItem(QStringLiteral("device"), devId);
query.addQueryItem(QStringLiteral("folder"), dirId);
auto *const reply = requestData(QStringLiteral("db/completion"), query);
reply->setProperty("devId", devId);
reply->setProperty("dirId", dirId);
m_otherReplies << reply;
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readCompletion);
}
/*!
* \brief Reads data from requestCompletion().
*/
void SyncthingConnection::readCompletion()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
const auto devId(reply->property("devId").toString());
const auto dirId(reply->property("dirId").toString());
reply->deleteLater();
m_otherReplies.removeAll(reply);
switch (reply->error()) {
case QNetworkReply::NoError: {
// determine relevant dev/dir
int index;
auto *const dir = findDirInfo(dirId, index);
// discard status for unknown dirs
if (!dir) {
return;
}
// parse JSON
QJsonParseError jsonError;
const auto response(reply->readAll());
const auto replyDoc(QJsonDocument::fromJson(response, &jsonError));
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse completion for device/directory %1/%2: ").arg(devId, dirId), jsonError, reply, response);
return;
}
// update the relevant completion info
readRemoteFolderCompletion(DateTime::gmtNow(), replyDoc.object(), *dir, index, devId);
concludeConnection();
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request completion for device/directory %1/%2: ").arg(devId, dirId), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Requests device statistics asynchronously.
*/
void SyncthingConnection::requestDeviceStatistics()
{
if (m_devStatsReply) {
return;
}
QObject::connect(
requestData(QStringLiteral("stats/device"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readDeviceStatistics);
}
/*!
* \brief Reads results of requestDeviceStatistics().
*/
void SyncthingConnection::readDeviceStatistics()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_devStatsReply) {
m_devStatsReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse device statistics: "), jsonError, reply, response);
return;
}
const QJsonObject replyObj(replyDoc.object());
int index = 0;
for (SyncthingDev &devInfo : m_devs) {
const QJsonObject devObj(replyObj.value(devInfo.id).toObject());
if (!devObj.isEmpty()) {
try {
devInfo.lastSeen = DateTime::fromIsoStringLocal(devObj.value(QLatin1String("lastSeen")).toString().toUtf8().data());
emit devStatusChanged(devInfo, index);
} catch (const ConversionException &) {
devInfo.lastSeen = DateTime();
}
}
++index;
}
// since there seems no event for this data, keep polling
if (m_keepPolling) {
concludeConnection();
if (m_devStatsPollTimer.interval()) {
m_devStatsPollTimer.start();
}
}
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request device statistics: "), SyncthingErrorCategory::OverallConnection, reply);
}
}
void SyncthingConnection::requestVersion()
{
if (m_versionReply) {
return;
}
QObject::connect(m_versionReply = requestData(QStringLiteral("system/version"), QUrlQuery()), &QNetworkReply::finished, this,
&SyncthingConnection::readVersion);
}
/*!
* \brief Reads data from requestVersion().
*/
void SyncthingConnection::readVersion()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_versionReply) {
m_versionReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const auto replyDoc(QJsonDocument::fromJson(response, &jsonError));
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse version: "), jsonError, reply, response);
return;
}
const auto replyObj(replyDoc.object());
m_syncthingVersion = replyObj.value(QLatin1String("longVersion")).toString();
if (m_keepPolling) {
concludeConnection();
}
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request version: "), SyncthingErrorCategory::OverallConnection, reply);
}
}
/*!
* \brief Requests a QR code for the specified \a text.
*
* qrCodeAvailable() is emitted on success; otherwise error() is emitted.
*/
void SyncthingConnection::requestQrCode(const QString &text)
{
QUrlQuery query;
query.addQueryItem(QStringLiteral("text"), text);
QNetworkReply *reply = requestData(QStringLiteral("/qr/"), query, false);
reply->setProperty("qrText", text);
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readQrCode);
}
/*!
* \brief Reads the QR code queried via requestQrCode().
*/
void SyncthingConnection::readQrCode()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError:
emit qrCodeAvailable(reply->property("qrText").toString(), reply->readAll());
break;
case QNetworkReply::OperationCanceledError:
break;
default:
emit error(tr("Unable to request QR-Code: ") + reply->errorString(), SyncthingErrorCategory::SpecificRequest, reply->error());
}
}
/*!
* \brief Requests the Syncthing log.
*
* logAvailable() is emitted on success; otherwise error() is emitted.
*/
void SyncthingConnection::requestLog()
{
if (m_logReply) {
return;
}
QObject::connect(
m_logReply = requestData(QStringLiteral("system/log"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readLog);
}
/*!
* \brief Reads log entries queried via requestLog().
*/
void SyncthingConnection::readLog()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_logReply) {
m_logReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emit error(tr("Unable to parse Syncthing log: ") + jsonError.errorString(), SyncthingErrorCategory::Parsing, QNetworkReply::NoError);
return;
}
const QJsonArray log(replyDoc.object().value(QLatin1String("messages")).toArray());
vector<SyncthingLogEntry> logEntries;
logEntries.reserve(static_cast<size_t>(log.size()));
for (const QJsonValue &logVal : log) {
const QJsonObject logObj(logVal.toObject());
logEntries.emplace_back(logObj.value(QLatin1String("when")).toString(), logObj.value(QLatin1String("message")).toString());
}
emit logAvailable(logEntries);
break;
}
case QNetworkReply::OperationCanceledError:
break;
default:
emit error(tr("Unable to request Syncthing log: ") + reply->errorString(), SyncthingErrorCategory::SpecificRequest, reply->error());
}
}
// post config
/*!
* \brief Posts the specified \a rawConfig.
* \remarks The signal newConfigTriggered() is emitted when the config has been posted sucessfully. In the error case, error() is emitted.
* Besides, the newConfig() signal should be emitted as well, indicating Syncthing has actually applied the new configuration.
*/
void SyncthingConnection::postConfigFromJsonObject(const QJsonObject &rawConfig)
{
QObject::connect(postData(QStringLiteral("system/config"), QUrlQuery(), QJsonDocument(rawConfig).toJson(QJsonDocument::Compact)),
&QNetworkReply::finished, this, &SyncthingConnection::readPostConfig);
}
/*!
* \brief Posts the specified \a rawConfig.
* \param rawConfig A valid JSON document containing the configuration. It is directly passed to Syncthing.
* \remarks The signal newConfigTriggered() is emitted when the config has been posted sucessfully. In the error case, error() is emitted.
* Besides, the newConfig() signal should be emitted as well, indicating Syncthing has actually applied the new configuration.
*/
void SyncthingConnection::postConfigFromByteArray(const QByteArray &rawConfig)
{
QObject::connect(
postData(QStringLiteral("system/config"), QUrlQuery(), rawConfig), &QNetworkReply::finished, this, &SyncthingConnection::readPostConfig);
}
/*!
* \brief Reads data from postConfigFromJsonObject() and postConfigFromByteArray().
*/
void SyncthingConnection::readPostConfig()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
switch (reply->error()) {
case QNetworkReply::NoError:
emit newConfigTriggered();
break;
default:
emitError(tr("Unable to post config: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Reads data from requestDirStatus() and FolderSummary-event and stores them to \a dir.
*/
bool SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &summary, SyncthingDir &dir, int index)
{
if (summary.isEmpty() || dir.lastStatisticsUpdate > eventTime) {
return false;
}
// backup previous statistics -> if there's no difference after all, don't emit completed event
auto &globalStats(dir.globalStats);
auto &localStats(dir.localStats);
auto &neededStats(dir.neededStats);
const auto previouslyUpdated(!dir.lastStatisticsUpdate.isNull());
const auto previouslyGlobal(globalStats);
const auto previouslyNeeded(neededStats);
// update statistics
globalStats.bytes = jsonValueToInt(summary.value(QLatin1String("globalBytes")));
globalStats.deletes = jsonValueToInt(summary.value(QLatin1String("globalDeleted")));
globalStats.files = jsonValueToInt(summary.value(QLatin1String("globalFiles")));
globalStats.dirs = jsonValueToInt(summary.value(QLatin1String("globalDirectories")));
globalStats.symlinks = jsonValueToInt(summary.value(QLatin1String("globalSymlinks")));
localStats.bytes = jsonValueToInt(summary.value(QLatin1String("localBytes")));
localStats.deletes = jsonValueToInt(summary.value(QLatin1String("localDeleted")));
localStats.files = jsonValueToInt(summary.value(QLatin1String("localFiles")));
localStats.dirs = jsonValueToInt(summary.value(QLatin1String("localDirectories")));
localStats.symlinks = jsonValueToInt(summary.value(QLatin1String("localSymlinks")));
neededStats.bytes = jsonValueToInt(summary.value(QLatin1String("needBytes")));
neededStats.deletes = jsonValueToInt(summary.value(QLatin1String("needDeletes")));
neededStats.files = jsonValueToInt(summary.value(QLatin1String("needFiles")));
neededStats.dirs = jsonValueToInt(summary.value(QLatin1String("needDirectories")));
neededStats.symlinks = jsonValueToInt(summary.value(QLatin1String("needSymlinks")));
dir.ignorePatterns = summary.value(QLatin1String("ignorePatterns")).toBool();
dir.lastStatisticsUpdate = eventTime;
// update status
bool stateChanged = false;
const QString state(summary.value(QLatin1String("state")).toString());
if (!state.isEmpty()) {
try {
dir.assignStatus(state, DateTime::fromIsoStringGmt(summary.value(QLatin1String("stateChanged")).toString().toUtf8().data()));
} catch (const ConversionException &) {
// FIXME: warning about invalid stateChanged
}
}
dir.completionPercentage = globalStats.bytes ? static_cast<int>((globalStats.bytes - neededStats.bytes) * 100 / globalStats.bytes) : 100;
emit dirStatusChanged(dir, index);
if (neededStats.isNull() && previouslyUpdated && (neededStats != previouslyNeeded || globalStats != previouslyGlobal)) {
emit dirCompleted(eventTime, dir, index);
}
return stateChanged;
}
/*!
* \brief Reads data from "FolderRejected"-event.
*/
void SyncthingConnection::readDirRejected(DateTime eventTime, const QString &dirId, const QJsonObject &eventData)
{
// ignore if dir has already been added
int row;
const auto *const dir(findDirInfo(dirId, row));
if (dir) {
return;
}
// emit newDirAvailable() signal
const auto dirLabel(eventData.value(QLatin1String("folderLabel")).toString());
const auto devId(eventData.value(QLatin1String("device")).toString());
const auto *const device(findDevInfo(devId, row));
emit newDirAvailable(eventTime, devId, device, dirId, dirLabel);
}
void SyncthingConnection::readDevRejected(DateTime eventTime, const QString &devId, const QJsonObject &eventData)
{
// ignore if dev has already been added
int row;
const auto *const dev(findDevInfo(devId, row));
if (dev) {
return;
}
// emit newDevAvailable() signal
emit newDevAvailable(eventTime, devId, eventData.value(QLatin1String("address")).toString());
}
/*!
* \brief Reads "LocalChangeDetected" and "RemoveChangeDetected" events from requestEvents() and requestDiskEvents().
*/
void SyncthingConnection::readChangeEvent(DateTime eventTime, const QString &eventType, const QJsonObject &eventData)
{
int index;
auto *const dirInfo(findDirInfo(QLatin1String("folderID"), eventData, &index));
if (!dirInfo) {
return;
}
SyncthingFileChange change;
change.local = eventType.startsWith("Local");
change.eventTime = eventTime;
change.action = eventData.value(QLatin1String("action")).toString();
change.type = eventData.value(QLatin1String("type")).toString();
change.modifiedBy = eventData.value(QLatin1String("modifiedBy")).toString();
change.path = eventData.value(QLatin1String("path")).toString();
dirInfo->recentChanges.emplace_back(move(change));
emit dirStatusChanged(*dirInfo, index);
}
// events / long polling API
/*!
* \brief Requests the Syncthing events (since the last successful call) asynchronously.
*
* The signal newEvents() is emitted on success; otherwise error() is emitted.
*/
void SyncthingConnection::requestEvents()
{
if (m_eventsReply) {
return;
}
QUrlQuery query;
if (m_lastEventId) {
query.addQueryItem(QStringLiteral("since"), QString::number(m_lastEventId));
}
// force to return immediately after the first call
if (!m_hasEvents) {
query.addQueryItem(QStringLiteral("timeout"), QStringLiteral("0"));
}
QObject::connect(m_eventsReply = requestData(QStringLiteral("events"), query), &QNetworkReply::finished, this, &SyncthingConnection::readEvents);
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readEvents()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_eventsReply) {
m_eventsReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse Syncthing events: "), jsonError, reply, response);
handleFatalConnectionError();
return;
}
m_hasEvents = true;
const auto replyArray(replyDoc.array());
emit newEvents(replyArray);
readEventsFromJsonArray(replyArray, m_lastEventId);
#ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS
if (!replyArray.isEmpty()) {
cerr << Phrases::Info << "Received " << replyArray.size() << " Syncthing events:" << Phrases::End
<< replyDoc.toJson(QJsonDocument::Indented).data() << endl;
}
#endif
break;
}
case QNetworkReply::TimeoutError:
// no new events available, keep polling
break;
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request Syncthing events: "), SyncthingErrorCategory::OverallConnection, reply);
handleFatalConnectionError();
return;
}
if (m_keepPolling) {
requestEvents();
concludeConnection();
} else {
setStatus(SyncthingStatus::Disconnected);
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readEventsFromJsonArray(const QJsonArray &events, int &idVariable)
{
for (const auto &eventVal : events) {
const auto event(eventVal.toObject());
const auto eventTime([&] {
try {
return DateTime::fromIsoStringGmt(event.value(QLatin1String("time")).toString().toLocal8Bit().data());
} catch (const ConversionException &) {
return DateTime(); // ignore conversion error
}
}());
const auto eventType(event.value(QLatin1String("type")).toString());
const auto eventData(event.value(QLatin1String("data")).toObject());
idVariable = event.value(QLatin1String("id")).toInt(idVariable);
if (eventType == QLatin1String("Starting")) {
readStartingEvent(eventData);
} else if (eventType == QLatin1String("StateChanged")) {
readStatusChangedEvent(eventTime, eventData);
} else if (eventType == QLatin1String("DownloadProgress")) {
readDownloadProgressEvent(eventTime, eventData);
} else if (eventType.startsWith(QLatin1String("Folder"))) {
readDirEvent(eventTime, eventType, eventData);
} else if (eventType.startsWith(QLatin1String("Device"))) {
readDeviceEvent(eventTime, eventType, eventData);
} else if (eventType == QLatin1String("ItemStarted")) {
readItemStarted(eventTime, eventData);
} else if (eventType == QLatin1String("ItemFinished")) {
readItemFinished(eventTime, eventData);
} else if (eventType == QLatin1String("RemoteIndexUpdated")) {
readRemoteIndexUpdated(eventTime, eventData);
} else if (eventType == QLatin1String("ConfigSaved")) {
requestConfig(); // just consider current config as invalidated
} else if (eventType.endsWith(QLatin1String("ChangeDetected"))) {
readChangeEvent(eventTime, eventType, eventData);
}
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readStartingEvent(const QJsonObject &eventData)
{
const QString configDir(eventData.value(QLatin1String("home")).toString());
if (configDir != m_configDir) {
emit configDirChanged(m_configDir = configDir);
}
emitMyIdChanged(eventData.value(QLatin1String("myID")).toString());
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readStatusChangedEvent(DateTime eventTime, const QJsonObject &eventData)
{
const QString dir(eventData.value(QLatin1String("folder")).toString());
if (dir.isEmpty()) {
return;
}
// find the directory
int index;
SyncthingDir *dirInfo = findDirInfo(dir, index);
// add a new directory if the dir is not present yet
const bool dirAlreadyPresent = dirInfo;
if (!dirAlreadyPresent) {
m_dirs.emplace_back(dir);
dirInfo = &m_dirs.back();
}
// assign new status
bool statusChanged = dirInfo->assignStatus(eventData.value(QLatin1String("to")).toString(), eventTime);
if (dirInfo->status == SyncthingDirStatus::OutOfSync) {
const QString errorMessage(eventData.value(QLatin1String("error")).toString());
if (!errorMessage.isEmpty()) {
dirInfo->globalError = errorMessage;
statusChanged = true;
}
}
if (dirAlreadyPresent) {
// emit status changed when dir already present
if (statusChanged) {
emit dirStatusChanged(*dirInfo, index);
}
} else {
// request config for complete meta data of new directory
requestConfig();
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readDownloadProgressEvent(DateTime eventTime, const QJsonObject &eventData)
{
VAR_UNUSED(eventTime)
for (SyncthingDir &dirInfo : m_dirs) {
// disappearing implies that the download has been finished so just wipe old entries
dirInfo.downloadingItems.clear();
dirInfo.blocksAlreadyDownloaded = dirInfo.blocksToBeDownloaded = 0;
// read progress of currently downloading items
const QJsonObject dirObj(eventData.value(dirInfo.id).toObject());
if (!dirObj.isEmpty()) {
dirInfo.downloadingItems.reserve(static_cast<size_t>(dirObj.size()));
for (auto filePair = dirObj.constBegin(), end = dirObj.constEnd(); filePair != end; ++filePair) {
dirInfo.downloadingItems.emplace_back(dirInfo.path, filePair.key(), filePair.value().toObject());
const SyncthingItemDownloadProgress &itemProgress = dirInfo.downloadingItems.back();
dirInfo.blocksAlreadyDownloaded += itemProgress.blocksAlreadyDownloaded;
dirInfo.blocksToBeDownloaded += itemProgress.totalNumberOfBlocks;
}
}
dirInfo.downloadPercentage = (dirInfo.blocksAlreadyDownloaded > 0 && dirInfo.blocksToBeDownloaded > 0)
? (static_cast<unsigned int>(dirInfo.blocksAlreadyDownloaded) * 100 / static_cast<unsigned int>(dirInfo.blocksToBeDownloaded))
: 0;
dirInfo.downloadLabel
= QStringLiteral("%1 / %2 - %3 %")
.arg(QString::fromLatin1(dataSizeToString(dirInfo.blocksAlreadyDownloaded > 0
? static_cast<uint64>(dirInfo.blocksAlreadyDownloaded) * SyncthingItemDownloadProgress::syncthingBlockSize
: 0)
.data()),
QString::fromLatin1(dataSizeToString(dirInfo.blocksToBeDownloaded > 0
? static_cast<uint64>(dirInfo.blocksToBeDownloaded) * SyncthingItemDownloadProgress::syncthingBlockSize
: 0)
.data()),
QString::number(dirInfo.downloadPercentage));
}
emit downloadProgressChanged();
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventType, const QJsonObject &eventData)
{
// read dir ID
const auto dirId([&eventData] {
const auto folder(eventData.value(QLatin1String("folder")).toString());
if (!folder.isEmpty()) {
return folder;
}
return eventData.value(QLatin1String("id")).toString();
}());
if (dirId.isEmpty()) {
return;
}
// handle "FolderRejected"-event which is a bit special because here the dir ID is supposed to be unknown
if (eventType == QLatin1String("FolderRejected")) {
readDirRejected(eventTime, dirId, eventData);
return;
}
// find related dir info for other events (which are about well-known dirs)
int index;
auto *const dirInfo = findDirInfo(dirId, index);
if (!dirInfo) {
return;
}
// distinguish specific events
if (eventType == QLatin1String("FolderErrors")) {
readFolderErrors(eventTime, eventData, *dirInfo, index);
} else if (eventType == QLatin1String("FolderSummary")) {
readDirSummary(eventTime, eventData.value(QLatin1String("summary")).toObject(), *dirInfo, index);
} else if (eventType == QLatin1String("FolderCompletion") && dirInfo->lastStatisticsUpdate < eventTime) {
readFolderCompletion(eventTime, eventData, *dirInfo, index);
} else if (eventType == QLatin1String("FolderScanProgress")) {
const double current = eventData.value(QLatin1String("current")).toDouble(0);
const double total = eventData.value(QLatin1String("total")).toDouble(0);
const double rate = eventData.value(QLatin1String("rate")).toDouble(0);
if (current > 0 && total > 0) {
dirInfo->scanningPercentage = static_cast<int>(current * 100 / total);
dirInfo->scanningRate = rate;
dirInfo->assignStatus(SyncthingDirStatus::Scanning, eventTime); // ensure state is scanning
emit dirStatusChanged(*dirInfo, index);
}
} else if (eventType == QLatin1String("FolderPaused")) {
if (!dirInfo->paused) {
dirInfo->paused = true;
emit dirStatusChanged(*dirInfo, index);
}
} else if (eventType == QLatin1String("FolderResumed")) {
if (dirInfo->paused) {
dirInfo->paused = false;
emit dirStatusChanged(*dirInfo, index);
}
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readDeviceEvent(DateTime eventTime, const QString &eventType, const QJsonObject &eventData)
{
// ignore device events happened before the last connections update
if (eventTime.isNull() && m_lastConnectionsUpdate.isNull() && eventTime < m_lastConnectionsUpdate) {
return;
}
const QString dev(eventData.value(QLatin1String("device")).toString());
if (dev.isEmpty()) {
return;
}
// handle "FolderRejected"-event which is a bit special because here the dir ID is supposed to be unknown
if (eventType == QLatin1String("DeviceRejected")) {
readDevRejected(eventTime, dev, eventData);
return;
}
// find relevant device info
int index;
auto *const devInfo(findDevInfo(dev, index));
if (!devInfo) {
return;
}
// distinguish specific events
SyncthingDevStatus status = devInfo->status;
bool paused = devInfo->paused;
if (eventType == QLatin1String("DeviceConnected")) {
status = SyncthingDevStatus::Idle; // TODO: figure out when dev is actually syncing
} else if (eventType == QLatin1String("DeviceDisconnected")) {
status = SyncthingDevStatus::Disconnected;
} else if (eventType == QLatin1String("DevicePaused")) {
paused = true;
} else if (eventType == QLatin1String("DeviceRejected")) {
status = SyncthingDevStatus::Rejected;
} else if (eventType == QLatin1String("DeviceResumed")) {
paused = false;
// FIXME: correct to assume device which has just been resumed is still disconnected?
status = SyncthingDevStatus::Disconnected;
} else if (eventType == QLatin1String("DeviceDiscovered")) {
// we know about this device already, set status anyways because it might still be unknown
if (status == SyncthingDevStatus::Unknown) {
status = SyncthingDevStatus::Disconnected;
}
} else {
return; // can't handle other event types currently
}
// assign new status
if (devInfo->status != status || devInfo->paused != paused) {
// don't mess with the status of the own device
if (devInfo->status != SyncthingDevStatus::OwnDevice) {
devInfo->status = status;
}
devInfo->paused = paused;
emit devStatusChanged(*devInfo, index);
}
}
/*!
* \brief Reads results of requestEvents().
* \todo Implement this.
*/
void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject &eventData)
{
VAR_UNUSED(eventTime)
VAR_UNUSED(eventData)
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject &eventData)
{
int index;
auto *const dirInfo = findDirInfo(QLatin1String("folder"), eventData, &index);
if (!dirInfo) {
return;
}
// handle unsuccessful operation
const auto error(eventData.value(QLatin1String("error")).toString()), item(eventData.value(QLatin1String("item")).toString());
if (!error.isEmpty()) {
if (dirInfo->status == SyncthingDirStatus::OutOfSync) {
// FIXME: find better way to check whether the event is still relevant
dirInfo->itemErrors.emplace_back(error, item);
// emitNotification will trigger status update, so no need to call setStatus(status())
emit dirStatusChanged(*dirInfo, index);
emitNotification(eventTime, error);
}
return;
}
// update last file
if (dirInfo->lastFileTime.isNull() || eventTime < dirInfo->lastFileTime) {
dirInfo->lastFileTime = eventTime;
dirInfo->lastFileName = item;
dirInfo->lastFileDeleted = (eventData.value(QLatin1String("action")) != QLatin1String("delete"));
if (eventTime > m_lastFileTime) {
m_lastFileTime = dirInfo->lastFileTime;
m_lastFileName = dirInfo->lastFileName;
m_lastFileDeleted = dirInfo->lastFileDeleted;
}
emit dirStatusChanged(*dirInfo, index);
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
{
const QJsonArray errors(eventData.value(QLatin1String("errors")).toArray());
if (errors.isEmpty()) {
return;
}
if (dirInfo.lastSyncStarted > eventTime) {
return;
}
for (const QJsonValue &errorVal : errors) {
const QJsonObject error(errorVal.toObject());
if (error.isEmpty()) {
continue;
}
auto &errors = dirInfo.itemErrors;
SyncthingItemError dirError(error.value(QLatin1String("error")).toString(), error.value(QLatin1String("path")).toString());
if (find(errors.cbegin(), errors.cend(), dirError) != errors.cend()) {
continue;
}
errors.emplace_back(move(dirError));
dirInfo.assignStatus(SyncthingDirStatus::OutOfSync, eventTime);
// emit newNotification() for new errors
const auto &previousErrors = dirInfo.previousItemErrors;
if (find(previousErrors.cbegin(), previousErrors.cend(), dirInfo.itemErrors.back()) == previousErrors.cend()) {
emitNotification(eventTime, dirInfo.itemErrors.back().message);
}
}
emit dirStatusChanged(dirInfo, index);
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
{
readFolderCompletion(eventTime, eventData, dirInfo, index, eventData.value(QLatin1String("device")).toString());
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(
DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId)
{
if (devId.isEmpty() || devId == myId()) {
readLocalFolderCompletion(eventTime, eventData, dirInfo, index);
} else {
readRemoteFolderCompletion(eventTime, eventData, dirInfo, index, devId);
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readLocalFolderCompletion(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
{
auto &neededStats(dirInfo.neededStats);
auto &globalStats(dirInfo.globalStats);
// backup previous statistics -> if there's no difference after all, don't emit completed event
const auto previouslyUpdated(!dirInfo.lastStatisticsUpdate.isNull());
const auto previouslyNeeded(neededStats);
const auto previouslyGlobal(globalStats);
// read values from event data
globalStats.bytes = jsonValueToInt(eventData.value(QLatin1String("globalBytes")), globalStats.bytes);
neededStats.bytes = jsonValueToInt(eventData.value(QLatin1String("needBytes")), neededStats.bytes);
neededStats.deletes = jsonValueToInt(eventData.value(QLatin1String("needDeletes")), neededStats.deletes);
neededStats.deletes = jsonValueToInt(eventData.value(QLatin1String("needItems")), neededStats.files);
dirInfo.lastStatisticsUpdate = eventTime;
dirInfo.completionPercentage = globalStats.bytes ? static_cast<int>((globalStats.bytes - neededStats.bytes) * 100 / globalStats.bytes) : 100;
emit dirStatusChanged(dirInfo, index);
if (neededStats.isNull() && previouslyUpdated && (neededStats != previouslyNeeded || globalStats != previouslyGlobal)
&& dirInfo.status != SyncthingDirStatus::Scanning) {
emit dirCompleted(eventTime, dirInfo, index);
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readRemoteFolderCompletion(
DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId)
{
auto &completion = dirInfo.completionByDevice[devId];
auto &needed(completion.needed);
const auto previouslyUpdated = !completion.lastUpdate.isNull();
const auto previouslyNeeded = !needed.isNull();
const auto previousGlobalBytes = completion.globalBytes;
completion.lastUpdate = eventTime;
completion.percentage = eventData.value(QLatin1String("completion")).toDouble();
completion.globalBytes = jsonValueToInt(eventData.value(QLatin1String("globalBytes")));
needed.bytes = jsonValueToInt(eventData.value(QLatin1String("needBytes")), needed.bytes);
needed.items = jsonValueToInt(eventData.value(QLatin1String("needItems")), needed.items);
needed.deletes = jsonValueToInt(eventData.value(QLatin1String("needDeletes")), needed.deletes);
emit dirStatusChanged(dirInfo, index);
if (needed.isNull() && previouslyUpdated && (previouslyNeeded || previousGlobalBytes != completion.globalBytes)) {
int devIndex;
if (const auto *const devInfo = findDevInfo(devId, devIndex)) {
emit dirCompleted(DateTime::gmtNow(), dirInfo, index, devInfo);
}
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readRemoteIndexUpdated(DateTime eventTime, const QJsonObject &eventData)
{
// ignore those events if we're not updating completion automatically
if (!m_requestCompletion) {
return;
}
// find dev/dir
const auto devId(eventData.value(QLatin1String("device")).toString());
const auto dirId(eventData.value(QLatin1String("folder")).toString());
if (dirId.isEmpty()) {
return;
}
int index;
auto *const dirInfo = findDirInfo(dirId, index);
if (!dirInfo) {
return;
}
// ignore event if we don't share the directory with the device
if (!dirInfo->deviceIds.contains(devId)) {
return;
}
// request completion again if out-of-date
const auto &completion = dirInfo->completionByDevice[devId];
if (completion.lastUpdate < eventTime) {
requestCompletion(devId, dirId);
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::requestDiskEvents(int limit)
{
if (m_diskEventsReply) {
return;
}
QUrlQuery query;
query.addQueryItem(QStringLiteral("limit"), QString::number(limit));
if (m_lastDiskEventId) {
query.addQueryItem(QStringLiteral("since"), QString::number(m_lastDiskEventId));
}
// force to return immediately after the first call
if (!m_hasDiskEvents) {
query.addQueryItem(QStringLiteral("timeout"), QStringLiteral("0"));
}
QObject::connect(
m_diskEventsReply = requestData(QStringLiteral("events/disk"), query), &QNetworkReply::finished, this, &SyncthingConnection::readDiskEvents);
}
/*!
* \brief Reads data from requestDiskEvents().
*/
void SyncthingConnection::readDiskEvents()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
if (reply == m_diskEventsReply) {
m_diskEventsReply = nullptr;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const auto replyDoc(QJsonDocument::fromJson(response, &jsonError));
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse disk events: "), jsonError, reply, response);
return;
}
m_hasDiskEvents = true;
readEventsFromJsonArray(replyDoc.array(), m_lastDiskEventId);
break;
}
case QNetworkReply::TimeoutError:
// no new events available, keep polling
break;
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request disk events: "), SyncthingErrorCategory::OverallConnection, reply);
handleFatalConnectionError();
return;
}
if (m_keepPolling) {
requestDiskEvents();
concludeConnection();
}
}
} // namespace Data