1067 lines
35 KiB
C++
1067 lines
35 KiB
C++
#include "./syncthingconnection.h"
|
|
#include "./syncthingconfig.h"
|
|
#include "./syncthingconnectionsettings.h"
|
|
#include "./utils.h"
|
|
|
|
#ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED
|
|
#include "./syncthingconnectionmockhelpers.h"
|
|
#endif
|
|
|
|
#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>
|
|
#include <iostream>
|
|
#endif
|
|
|
|
#include <QAuthenticator>
|
|
#include <QHostAddress>
|
|
#include <QJsonArray>
|
|
#include <QJsonDocument>
|
|
#include <QJsonValue>
|
|
#include <QNetworkAccessManager>
|
|
#include <QNetworkInterface>
|
|
#include <QNetworkReply>
|
|
#include <QStringBuilder>
|
|
#include <QTimer>
|
|
|
|
#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 {
|
|
|
|
/*!
|
|
* \brief Returns the QNetworkAccessManager instance used by SyncthingConnection instances.
|
|
*/
|
|
QNetworkAccessManager &networkAccessManager()
|
|
{
|
|
static auto networkAccessManager = new QNetworkAccessManager;
|
|
return *networkAccessManager;
|
|
}
|
|
|
|
/*!
|
|
* \class SyncthingConnection
|
|
* \brief The SyncthingConnection class allows Qt applications to access Syncthing via its REST API.
|
|
* \remarks All requests are performed asynchronously.
|
|
*
|
|
* The first thing to do when working with that class is setting the URL to connect to and the API key
|
|
* via the constructor or setSyncthingUrl() and setApiKey(). Credentials for the HTTP authentification
|
|
* can be set via setCredentials() if not included in the URL.
|
|
*
|
|
* Requests can then be done via the request...() methods, eg. requestConfig(). This would emit the
|
|
* newConfig() signal on success and the error() signal when an error occured. The other request...()
|
|
* methods work in a similar way.
|
|
*
|
|
* However, usually it is best to simply call the connect() method. It will do all required requests
|
|
* to populate dirInfo(), devInfo(), myId(), totalIncomingTraffic(), totalOutgoingTraffic() and all
|
|
* the other variables. It will also use Syncthing's event API to listen for changes. The signals
|
|
* newDirs() and newDevs() are can be used to know when dirInfo() and devInfo() become available.
|
|
* Note that in this case the previous dirInfo()/devInfo() is invalidated.
|
|
*/
|
|
|
|
/*!
|
|
* \brief Constructs a new instance ready to connect. To establish the connection, call connect().
|
|
*/
|
|
SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByteArray &apiKey, QObject *parent)
|
|
: QObject(parent)
|
|
, m_syncthingUrl(syncthingUrl)
|
|
, m_apiKey(apiKey)
|
|
, m_status(SyncthingStatus::Disconnected)
|
|
, m_keepPolling(false)
|
|
, m_abortingAllRequests(false)
|
|
, m_abortingToReconnect(false)
|
|
, m_requestCompletion(true)
|
|
, m_lastEventId(0)
|
|
, m_lastDiskEventId(0)
|
|
, m_autoReconnectTries(0)
|
|
, m_totalIncomingTraffic(unknownTraffic)
|
|
, m_totalOutgoingTraffic(unknownTraffic)
|
|
, m_totalIncomingRate(0.0)
|
|
, m_totalOutgoingRate(0.0)
|
|
, m_configReply(nullptr)
|
|
, m_statusReply(nullptr)
|
|
, m_connectionsReply(nullptr)
|
|
, m_errorsReply(nullptr)
|
|
, m_dirStatsReply(nullptr)
|
|
, m_devStatsReply(nullptr)
|
|
, m_eventsReply(nullptr)
|
|
, m_versionReply(nullptr)
|
|
, m_diskEventsReply(nullptr)
|
|
, m_logReply(nullptr)
|
|
, m_unreadNotifications(false)
|
|
, m_hasConfig(false)
|
|
, m_hasStatus(false)
|
|
, m_hasEvents(false)
|
|
, m_hasDiskEvents(false)
|
|
, m_lastFileDeleted(false)
|
|
, m_dirStatsAltered(false)
|
|
, m_recordFileChanges(false)
|
|
{
|
|
m_trafficPollTimer.setInterval(SyncthingConnectionSettings::defaultTrafficPollInterval);
|
|
m_trafficPollTimer.setTimerType(Qt::VeryCoarseTimer);
|
|
m_trafficPollTimer.setSingleShot(true);
|
|
QObject::connect(&m_trafficPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestConnections);
|
|
m_devStatsPollTimer.setInterval(SyncthingConnectionSettings::defaultDevStatusPollInterval);
|
|
m_devStatsPollTimer.setTimerType(Qt::VeryCoarseTimer);
|
|
m_devStatsPollTimer.setSingleShot(true);
|
|
QObject::connect(&m_devStatsPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestDeviceStatistics);
|
|
m_errorsPollTimer.setInterval(SyncthingConnectionSettings::defaultErrorsPollInterval);
|
|
m_errorsPollTimer.setTimerType(Qt::VeryCoarseTimer);
|
|
m_errorsPollTimer.setSingleShot(true);
|
|
QObject::connect(&m_errorsPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestErrors);
|
|
m_autoReconnectTimer.setTimerType(Qt::VeryCoarseTimer);
|
|
m_autoReconnectTimer.setInterval(SyncthingConnectionSettings::defaultReconnectInterval);
|
|
QObject::connect(&m_autoReconnectTimer, &QTimer::timeout, this, &SyncthingConnection::autoReconnect);
|
|
|
|
#ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED
|
|
setupTestData();
|
|
#endif
|
|
|
|
#ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS
|
|
QObject::connect(this, &SyncthingConnection::newDirs, [](const auto &dirs) {
|
|
std::cerr << Phrases::Info << "Directory list renewed:" << Phrases::End;
|
|
std::cerr << displayNames(dirs).join(QStringLiteral(", ")).toStdString() << endl;
|
|
});
|
|
QObject::connect(this, &SyncthingConnection::newDevices, [](const auto &devs) {
|
|
std::cerr << Phrases::Info << "Device list renewed:" << Phrases::End;
|
|
std::cerr << displayNames(devs).join(QStringLiteral(", ")).toStdString() << endl;
|
|
});
|
|
#endif
|
|
}
|
|
|
|
/*!
|
|
* \brief Destroys the instance. Ongoing requests are aborted.
|
|
*/
|
|
SyncthingConnection::~SyncthingConnection()
|
|
{
|
|
m_status = SyncthingStatus::BeingDestroyed;
|
|
disconnect();
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns whether the currently assigned syncthingUrl() refers to the Syncthing instance on the local machine.
|
|
*/
|
|
bool SyncthingConnection::isLocal() const
|
|
{
|
|
return ::Data::isLocal(QUrl(m_syncthingUrl));
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the string representation of the specified \a status.
|
|
*/
|
|
QString SyncthingConnection::statusText(SyncthingStatus status)
|
|
{
|
|
switch (status) {
|
|
case SyncthingStatus::Disconnected:
|
|
return tr("disconnected");
|
|
case SyncthingStatus::Reconnecting:
|
|
return tr("reconnecting");
|
|
case SyncthingStatus::Idle:
|
|
return tr("connected");
|
|
case SyncthingStatus::Scanning:
|
|
return tr("connected, scanning");
|
|
case SyncthingStatus::Paused:
|
|
return tr("connected, paused");
|
|
case SyncthingStatus::Synchronizing:
|
|
return tr("connected, synchronizing");
|
|
case SyncthingStatus::OutOfSync:
|
|
return tr("connected, out of sync");
|
|
default:
|
|
return tr("unknown");
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns whether there is at least one directory out-of-sync.
|
|
*/
|
|
bool SyncthingConnection::hasOutOfSyncDirs() const
|
|
{
|
|
for (const SyncthingDir &dir : m_dirs) {
|
|
if (dir.status == SyncthingDirStatus::OutOfSync) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*!
|
|
* \brief Sets all polling intervals (traffic, device statistics, errors) to 0 so polling is disabled.
|
|
* \remarks Does not affect the auto-reconnect and does not affect the *long* polling for the event API.
|
|
*/
|
|
void SyncthingConnection::disablePolling()
|
|
{
|
|
setTrafficPollInterval(0);
|
|
setDevStatsPollInterval(0);
|
|
setErrorsPollInterval(0);
|
|
}
|
|
|
|
/*!
|
|
* \brief Connects asynchronously to Syncthing. Does nothing if already connected.
|
|
*
|
|
* Use this to connect the first time or to connect to the same Syncthing instance again or to ensure
|
|
* the connection to the currently configured instance is established. Use reconnect() to connect to
|
|
* a different instance.
|
|
*
|
|
* \remarks Does not clear data from a previous connection (except error items). Use reconnect() if that
|
|
* is required.
|
|
*/
|
|
void SyncthingConnection::connect()
|
|
{
|
|
// reset auto-reconnect
|
|
m_autoReconnectTimer.stop();
|
|
m_autoReconnectTries = 0;
|
|
|
|
// skip if already connected (see reconnect() to force reconnecting)
|
|
if (isConnected()) {
|
|
return;
|
|
}
|
|
|
|
// reset status
|
|
m_abortingToReconnect = m_hasConfig = m_hasStatus = m_hasEvents = m_hasDiskEvents = false;
|
|
|
|
// check configuration
|
|
if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) {
|
|
emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
|
|
setStatus(SyncthingStatus::Disconnected);
|
|
return;
|
|
}
|
|
|
|
// start by requesting config and status; if both are available request further info and events
|
|
requestConfig();
|
|
requestStatus();
|
|
m_keepPolling = true;
|
|
}
|
|
|
|
/*!
|
|
* \brief Applies the specified configuration and tries to reconnect via reconnect() if properties requiring reconnect
|
|
* to take effect have changed.
|
|
* \remarks The expected SSL errors of the specified configuration are updated accordingly.
|
|
*/
|
|
void SyncthingConnection::connect(SyncthingConnectionSettings &connectionSettings)
|
|
{
|
|
if (applySettings(connectionSettings) || !isConnected()) {
|
|
reconnect();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Connects in \a milliSeconds. Useful to "schedule" another attempt in case of a failure.
|
|
* \remarks Does nothing if the connection attempt would happen anyways though auto-reconnect.
|
|
*/
|
|
void SyncthingConnection::connectLater(int milliSeconds)
|
|
{
|
|
// skip if conneting via auto-reconnect anyways
|
|
if (autoReconnectInterval() > 0 && milliSeconds > autoReconnectInterval()) {
|
|
return;
|
|
}
|
|
QTimer::singleShot(milliSeconds, this, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::connect));
|
|
}
|
|
|
|
/*!
|
|
* \brief Disconnects. That means all (long) polling is stopped and ongoing requests are aborted via abortAllRequests().
|
|
*/
|
|
void SyncthingConnection::disconnect()
|
|
{
|
|
m_abortingToReconnect = m_keepPolling = false;
|
|
m_trafficPollTimer.stop();
|
|
m_devStatsPollTimer.stop();
|
|
m_errorsPollTimer.stop();
|
|
m_autoReconnectTimer.stop();
|
|
m_autoReconnectTries = 0;
|
|
abortAllRequests();
|
|
}
|
|
|
|
/*!
|
|
* \brief Aborts status-relevant, pending requests.
|
|
* \remarks Status-relevant means that requests for tiggering actions like rescan() or restart() are excluded. requestQrCode() does not
|
|
* contribute to the status as well and is excluded as well.
|
|
*/
|
|
void SyncthingConnection::abortAllRequests()
|
|
{
|
|
m_abortingAllRequests = true;
|
|
if (m_configReply) {
|
|
m_configReply->abort();
|
|
}
|
|
if (m_statusReply) {
|
|
m_statusReply->abort();
|
|
}
|
|
if (m_connectionsReply) {
|
|
m_connectionsReply->abort();
|
|
}
|
|
if (m_errorsReply) {
|
|
m_errorsReply->abort();
|
|
}
|
|
if (m_dirStatsReply) {
|
|
m_dirStatsReply->abort();
|
|
}
|
|
if (m_devStatsReply) {
|
|
m_devStatsReply->abort();
|
|
}
|
|
if (m_eventsReply) {
|
|
m_eventsReply->abort();
|
|
}
|
|
if (m_versionReply) {
|
|
m_versionReply->abort();
|
|
}
|
|
if (m_diskEventsReply) {
|
|
m_diskEventsReply->abort();
|
|
}
|
|
if (m_logReply) {
|
|
m_logReply->abort();
|
|
}
|
|
for (auto *const reply : std::as_const(m_otherReplies)) {
|
|
reply->abort();
|
|
}
|
|
|
|
m_abortingAllRequests = false;
|
|
handleAdditionalRequestCanceled();
|
|
}
|
|
|
|
/*!
|
|
* \brief Disconnects if connected, then (re-)connects asynchronously.
|
|
* \remarks
|
|
* - Clears the currently cached configuration.
|
|
* - This explicit request to reconnect will reset the autoReconnectTries().
|
|
*/
|
|
void SyncthingConnection::reconnect()
|
|
{
|
|
// reset reconnect timer
|
|
m_autoReconnectTimer.stop();
|
|
m_autoReconnectTries = 0;
|
|
|
|
// stop other timers
|
|
m_trafficPollTimer.stop();
|
|
m_devStatsPollTimer.stop();
|
|
m_errorsPollTimer.stop();
|
|
|
|
// reset variables to track connection progress
|
|
// note: especially resetting events is important as it influences the subsequent hasPendingRequests() call
|
|
m_hasConfig = m_hasStatus = m_hasEvents = m_hasDiskEvents = false;
|
|
|
|
// reconnect right now if no pending requests to be aborted
|
|
if (!hasPendingRequests()) {
|
|
continueReconnecting();
|
|
return;
|
|
}
|
|
|
|
// abort pending requests before connecting again
|
|
m_keepPolling = m_abortingToReconnect = true;
|
|
abortAllRequests();
|
|
}
|
|
|
|
/*!
|
|
* \brief Applies the specified configuration and tries to reconnect via reconnect().
|
|
* \remarks The expected SSL errors of the specified configuration are updated accordingly.
|
|
*/
|
|
void SyncthingConnection::reconnect(SyncthingConnectionSettings &connectionSettings)
|
|
{
|
|
applySettings(connectionSettings);
|
|
reconnect();
|
|
}
|
|
|
|
/*!
|
|
* \brief Reconnects after the specified number of \a milliSeconds.
|
|
*/
|
|
void SyncthingConnection::reconnectLater(int milliSeconds)
|
|
{
|
|
QTimer::singleShot(milliSeconds, this, static_cast<void (SyncthingConnection::*)(void)>(&SyncthingConnection::reconnect));
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally called to reconnect; ensures currently cached config is cleared.
|
|
*/
|
|
void SyncthingConnection::continueReconnecting()
|
|
{
|
|
// notify that we're about to invalidate the configuration if not already invalidated anyways
|
|
const auto isConfigInvalidated = m_rawConfig.isEmpty();
|
|
if (!isConfigInvalidated) {
|
|
emit newConfig(m_rawConfig = QJsonObject());
|
|
}
|
|
|
|
// cleanup information from previous connection
|
|
m_keepPolling = true;
|
|
m_abortingToReconnect = false;
|
|
m_lastEventId = 0;
|
|
m_lastDiskEventId = 0;
|
|
m_configDir.clear();
|
|
m_myId.clear();
|
|
m_totalIncomingTraffic = unknownTraffic;
|
|
m_totalOutgoingTraffic = unknownTraffic;
|
|
m_totalIncomingRate = 0.0;
|
|
m_totalOutgoingRate = 0.0;
|
|
emit trafficChanged(unknownTraffic, unknownTraffic);
|
|
m_unreadNotifications = false;
|
|
m_hasConfig = false;
|
|
m_hasStatus = false;
|
|
m_hasEvents = false;
|
|
m_hasDiskEvents = false;
|
|
m_dirs.clear();
|
|
m_devs.clear();
|
|
m_lastConnectionsUpdate = DateTime();
|
|
m_lastFileTime = DateTime();
|
|
m_lastErrorTime = DateTime();
|
|
m_startTime = DateTime();
|
|
m_lastFileName.clear();
|
|
m_lastFileDeleted = false;
|
|
m_syncthingVersion.clear();
|
|
m_dirStatsAltered = false;
|
|
emit dirStatisticsChanged();
|
|
|
|
// notify that the configuration has been invalidated
|
|
if (!isConfigInvalidated) {
|
|
emit newConfigApplied();
|
|
}
|
|
|
|
if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) {
|
|
emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
|
|
setStatus(SyncthingStatus::Disconnected);
|
|
return;
|
|
}
|
|
|
|
requestConfig();
|
|
requestStatus();
|
|
|
|
setStatus(SyncthingStatus::Reconnecting);
|
|
}
|
|
|
|
/*!
|
|
* \brief Reads results of requestConfig() and requestStatus().
|
|
* \remarks Called in readConfig() or readStatus() to conclude reading parts requiring config *and* status
|
|
* being available. Does nothing if this is not the case (yet).
|
|
*/
|
|
void SyncthingConnection::concludeReadingConfigAndStatus()
|
|
{
|
|
if (!m_hasConfig || !m_hasStatus) {
|
|
return;
|
|
}
|
|
|
|
readDevs(m_rawConfig.value(QLatin1String("devices")).toArray());
|
|
readDirs(m_rawConfig.value(QLatin1String("folders")).toArray());
|
|
emit newConfigApplied();
|
|
|
|
continueConnecting();
|
|
}
|
|
|
|
/*!
|
|
* \brief Sets the state from (re)connecting to Syncthing's actual state if polling but there are no more pending requests.
|
|
* \remarks Called by read...() handlers for requests started in continueConnecting().
|
|
* \sa hasPendingRequests()
|
|
*/
|
|
void SyncthingConnection::concludeConnection()
|
|
{
|
|
if (!m_keepPolling || hasPendingRequests()) {
|
|
return;
|
|
}
|
|
setStatus(SyncthingStatus::Idle);
|
|
emitDirStatisticsChanged();
|
|
}
|
|
|
|
/*!
|
|
* \brief Connects increasing the auto-reconnect tries.
|
|
* \remarks Called via m_autoReconnectTimer.
|
|
*/
|
|
void SyncthingConnection::autoReconnect()
|
|
{
|
|
const auto tmp = m_autoReconnectTries;
|
|
connect();
|
|
m_autoReconnectTries = tmp + 1;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the directory info object for the directory with the specified ID.
|
|
* \returns Returns a pointer to the object or nullptr if not found.
|
|
* \remarks The returned object becomes invalid when the newDirs() signal is emitted or the connection is destroyed.
|
|
*/
|
|
SyncthingDir *SyncthingConnection::findDirInfo(const QString &dirId, int &row)
|
|
{
|
|
row = 0;
|
|
for (SyncthingDir &d : m_dirs) {
|
|
if (d.id == dirId) {
|
|
return &d;
|
|
}
|
|
++row;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the directory info object for the directory with the ID stored in the specified \a object with the specified \a key.
|
|
*/
|
|
SyncthingDir *SyncthingConnection::findDirInfo(QLatin1String key, const QJsonObject &object, int *row)
|
|
{
|
|
const auto dirId(object.value(key).toString());
|
|
if (dirId.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
int dummyRow;
|
|
auto &rowRef(row ? *row : dummyRow);
|
|
return findDirInfo(dirId, rowRef);
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the directory info object for the directory with the specified \a path.
|
|
*
|
|
* If a corresponding Syncthing directory could be found, \a relativePath is set to the path of the item relative
|
|
* to the location of the corresponding Syncthing directory.
|
|
*
|
|
* \returns Returns a pointer to the object or nullptr if not found.
|
|
* \remarks The returned object becomes invalid when the newDirs() signal is emitted or the connection is destroyed.
|
|
*/
|
|
SyncthingDir *SyncthingConnection::findDirInfoByPath(const QString &path, QString &relativePath, int &row)
|
|
{
|
|
row = 0;
|
|
for (SyncthingDir &dir : m_dirs) {
|
|
if (path == dir.pathWithoutTrailingSlash()) {
|
|
relativePath.clear();
|
|
return &dir;
|
|
} else if (path.startsWith(dir.path)) {
|
|
relativePath = path.mid(dir.path.size());
|
|
return &dir;
|
|
}
|
|
++row;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/*!
|
|
* \brief Appends a directory info object with the specified \a dirId to \a dirs.
|
|
*
|
|
* If such an object already exists, it is recycled by moving it to \a dirs.
|
|
* Otherwise a new, empty object is created.
|
|
*
|
|
* \returns Returns the directory info object or nullptr if \a dirId is invalid.
|
|
*/
|
|
SyncthingDir *SyncthingConnection::addDirInfo(std::vector<SyncthingDir> &dirs, const QString &dirId)
|
|
{
|
|
if (dirId.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
int row;
|
|
if (auto *const existingDirInfo = findDirInfo(dirId, row)) {
|
|
dirs.emplace_back(move(*existingDirInfo));
|
|
} else {
|
|
dirs.emplace_back(dirId);
|
|
}
|
|
return &dirs.back();
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the device info object for the device with the specified ID.
|
|
* \returns Returns a pointer to the object or nullptr if not found.
|
|
* \remarks The returned object becomes invalid when the newConfig() signal is emitted or the connection is destroyed.
|
|
*/
|
|
SyncthingDev *SyncthingConnection::findDevInfo(const QString &devId, int &row)
|
|
{
|
|
row = 0;
|
|
for (SyncthingDev &d : m_devs) {
|
|
if (d.id == devId) {
|
|
return &d;
|
|
}
|
|
++row;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the device info object for the first device with the specified name.
|
|
* \returns Returns a pointer to the object or nullptr if not found.
|
|
* \remarks The returned object becomes invalid when the newConfig() signal is emitted or the connection is destroyed.
|
|
*/
|
|
SyncthingDev *SyncthingConnection::findDevInfoByName(const QString &devName, int &row)
|
|
{
|
|
row = 0;
|
|
for (SyncthingDev &d : m_devs) {
|
|
if (d.name == devName) {
|
|
return &d;
|
|
}
|
|
++row;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns all directory IDs for the current configuration.
|
|
* \remarks Computed by looping dirInfo().
|
|
*/
|
|
QStringList SyncthingConnection::directoryIds() const
|
|
{
|
|
return ids(m_dirs);
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns all device IDs for the current configuration.
|
|
* \remarks Computed by looping devInfo().
|
|
*/
|
|
QStringList SyncthingConnection::deviceIds() const
|
|
{
|
|
return ids(m_devs);
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the device name for the specified \a deviceId if known; otherwise returns just the \a deviceId itself.
|
|
*/
|
|
QString SyncthingConnection::deviceNameOrId(const QString &deviceId) const
|
|
{
|
|
for (const auto &dev : devInfo()) {
|
|
if (dev.id == deviceId) {
|
|
return dev.name;
|
|
}
|
|
}
|
|
return deviceId;
|
|
}
|
|
|
|
/*!
|
|
* \brief Returns the number of devices Syncthing is currently connected to.
|
|
* \remarks Computed by looping devInfo().
|
|
*/
|
|
std::vector<const SyncthingDev *> SyncthingConnection::connectedDevices() const
|
|
{
|
|
std::vector<const SyncthingDev *> connectedDevs;
|
|
connectedDevs.reserve(devInfo().size());
|
|
for (const SyncthingDev &dev : devInfo()) {
|
|
if (dev.isConnected()) {
|
|
connectedDevs.emplace_back(&dev);
|
|
}
|
|
}
|
|
return connectedDevs;
|
|
}
|
|
|
|
/*!
|
|
* \brief Appends a device info object with the specified \a devId to \a devs.
|
|
*
|
|
* If such an object already exists, it is recycled by moving it to \a devs.
|
|
* Otherwise a new, empty object is created.
|
|
*
|
|
* \returns Returns the device info object or nullptr if \a devId is invalid.
|
|
*/
|
|
SyncthingDev *SyncthingConnection::addDevInfo(std::vector<SyncthingDev> &devs, const QString &devId)
|
|
{
|
|
if (devId.isEmpty()) {
|
|
return nullptr;
|
|
}
|
|
int row;
|
|
if (SyncthingDev *const existingDevInfo = findDevInfo(devId, row)) {
|
|
devs.emplace_back(move(*existingDevInfo));
|
|
} else {
|
|
devs.emplace_back(devId);
|
|
}
|
|
return &devs.back();
|
|
}
|
|
|
|
/*!
|
|
* \brief Continues connecting if both - config and status - have been parsed yet and continuous polling is enabled.
|
|
*/
|
|
void SyncthingConnection::continueConnecting()
|
|
{
|
|
// skip if config and status are missing or we're not supposed to actually connect
|
|
if (!m_keepPolling || !m_hasConfig || !m_hasStatus) {
|
|
return;
|
|
}
|
|
|
|
// read additional information (beside config and status)
|
|
// FIXME: make those requests configurable (eg. flag enum)
|
|
requestConnections();
|
|
requestDirStatistics();
|
|
requestDeviceStatistics();
|
|
requestErrors();
|
|
requestVersion();
|
|
for (const SyncthingDir &dir : m_dirs) {
|
|
requestDirStatus(dir.id);
|
|
if (!m_requestCompletion) {
|
|
continue;
|
|
}
|
|
for (const QString &devId : dir.deviceIds) {
|
|
requestCompletion(devId, dir.id);
|
|
}
|
|
}
|
|
|
|
// poll for events
|
|
m_lastEventId = m_lastDiskEventId = 0;
|
|
requestEvents();
|
|
requestDiskEvents();
|
|
}
|
|
|
|
/*!
|
|
* \brief Locates and loads the (self-signed) certificate used by the Syncthing GUI.
|
|
* \remarks
|
|
* - Ensures any previous certificates are cleared in any case.
|
|
* - Emits error() when an error occurs.
|
|
* - Loading the certificate is only possible if the connection object is configured
|
|
* to connect to the locally running Syncthing instance. Otherwise this method will
|
|
* only do the cleanup of previous certificates but not emit any errors.
|
|
* \returns Returns whether a certificate could be loaded.
|
|
*/
|
|
bool SyncthingConnection::loadSelfSignedCertificate()
|
|
{
|
|
// ensure current exceptions for self-signed certificates are cleared
|
|
m_expectedSslErrors.clear();
|
|
|
|
// not required when not using secure connection
|
|
const QUrl syncthingUrl(m_syncthingUrl);
|
|
if (!syncthingUrl.scheme().endsWith(QChar('s'))) {
|
|
return false;
|
|
}
|
|
|
|
// only possible if the Syncthing instance is running on the local machine
|
|
if (!::Data::isLocal(syncthingUrl)) {
|
|
return false;
|
|
}
|
|
|
|
// find cert
|
|
const QString certPath = !m_configDir.isEmpty() ? (m_configDir + QStringLiteral("/https-cert.pem")) : SyncthingConfig::locateHttpsCertificate();
|
|
if (certPath.isEmpty()) {
|
|
emit error(tr("Unable to locate certificate used by Syncthing."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
|
|
return false;
|
|
}
|
|
// add exception
|
|
const QList<QSslCertificate> certs = QSslCertificate::fromPath(certPath);
|
|
if (certs.isEmpty()) {
|
|
emit error(tr("Unable to load certificate used by Syncthing."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);
|
|
return false;
|
|
}
|
|
const QSslCertificate &cert = certs.at(0);
|
|
m_expectedSslErrors.reserve(4);
|
|
m_expectedSslErrors << QSslError(QSslError::UnableToGetLocalIssuerCertificate, cert) << QSslError(QSslError::UnableToVerifyFirstCertificate, cert)
|
|
<< QSslError(QSslError::SelfSignedCertificate, cert) << QSslError(QSslError::HostNameMismatch, cert);
|
|
return true;
|
|
}
|
|
|
|
/*!
|
|
* \brief Applies the specified configuration.
|
|
* \remarks
|
|
* - The expected SSL errors of the specified configuration are updated accordingly.
|
|
* - The configuration is not used instantly. It will be used on the next reconnect.
|
|
* \returns Returns whether at least one property requiring a reconnect to take effect has changed.
|
|
* \sa reconnect()
|
|
*/
|
|
bool SyncthingConnection::applySettings(SyncthingConnectionSettings &connectionSettings)
|
|
{
|
|
bool reconnectRequired = false;
|
|
if (syncthingUrl() != connectionSettings.syncthingUrl) {
|
|
setSyncthingUrl(connectionSettings.syncthingUrl);
|
|
reconnectRequired = true;
|
|
}
|
|
if (apiKey() != connectionSettings.apiKey) {
|
|
setApiKey(connectionSettings.apiKey);
|
|
reconnectRequired = true;
|
|
}
|
|
if ((connectionSettings.authEnabled && (user() != connectionSettings.userName || password() != connectionSettings.password))
|
|
|| (!connectionSettings.authEnabled && (!user().isEmpty() || !password().isEmpty()))) {
|
|
if (connectionSettings.authEnabled) {
|
|
setCredentials(connectionSettings.userName, connectionSettings.password);
|
|
} else {
|
|
setCredentials(QString(), QString());
|
|
}
|
|
reconnectRequired = true;
|
|
}
|
|
if (connectionSettings.expectedSslErrors.isEmpty()) {
|
|
const bool previouslyHadExpectedSslErrors = !expectedSslErrors().isEmpty();
|
|
const bool ok = loadSelfSignedCertificate();
|
|
connectionSettings.expectedSslErrors = expectedSslErrors();
|
|
if (ok || (previouslyHadExpectedSslErrors && !ok)) {
|
|
reconnectRequired = true;
|
|
}
|
|
} else if (expectedSslErrors() != connectionSettings.expectedSslErrors) {
|
|
m_expectedSslErrors = connectionSettings.expectedSslErrors;
|
|
reconnectRequired = true;
|
|
}
|
|
|
|
setTrafficPollInterval(connectionSettings.trafficPollInterval);
|
|
setDevStatsPollInterval(connectionSettings.devStatsPollInterval);
|
|
setErrorsPollInterval(connectionSettings.errorsPollInterval);
|
|
setAutoReconnectInterval(connectionSettings.reconnectInterval);
|
|
|
|
return reconnectRequired;
|
|
}
|
|
|
|
/*!
|
|
* \brief Sets the connection status. Ensures statusChanged() is emitted.
|
|
* \param status Specifies the status; should be either SyncthingStatus::Disconnected, SyncthingStatus::Reconnecting, or
|
|
* SyncthingStatus::Idle. There is no use in specifying other values such as SyncthingStatus::Synchronizing as
|
|
* these are determined automatically within the method.
|
|
*/
|
|
void SyncthingConnection::setStatus(SyncthingStatus status)
|
|
{
|
|
if (m_status == SyncthingStatus::BeingDestroyed) {
|
|
return;
|
|
}
|
|
switch (status) {
|
|
case SyncthingStatus::Disconnected:
|
|
// disable (long) polling
|
|
m_keepPolling = false;
|
|
[[fallthrough]];
|
|
case SyncthingStatus::Reconnecting:
|
|
m_devStatsPollTimer.stop();
|
|
m_trafficPollTimer.stop();
|
|
m_errorsPollTimer.stop();
|
|
break;
|
|
default:
|
|
// reset reconnect tries
|
|
m_autoReconnectTries = 0;
|
|
|
|
// check whether at least one directory is scanning, preparing to synchronize or synchronizing
|
|
// note: We don't distinguish between "preparing to sync" and "synchronizing" for computing the overall
|
|
// status at the moment.
|
|
bool scanning = false;
|
|
bool synchronizing = false;
|
|
for (const SyncthingDir &dir : m_dirs) {
|
|
switch (dir.status) {
|
|
case SyncthingDirStatus::PreparingToSync:
|
|
case SyncthingDirStatus::Synchronizing:
|
|
synchronizing = true;
|
|
break;
|
|
case SyncthingDirStatus::WaitingToScan:
|
|
case SyncthingDirStatus::Scanning:
|
|
scanning = true;
|
|
break;
|
|
default:;
|
|
}
|
|
if (synchronizing) {
|
|
break; // skip remaining dirs, "synchronizing" overrides "scanning" anyways
|
|
}
|
|
}
|
|
|
|
if (synchronizing) {
|
|
status = SyncthingStatus::Synchronizing;
|
|
} else if (scanning) {
|
|
status = SyncthingStatus::Scanning;
|
|
} else {
|
|
// check whether at least one device is paused
|
|
bool paused = false;
|
|
for (const SyncthingDev &dev : m_devs) {
|
|
if (dev.paused) {
|
|
paused = true;
|
|
break;
|
|
}
|
|
}
|
|
if (paused) {
|
|
status = SyncthingStatus::Paused;
|
|
} else {
|
|
status = SyncthingStatus::Idle;
|
|
}
|
|
}
|
|
}
|
|
if (m_status != status) {
|
|
emit statusChanged(m_status = status);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally called to emit a JSON parsing error.
|
|
* \remarks Since in this case the reply has already been read, its response must be passed as extra argument.
|
|
*/
|
|
void SyncthingConnection::emitError(const QString &message, const QJsonParseError &jsonError, QNetworkReply *reply, const QByteArray &response)
|
|
{
|
|
emit error(message % jsonError.errorString() % QChar(' ') % QChar('(') % tr("at offset %1").arg(jsonError.offset) % QChar(')'),
|
|
SyncthingErrorCategory::Parsing, QNetworkReply::NoError, reply->request(), response);
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally called to emit a network error (server replied error code are connection or server could not be reached at all).
|
|
*/
|
|
void SyncthingConnection::emitError(const QString &message, SyncthingErrorCategory category, QNetworkReply *reply)
|
|
{
|
|
emit error(message + reply->errorString(), category, reply->error(), reply->request(), reply->bytesAvailable() ? reply->readAll() : QByteArray());
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally called to emit myIdChanged() signal.
|
|
*/
|
|
void SyncthingConnection::emitMyIdChanged(const QString &newId)
|
|
{
|
|
if (newId.isEmpty() || m_myId == newId) {
|
|
return;
|
|
}
|
|
|
|
// adjust device status
|
|
int row = 0;
|
|
for (SyncthingDev &dev : m_devs) {
|
|
if (dev.id == newId) {
|
|
if (dev.status != SyncthingDevStatus::OwnDevice) {
|
|
dev.status = SyncthingDevStatus::OwnDevice;
|
|
emit devStatusChanged(dev, row);
|
|
}
|
|
} else if (dev.status == SyncthingDevStatus::OwnDevice) {
|
|
dev.status = SyncthingDevStatus::Unknown;
|
|
emit devStatusChanged(dev, row);
|
|
}
|
|
++row;
|
|
}
|
|
|
|
emit myIdChanged(m_myId = newId);
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally called to emit dirStatisticsChanged() event.
|
|
*/
|
|
void SyncthingConnection::emitDirStatisticsChanged()
|
|
{
|
|
if (m_dirStatsAltered) {
|
|
m_dirStatsAltered = false;
|
|
emit dirStatisticsChanged();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally called to handle a fatal error when reading config (dirs/devs), status and events.
|
|
*/
|
|
void SyncthingConnection::handleFatalConnectionError()
|
|
{
|
|
setStatus(SyncthingStatus::Disconnected);
|
|
abortAllRequests();
|
|
if (m_autoReconnectTimer.interval()) {
|
|
m_autoReconnectTimer.start();
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Handles cancelation of additional requests done via continueConnecting() method.
|
|
*/
|
|
void SyncthingConnection::handleAdditionalRequestCanceled()
|
|
{
|
|
// postpone handling if there are still other requests pending
|
|
if (hasPendingRequests()) {
|
|
return;
|
|
}
|
|
if (m_abortingToReconnect) {
|
|
// if reconnection flag is set, instantly etstablish a new connection ...
|
|
continueReconnecting();
|
|
} else {
|
|
// ... otherwise declare we're disconnected if that was the last pending request
|
|
setStatus(SyncthingStatus::Disconnected);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \brief Internally called to recalculate the overall connection status, e.g. after the status of a directory
|
|
* changed.
|
|
* \remarks
|
|
* - This is achieved by simply setting the status to idle. setStatus() will calculate the specific status.
|
|
* - If not connected, this method does nothing. This is important, because when this method is called when
|
|
* establishing a connection (and the status is hence still disconnected) timers for polling would be killed.
|
|
*/
|
|
void SyncthingConnection::recalculateStatus()
|
|
{
|
|
if (isConnected()) {
|
|
setStatus(SyncthingStatus::Idle);
|
|
}
|
|
}
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newConfig()
|
|
* \brief Indicates new configuration (dirs, devs, ...) is available.
|
|
* \remarks
|
|
* - Configuration is requested automatically when connecting.
|
|
* - Previous directories (and directory info objects!) are invalidated.
|
|
* - Previous devices (and device info objects!) are invalidated.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newDirs()
|
|
* \brief Indicates new directories are available.
|
|
* \remarks Always emitted after newConfig() as soon as new directory info objects become available.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newDevices()
|
|
* \brief Indicates new devices are available.
|
|
* \remarks Always emitted after newConfig() as soon as new device info objects become available.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newEvents()
|
|
* \brief Indicates new events (dir status changed, ...) are available.
|
|
* \remarks New events are automatically polled when connected.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::dirStatusChanged()
|
|
* \brief Indicates the status of the specified \a dir changed.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::devStatusChanged()
|
|
* \brief Indicates the status of the specified \a dev changed.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::downloadProgressChanged()
|
|
* \brief Indicates the download progress changed.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newNotification()
|
|
* \brief Indicates a new Syncthing notification is available.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newDevAvailable()
|
|
* \brief Indicates another device wants to talk to us.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newDirAvailable()
|
|
* \brief Indicates a device wants to share an so far unknown directory with us.
|
|
* \remarks \a dev might be nullptr. In this case the device which wants to share the directory is unknown as well.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::error()
|
|
* \brief Indicates a request (for configuration, events, ...) failed.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::statusChanged()
|
|
* \brief Indicates the status of the connection changed (status(), hasOutOfSyncDirs()).
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::configDirChanged()
|
|
* \brief Indicates the Syncthing home/configuration directory changed.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::myIdChanged()
|
|
* \brief Indicates ID of the own Syncthing device changed.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::trafficChanged()
|
|
* \brief Indicates totalIncomingTraffic() or totalOutgoingTraffic() has changed.
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::newConfigTriggered()
|
|
* \brief Indicates a new configuration has posted sucessfully via postConfig().
|
|
* \remarks In contrast to newConfig(), this signal is only emitted for configuration changes internally posted via postConfig().
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::rescanTriggered()
|
|
* \brief Indicates a rescan has been triggered sucessfully.
|
|
* \remarks Only emitted for rescans triggered internally via rescan() or rescanAll().
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::pauseTriggered()
|
|
* \brief Indicates a device has been paused sucessfully.
|
|
* \remarks Only emitted for pausing triggered internally via pause() or pauseAll().
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::resumeTriggered()
|
|
* \brief Indicates a device has been resumed sucessfully.
|
|
* \remarks Only emitted for resuming triggered internally via resume() or resumeAll().
|
|
*/
|
|
|
|
/*!
|
|
* \fn SyncthingConnection::restartTriggered()
|
|
* \brief Indicates a restart has been successfully triggered via restart().
|
|
*/
|
|
} // namespace Data
|