syncthingtray/connector/syncthingconnection_requests.cpp
Martchus 5227a7961e Use queued connection for "other replies"
so the handler is not called while looping through the
list of these replies to abort them. The handler must
not be called in that situation because it mutates the
list.
2020-03-01 23:40:27 +01:00

2144 lines
76 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 CppUtilities;
#if defined(LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS) || defined(LIB_SYNCTHING_CONNECTOR_LOG_API_CALLS)
using namespace CppUtilities::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;
}
/*!
* \brief Prepares the current reply.
*/
QNetworkReply *SyncthingConnection::prepareReply()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
// skip further processing if aborting to reconnect
if (m_abortingToReconnect) {
handleAdditionalRequestCanceled();
return nullptr;
}
return reply;
}
/*!
* \brief Prepares the current reply.
*/
QNetworkReply *SyncthingConnection::prepareReply(QNetworkReply *&expectedReply)
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
// unset the expected reply so it is no longer considered pending
if (reply == expectedReply) {
expectedReply = nullptr;
}
// skip further processing if aborting to reconnect
if (m_abortingToReconnect) {
handleAdditionalRequestCanceled();
return nullptr;
}
return reply;
}
/*!
* \brief Prepares the current reply.
*/
QNetworkReply *SyncthingConnection::prepareReply(QList<QNetworkReply *> &expectedReplies)
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
// unset the expected reply so it is no longer considered pending
expectedReplies.removeAll(reply);
// skip further processing if aborting to reconnect
if (m_abortingToReconnect) {
handleAdditionalRequestCanceled();
return nullptr;
}
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 = prepareReply();
if (!reply) {
return;
}
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 = prepareReply();
if (!reply) {
return;
}
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 = prepareReply();
if (!reply) {
return;
}
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 = prepareReply();
if (!reply) {
return;
}
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 = prepareReply();
if (!reply) {
return;
}
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 = prepareReply();
if (!reply) {
return;
}
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 = prepareReply(m_configReply);
if (!reply) {
return;
}
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());
emit newConfigApplied();
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 = prepareReply(m_statusReply);
if (!reply) {
return;
}
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 = prepareReply(m_connectionsReply);
if (!reply) {
return;
}
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 std::uint64_t totalIncomingTraffic = totalIncomingTrafficValue.isDouble() ? jsonValueToInt(totalIncomingTrafficValue) : unknownTraffic;
const std::uint64_t 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 = prepareReply(m_errorsReply);
if (!reply) {
return;
}
// 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 = prepareReply(m_dirStatsReply);
if (!reply) {
return;
}
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, Qt::QueuedConnection);
}
/*!
* \brief Reads data from requestDirStatus().
*/
void SyncthingConnection::readDirStatus()
{
auto *const reply = prepareReply(m_otherReplies);
if (!reply) {
return;
}
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;
}
readDirSummary(DateTime::gmtNow(), replyDoc.object(), *dir, index);
if (m_keepPolling) {
concludeConnection();
}
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
return;
default:
emitError(tr("Unable to request directory statistics: "), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Requests pull errors for \a dirId asynchronously.
*
* The dirStatusChanged() signal is emitted on success and error() in the error case.
*/
void SyncthingConnection::requestDirPullErrors(const QString &dirId, int page, int perPage)
{
QUrlQuery query;
query.addQueryItem(QStringLiteral("folder"), dirId);
if (page > 0 && perPage > 0) {
query.addQueryItem(QStringLiteral("page"), QString::number(page));
query.addQueryItem(QStringLiteral("perpage"), QString::number(perPage));
}
auto *const reply = requestData(QStringLiteral("folder/pullerrors"), query);
reply->setProperty("dirId", dirId);
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirPullErrors);
}
/*!
* \brief Reads data from requestDirPullErrors().
*/
void SyncthingConnection::readDirPullErrors()
{
auto *const reply = prepareReply();
if (!reply) {
return;
}
// determine relevant dir
int index;
const QString dirId(reply->property("dirId").toString());
SyncthingDir *const dir = findDirInfo(dirId, index);
if (!dir) {
// discard errors for unknown dirs
return;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
// 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 pull errors for directory %1: ").arg(dirId), jsonError, reply, response);
return;
}
readFolderErrors(DateTime::gmtNow(), replyDoc.object(), *dir, index);
break;
}
case QNetworkReply::OperationCanceledError:
return;
default:
emitError(tr("Unable to request pull errors for directory %1: ").arg(dirId), 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, Qt::QueuedConnection);
}
/// \cond
static void ensureCompletionNotConsideredRequested(const QString &devId, SyncthingDev *devInfo, const QString &dirId, SyncthingDir *dirInfo)
{
if (devInfo) {
devInfo->completionByDir[dirId].requested = false;
}
if (dirInfo) {
dirInfo->completionByDevice[dirId].requested = false;
}
}
/// \endcond
/*!
* \brief Reads data from requestCompletion().
*/
void SyncthingConnection::readCompletion()
{
auto *reply = prepareReply(m_otherReplies);
const auto cancelled = reply == nullptr;
if (!reply) {
reply = static_cast<QNetworkReply *>(sender());
}
// determine relevant dev/dir
const auto devId(reply->property("devId").toString());
const auto dirId(reply->property("dirId").toString());
int devIndex, dirIndex;
auto *const devInfo = findDevInfo(devId, devIndex);
auto *const dirInfo = findDirInfo(dirId, dirIndex);
if (!devInfo && !dirInfo) {
return;
}
if (cancelled) {
ensureCompletionNotConsideredRequested(devId, devInfo, dirId, dirInfo);
return;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
// parse JSON
QJsonParseError jsonError;
const auto response(reply->readAll());
const auto replyDoc(QJsonDocument::fromJson(response, &jsonError));
if (jsonError.error == QJsonParseError::NoError) {
// update the relevant completion info
readRemoteFolderCompletion(DateTime::gmtNow(), replyDoc.object(), devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
concludeConnection();
return;
}
emitError(tr("Unable to parse completion for device/directory %1/%2: ").arg(devId, dirId), jsonError, reply, response);
break;
}
case QNetworkReply::OperationCanceledError:
handleAdditionalRequestCanceled();
break;
default:
emitError(tr("Unable to request completion for device/directory %1/%2: ").arg(devId, dirId), SyncthingErrorCategory::SpecificRequest, reply);
}
ensureCompletionNotConsideredRequested(devId, devInfo, dirId, dirInfo);
}
/*!
* \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 = prepareReply(m_devStatsReply);
if (!reply) {
return;
}
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 = prepareReply(m_versionReply);
if (!reply) {
return;
}
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 = prepareReply();
if (!reply) {
return;
}
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 = prepareReply(m_logReply);
if (!reply) {
return;
}
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.
*/
void SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &summary, SyncthingDir &dir, int index)
{
if (summary.isEmpty() || dir.lastStatisticsUpdate > eventTime) {
return;
}
// 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.pullErrorCount = jsonValueToInt(summary.value(QLatin1String("pullErrors")));
m_dirStatsAltered = true;
dir.ignorePatterns = summary.value(QLatin1String("ignorePatterns")).toBool();
dir.lastStatisticsUpdate = eventTime;
// update status
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);
}
}
/*!
* \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);
}
/*!
* \brief Reads data from "DeviceRejected"-event.
*/
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();
if (m_recordFileChanges) {
dirInfo->recentChanges.emplace_back(move(change));
emit dirStatusChanged(*dirInfo, index);
emit fileChanged(*dirInfo, index, dirInfo->recentChanges.back());
} else {
emit fileChanged(*dirInfo, index, change);
}
}
// 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 = prepareReply(m_eventsReply);
if (!reply) {
return;
}
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);
}
}
emitDirStatisticsChanged();
}
/*!
* \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)
{
CPP_UTILITIES_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<std::uint64_t>(dirInfo.blocksAlreadyDownloaded) * SyncthingItemDownloadProgress::syncthingBlockSize
: 0)
.data()),
QString::fromLatin1(dataSizeToString(dirInfo.blocksToBeDownloaded > 0
? static_cast<std::uint64_t>(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()) {
// handle events which don't necessarily require a corresponding dir info
if (eventType == eventType == QLatin1String("FolderCompletion")) {
readFolderCompletion(eventTime, eventData, dirId, nullptr, -1);
}
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, dirId, 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")) {
devInfo->setConnectedStateAccordingToCompletion();
} 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)
{
CPP_UTILITIES_UNUSED(eventTime)
CPP_UTILITIES_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()) {
// add error item if not already present
if (dirInfo->status != SyncthingDirStatus::OutOfSync) {
// FIXME: find better way to check whether the event is still relevant
return;
}
for (const auto &itemError : dirInfo->itemErrors) {
if (itemError.message == error && itemError.path == item) {
return;
}
}
dirInfo->itemErrors.emplace_back(error, item);
if (dirInfo->pullErrorCount < dirInfo->itemErrors.size()) {
dirInfo->pullErrorCount = dirInfo->itemErrors.size();
}
// 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() and requestDirPullErrors().
*/
void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
{
// ignore errors occurred before the last time the directory was in "sync" state (Syncthing re-emits recurring errors)
if (dirInfo.lastSyncStarted > eventTime) {
return;
}
// clear previous errors (considering syncthing/lib/model/rwfolder.go it seems that also the event API always returns a
// full list of events and not only new ones)
dirInfo.itemErrors.clear();
// add errors
for (const QJsonValueRef errorVal : eventData.value(QLatin1String("errors")).toArray()) {
const QJsonObject error(errorVal.toObject());
if (error.isEmpty()) {
continue;
}
dirInfo.itemErrors.emplace_back(error.value(QLatin1String("error")).toString(), error.value(QLatin1String("path")).toString());
}
// set pullErrorCount in case it has not already been populated from the FolderSummary event
if (dirInfo.pullErrorCount < dirInfo.itemErrors.size()) {
dirInfo.pullErrorCount = dirInfo.itemErrors.size();
}
// ensure the directory is considered out-of-sync
if (dirInfo.pullErrorCount) {
dirInfo.assignStatus(SyncthingDirStatus::OutOfSync, eventTime);
}
emit dirStatusChanged(dirInfo, index);
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(
DateTime eventTime, const QJsonObject &eventData, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
const auto devId = eventData.value(QLatin1String("device")).toString();
int devIndex;
auto *const devInfo = findDevInfo(devId, devIndex);
readFolderCompletion(eventTime, eventData, devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(DateTime eventTime, const QJsonObject &eventData, const QString &devId, SyncthingDev *devInfo,
int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
if (devInfo && !devId.isEmpty() && devId != myId()) {
readRemoteFolderCompletion(eventTime, eventData, devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
} else if (dirInfo) {
readLocalFolderCompletion(eventTime, eventData, *dirInfo, dirIndex);
}
}
/*!
* \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::WaitingToScan && dirInfo.status != SyncthingDirStatus::Scanning) {
emit dirCompleted(eventTime, dirInfo, index);
}
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readRemoteFolderCompletion(DateTime eventTime, const QJsonObject &eventData, const QString &devId, SyncthingDev *devInfo,
int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
// make new completion
auto completion = SyncthingCompletion();
auto &needed(completion.needed);
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);
// update dir info
if (dirInfo) {
auto &previousCompletion = dirInfo->completionByDevice[devId];
const auto previouslyUpdated = !previousCompletion.lastUpdate.isNull();
const auto previouslyNeeded = !previousCompletion.needed.isNull();
const auto previousGlobalBytes = previousCompletion.globalBytes;
previousCompletion = completion;
emit dirStatusChanged(*dirInfo, dirIndex);
if (devInfo && needed.isNull() && previouslyUpdated && (previouslyNeeded || previousGlobalBytes != completion.globalBytes)) {
emit dirCompleted(DateTime::gmtNow(), *dirInfo, dirIndex, devInfo);
}
}
// update dev info
if (devInfo) {
auto &previousCompletion = devInfo->completionByDir[dirId];
devInfo->overallCompletion -= previousCompletion;
devInfo->overallCompletion += completion;
devInfo->overallCompletion.recomputePercentage();
previousCompletion = completion;
if (devInfo->isConnected()) {
devInfo->setConnectedStateAccordingToCompletion();
}
emit devStatusChanged(*devInfo, devIndex);
}
}
/*!
* \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 && !m_keepPolling) {
return;
}
// find dev/dir
const auto devId(eventData.value(QLatin1String("device")).toString());
const auto dirId(eventData.value(QLatin1String("folder")).toString());
if (devId.isEmpty() || dirId.isEmpty()) {
return;
}
int devIndex, dirIndex;
auto *const devInfo = findDevInfo(devId, devIndex);
auto *const dirInfo = findDirInfo(dirId, dirIndex);
// discard if the related dev and dir are unknown
if (!devInfo && !dirInfo) {
return;
}
// ignore event if we don't know the device and if we don't share the directory with the device
if (!devInfo && !dirInfo->deviceIds.contains(devId)) {
return;
}
// request completion again if not already requested and out-of-date
const auto &completion = dirInfo ? dirInfo->completionByDevice[devId] : devInfo->completionByDir[dirId];
if (!completion.requested && 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 = prepareReply(m_diskEventsReply);
if (!reply) {
return;
}
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