#include "./syncthingconnection.h" #include "./syncthingconfig.h" #include "./syncthingconnectionsettings.h" #include "./utils.h" #ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED #include "./syncthingconnectionmockhelpers.h" #endif #include #include #ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS #include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; using namespace ChronoUtilities; using namespace ConversionUtilities; #ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS using namespace 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. * \remarks All requests are performed asynchronously. */ /*! * \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_reconnecting(false) , m_requestCompletion(false) , m_lastEventId(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_eventsReply(nullptr) , m_unreadNotifications(false) , m_hasConfig(false) , m_hasStatus(false) , m_lastFileDeleted(false) { m_trafficPollTimer.setInterval(2000); m_trafficPollTimer.setTimerType(Qt::VeryCoarseTimer); m_trafficPollTimer.setSingleShot(true); QObject::connect(&m_trafficPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestConnections); m_devStatsPollTimer.setInterval(60000); m_devStatsPollTimer.setTimerType(Qt::VeryCoarseTimer); m_devStatsPollTimer.setSingleShot(true); QObject::connect(&m_devStatsPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestDeviceStatistics); m_errorsPollTimer.setInterval(30000); m_errorsPollTimer.setTimerType(Qt::VeryCoarseTimer); m_errorsPollTimer.setSingleShot(true); QObject::connect(&m_errorsPollTimer, &QTimer::timeout, this, &SyncthingConnection::requestErrors); m_autoReconnectTimer.setTimerType(Qt::VeryCoarseTimer); QObject::connect(&m_autoReconnectTimer, &QTimer::timeout, this, &SyncthingConnection::autoReconnect); #ifdef LIB_SYNCTHING_CONNECTOR_CONNECTION_MOCKED setupTestData(); #endif } /*! * \brief Destroys the instance. Ongoing requests are aborted. */ SyncthingConnection::~SyncthingConnection() { m_status = SyncthingStatus::BeingDestroyed; disconnect(); } bool SyncthingConnection::isLocal() const { return ::Data::isLocal(QUrl(m_syncthingUrl)); } /*! * \brief Returns the string representation of the current status(). */ QString SyncthingConnection::statusText() const { switch (m_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 Connects asynchronously to Syncthing. Does nothing if already connected. */ void SyncthingConnection::connect() { m_autoReconnectTimer.stop(); m_autoReconnectTries = 0; if (isConnected()) { return; } m_reconnecting = m_hasConfig = m_hasStatus = false; if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) { emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError); return; } 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(); } } void SyncthingConnection::connectLater(int milliSeconds) { // skip if conneting via auto-reconnect anyways if (autoReconnectInterval() > 0 && milliSeconds < autoReconnectInterval()) { return; } QTimer::singleShot(milliSeconds, this, static_cast(&SyncthingConnection::connect)); } /*! * \brief Disconnects. Does nothing if not connected. */ void SyncthingConnection::disconnect() { m_reconnecting = m_hasConfig = m_hasStatus = m_keepPolling = false; m_autoReconnectTries = 0; abortAllRequests(); } /*! * \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() { m_autoReconnectTimer.stop(); m_autoReconnectTries = 0; if (isConnected()) { m_reconnecting = true; m_hasConfig = m_hasStatus = false; abortAllRequests(); } else { continueReconnecting(); } } /*! * \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(); } void SyncthingConnection::reconnectLater(int milliSeconds) { QTimer::singleShot(milliSeconds, this, static_cast(&SyncthingConnection::reconnect)); } /*! * \brief Internally called to reconnect; ensures currently cached config is cleared. */ void SyncthingConnection::continueReconnecting() { emit newConfig(QJsonObject()); // configuration will be invalidated setStatus(SyncthingStatus::Reconnecting); m_keepPolling = true; m_reconnecting = false; m_lastEventId = 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_dirs.clear(); m_devs.clear(); m_lastConnectionsUpdate = DateTime(); m_lastFileTime = DateTime(); m_lastErrorTime = DateTime(); m_lastFileName.clear(); m_lastFileDeleted = false; if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) { emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError); return; } requestConfig(); requestStatus(); } void SyncthingConnection::autoReconnect() { const auto tmp = m_autoReconnectTries; connect(); m_autoReconnectTries = tmp + 1; } /*! * \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 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 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 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 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 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 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 *reply = networkAccessManager().get(prepareRequest(path, query, rest)); 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 *reply = networkAccessManager().post(prepareRequest(path, query), data); reply->ignoreSslErrors(m_expectedSslErrors); return reply; } /*! * \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 *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 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 *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; } /*! * \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 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 &dirs, const QString &dirId) { if (dirId.isEmpty()) { return nullptr; } int row; if (SyncthingDir *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::size_t SyncthingConnection::connectedDevices() const { size_t connectedDevs = 0; for (const SyncthingDev &dev : devInfo()) { if (dev.isConnected()) { ++connectedDevs; } } 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 &devs, const QString &devId) { if (devId.isEmpty()) { return nullptr; } int row; if (SyncthingDev *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() { if (!m_keepPolling || !m_hasConfig || !m_hasStatus) { return; } requestConnections(); requestDirStatistics(); requestDeviceStatistics(); requestErrors(); for (const SyncthingDir &dir : m_dirs) { requestDirStatus(dir.id); if (!m_requestCompletion) { continue; } for (const QString &devId : dir.deviceIds) { requestCompletion(devId, dir.id); } } // since config and status could be read successfully, let's poll for events m_lastEventId = 0; requestEvents(); } /*! * \brief Aborts all pending requests. */ void SyncthingConnection::abortAllRequests() { 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_eventsReply) { m_eventsReply->abort(); } } /*! * \brief Requests the Syncthing configuration asynchronously. * * The signal newConfig() is emitted on success; otherwise error() is emitted. */ void SyncthingConnection::requestConfig() { QObject::connect( m_configReply = requestData(QStringLiteral("system/config"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readConfig); } /*! * \brief Requests the Syncthing status asynchronously. * * The signals configDirChanged() and myIdChanged() are emitted when those values have changed; error() is emitted in the error case. */ void SyncthingConnection::requestStatus() { QObject::connect( m_statusReply = requestData(QStringLiteral("system/status"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readStatus); } /*! * \brief Requests the Syncthing configuration and status asynchronously. * * \sa requestConfig() and requestStatus() for emitted signals. */ void SyncthingConnection::requestConfigAndStatus() { requestConfig(); requestStatus(); } /*! * \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() { QObject::connect(m_connectionsReply = requestData(QStringLiteral("system/connections"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readConnections); } /*! * \brief Requests errors asynchronously. * * The signal newNotification() is emitted on success; error() is emitted in the error case. */ void SyncthingConnection::requestErrors() { QObject::connect( m_errorsReply = requestData(QStringLiteral("system/error"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readErrors); } /*! * \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 Requests statistics (last file, last scan) for all directories asynchronously. */ void SyncthingConnection::requestDirStatistics() { QObject::connect( requestData(QStringLiteral("stats/folder"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readDirStatistics); } /*! * \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); QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirStatus); } /*! * \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); QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readCompletion); } /*! * \brief Requests device statistics asynchronously. */ void SyncthingConnection::requestDeviceStatistics() { QObject::connect( requestData(QStringLiteral("stats/device"), QUrlQuery()), &QNetworkReply::finished, this, &SyncthingConnection::readDeviceStatistics); } /*! * \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 Requests the Syncthing events (since the last successful call) asynchronously. * * The signal newEvents() is emitted on success; otherwise error() is emitted. */ void SyncthingConnection::requestEvents() { QUrlQuery query; if (m_lastEventId) { query.addQueryItem(QStringLiteral("since"), QString::number(m_lastEventId)); } QObject::connect(m_eventsReply = requestData(QStringLiteral("events"), query), &QNetworkReply::finished, this, &SyncthingConnection::readEvents); } /*! * \brief Requests a QR code for the specified \a text. * * The specified \a callback is called on success; otherwise error() is emitted. */ QMetaObject::Connection SyncthingConnection::requestQrCode(const QString &text, std::function callback) { QUrlQuery query; query.addQueryItem(QStringLiteral("text"), text); QNetworkReply *reply = requestData(QStringLiteral("/qr/"), query, false); return QObject::connect(reply, &QNetworkReply::finished, [this, reply, callback] { reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: callback(reply->readAll()); break; default: emit error(tr("Unable to request QR-Code: ") + reply->errorString(), SyncthingErrorCategory::SpecificRequest, reply->error()); } }); } /*! * \brief Requests the Syncthing log. * * The specified \a callback is called on success; otherwise error() is emitted. */ QMetaObject::Connection SyncthingConnection::requestLog(std::function &)> callback) { QNetworkReply *reply = requestData(QStringLiteral("system/log"), QUrlQuery()); return QObject::connect(reply, &QNetworkReply::finished, [this, reply, callback] { reply->deleteLater(); 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(QStringLiteral("messages")).toArray()); vector logEntries; logEntries.reserve(static_cast(log.size())); for (const QJsonValue &logVal : log) { const QJsonObject logObj(logVal.toObject()); logEntries.emplace_back(logObj.value(QStringLiteral("when")).toString(), logObj.value(QStringLiteral("message")).toString()); } callback(logEntries); break; } default: emit error(tr("Unable to request Syncthing log: ") + reply->errorString(), SyncthingErrorCategory::SpecificRequest, reply->error()); } }); } /*! * \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 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 Reads results of requestConfig(). */ void SyncthingConnection::readConfig() { auto *const reply = static_cast(sender()); reply->deleteLater(); if (reply == m_configReply) { m_configReply = nullptr; } switch (reply->error()) { case QNetworkReply::NoError: { const QByteArray response(reply->readAll()); QJsonParseError jsonError; const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { emitError(tr("Unable to parse Syncthing config: "), jsonError, reply, response); handleFatalConnectionError(); return; } m_rawConfig = replyDoc.object(); m_hasConfig = true; emit newConfig(m_rawConfig); if (m_keepPolling) { concludeReadingConfigAndStatus(); } else { readDevs(m_rawConfig.value(QStringLiteral("devices")).toArray()); readDirs(m_rawConfig.value(QStringLiteral("folders")).toArray()); } break; } case QNetworkReply::OperationCanceledError: return; // intended, not an error 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 newDirs; newDirs.reserve(static_cast(dirs.size())); int dummy; for (const QJsonValue &dirVal : dirs) { const QJsonObject dirObj(dirVal.toObject()); SyncthingDir *const dirItem = addDirInfo(newDirs, dirObj.value(QStringLiteral("id")).toString()); if (!dirItem) { continue; } dirItem->label = dirObj.value(QStringLiteral("label")).toString(); dirItem->path = dirObj.value(QStringLiteral("path")).toString(); dirItem->deviceIds.clear(); dirItem->deviceNames.clear(); for (const QJsonValue &dev : dirObj.value(QStringLiteral("devices")).toArray()) { const QString devId = dev.toObject().value(QStringLiteral("deviceID")).toString(); if (!devId.isEmpty() && devId != m_myId) { dirItem->deviceIds << devId; if (const SyncthingDev *const dev = findDevInfo(devId, dummy)) { dirItem->deviceNames << dev->name; } } } dirItem->readOnly = dirObj.value(QStringLiteral("readOnly")).toBool(false); dirItem->rescanInterval = dirObj.value(QStringLiteral("rescanIntervalS")).toInt(-1); dirItem->ignorePermissions = dirObj.value(QStringLiteral("ignorePerms")).toBool(false); dirItem->autoNormalize = dirObj.value(QStringLiteral("autoNormalize")).toBool(false); dirItem->minDiskFreePercentage = dirObj.value(QStringLiteral("minDiskFreePct")).toInt(-1); dirItem->paused = dirObj.value(QStringLiteral("paused")).toBool(dirItem->paused); } 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 newDevs; newDevs.reserve(static_cast(devs.size())); for (const QJsonValue &devVal : devs) { const QJsonObject devObj(devVal.toObject()); SyncthingDev *const devItem = addDevInfo(newDevs, devObj.value(QStringLiteral("deviceID")).toString()); if (!devItem) { continue; } devItem->name = devObj.value(QStringLiteral("name")).toString(); devItem->addresses.clear(); for (const QJsonValue &addrVal : devObj.value(QStringLiteral("addresses")).toArray()) { devItem->addresses << addrVal.toString(); } devItem->compression = devObj.value(QStringLiteral("compression")).toString(); devItem->certName = devObj.value(QStringLiteral("certName")).toString(); devItem->introducer = devObj.value(QStringLiteral("introducer")).toBool(false); devItem->status = devItem->id == m_myId ? SyncthingDevStatus::OwnDevice : SyncthingDevStatus::Unknown; devItem->paused = devObj.value(QStringLiteral("paused")).toBool(devItem->paused); } m_devs.swap(newDevs); emit this->newDevices(m_devs); } /*! * \brief Reads results of requestStatus(). */ void SyncthingConnection::readStatus() { auto *const reply = static_cast(sender()); reply->deleteLater(); if (reply == m_statusReply) { m_statusReply = nullptr; } switch (reply->error()) { case QNetworkReply::NoError: { const QByteArray response(reply->readAll()); QJsonParseError jsonError; const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { emitError(tr("Unable to parse Syncthing status: "), jsonError, reply, response); handleFatalConnectionError(); return; } emitMyIdChanged(replyDoc.object().value(QStringLiteral("myID")).toString()); // other values are currently not interesting m_hasStatus = true; if (m_keepPolling) { concludeReadingConfigAndStatus(); } break; } case QNetworkReply::OperationCanceledError: return; // intended, not an error default: emitError(tr("Unable to request Syncthing status: "), SyncthingErrorCategory::OverallConnection, reply); handleFatalConnectionError(); } } /*! * \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(QStringLiteral("devices")).toArray()); readDirs(m_rawConfig.value(QStringLiteral("folders")).toArray()); if (isConnected()) { setStatus(SyncthingStatus::Idle); return; } continueConnecting(); } /*! * \brief Reads results of requestConnections(). */ void SyncthingConnection::readConnections() { auto *const reply = static_cast(sender()); reply->deleteLater(); if (reply == m_connectionsReply) { m_connectionsReply = nullptr; } switch (reply->error()) { case QNetworkReply::NoError: { const QByteArray response(reply->readAll()); QJsonParseError jsonError; const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { emitError(tr("Unable to parse connections: "), jsonError, reply, response); return; } const QJsonObject replyObj(replyDoc.object()); const QJsonObject totalObj(replyObj.value(QStringLiteral("total")).toObject()); // read traffic, the conversion to double is neccassary because toInt() doesn't work for high values const QJsonValue totalIncomingTrafficValue(totalObj.value(QStringLiteral("inBytesTotal"))); const QJsonValue totalOutgoingTrafficValue(totalObj.value(QStringLiteral("outBytesTotal"))); const uint64 totalIncomingTraffic = totalIncomingTrafficValue.isDouble() ? jsonValueToInt(totalIncomingTrafficValue) : unknownTraffic; const uint64 totalOutgoingTraffic = totalOutgoingTrafficValue.isDouble() ? jsonValueToInt(totalOutgoingTrafficValue) : unknownTraffic; double transferTime; 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_totalOutgoingRate != 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(QStringLiteral("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(QStringLiteral("connected")).toBool(false)) { dev.status = SyncthingDevStatus::Idle; } else { dev.status = SyncthingDevStatus::Disconnected; } break; default: if (!connectionObj.value(QStringLiteral("connected")).toBool(false)) { dev.status = SyncthingDevStatus::Disconnected; } } dev.paused = connectionObj.value(QStringLiteral("paused")).toBool(false); dev.totalIncomingTraffic = jsonValueToInt(connectionObj.value(QStringLiteral("inBytesTotal"))); dev.totalOutgoingTraffic = jsonValueToInt(connectionObj.value(QStringLiteral("outBytesTotal"))); dev.connectionAddress = connectionObj.value(QStringLiteral("address")).toString(); dev.connectionType = connectionObj.value(QStringLiteral("type")).toString(); dev.clientVersion = connectionObj.value(QStringLiteral("clientVersion")).toString(); emit devStatusChanged(dev, index); ++index; } m_lastConnectionsUpdate = DateTime::gmtNow(); // since there seems no event for this data, keep polling if (m_keepPolling && m_trafficPollTimer.interval()) { m_trafficPollTimer.start(); } break; } case QNetworkReply::OperationCanceledError: return; // intended, not an error default: emitError(tr("Unable to request connections: "), SyncthingErrorCategory::OverallConnection, reply); } } /*! * \brief Reads results of requestDirStatistics(). */ void SyncthingConnection::readDirStatistics() { auto *const reply = static_cast(sender()); reply->deleteLater(); 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(QStringLiteral("lastScan")).toString().toUtf8().data()); dirModified = true; } catch (const ConversionException &) { dirInfo.lastScanTime = DateTime(); } const QJsonObject lastFileObj(dirObj.value(QStringLiteral("lastFile")).toObject()); if (!lastFileObj.isEmpty()) { dirInfo.lastFileName = lastFileObj.value(QStringLiteral("filename")).toString(); dirModified = true; if (!dirInfo.lastFileName.isEmpty()) { dirInfo.lastFileDeleted = lastFileObj.value(QStringLiteral("deleted")).toBool(false); try { dirInfo.lastFileTime = DateTime::fromIsoStringLocal(lastFileObj.value(QStringLiteral("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; } break; } case QNetworkReply::OperationCanceledError: return; // intended, not an error default: emitError(tr("Unable to request directory statistics: "), SyncthingErrorCategory::OverallConnection, reply); } } /*! * \brief Reads results of requestDeviceStatistics(). */ void SyncthingConnection::readDeviceStatistics() { auto *const reply = static_cast(sender()); reply->deleteLater(); 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(QStringLiteral("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 && m_devStatsPollTimer.interval()) { m_devStatsPollTimer.start(); } break; } case QNetworkReply::OperationCanceledError: return; // intended, not an error default: emitError(tr("Unable to request device statistics: "), SyncthingErrorCategory::OverallConnection, reply); } } /*! * \brief Reads results of requestErrors(). */ void SyncthingConnection::readErrors() { auto *const reply = static_cast(sender()); reply->deleteLater(); if (reply == m_errorsReply) { m_errorsReply = nullptr; } // ignore any errors occured before connecting if (m_lastErrorTime.isNull()) { m_lastErrorTime = DateTime::now(); } switch (reply->error()) { case QNetworkReply::NoError: { const QByteArray response(reply->readAll()); QJsonParseError jsonError; const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { emitError(tr("Unable to parse errors: "), jsonError, reply, response); return; } for (const QJsonValue &errorVal : replyDoc.object().value(QStringLiteral("errors")).toArray()) { const QJsonObject errorObj(errorVal.toObject()); if (errorObj.isEmpty()) { continue; } try { const DateTime when = DateTime::fromIsoStringLocal(errorObj.value(QStringLiteral("when")).toString().toLocal8Bit().data()); if (m_lastErrorTime < when) { emitNotification(m_lastErrorTime = when, errorObj.value(QStringLiteral("message")).toString()); } } catch (const ConversionException &) { } } // since there seems no event for this data, keep polling if (m_keepPolling && m_errorsPollTimer.interval()) { m_errorsPollTimer.start(); } break; } case QNetworkReply::OperationCanceledError: return; // intended, not an error default: emitError(tr("Unable to request errors: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads results of requestClearingErrors(). */ void SyncthingConnection::readClearingErrors() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: break; default: emitError(tr("Unable to request clearing errors: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads results of requestEvents(). */ void SyncthingConnection::readEvents() { auto *const reply = static_cast(sender()); reply->deleteLater(); if (reply == m_eventsReply) { m_eventsReply = nullptr; } switch (reply->error()) { case QNetworkReply::NoError: { const QByteArray response(reply->readAll()); QJsonParseError jsonError; const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { emitError(tr("Unable to parse Syncthing events: "), jsonError, reply, response); handleFatalConnectionError(); return; } const QJsonArray replyArray = replyDoc.array(); emit newEvents(replyArray); // search the array for interesting events for (const QJsonValue &eventVal : replyArray) { const QJsonObject event = eventVal.toObject(); m_lastEventId = event.value(QStringLiteral("id")).toInt(m_lastEventId); DateTime eventTime; try { eventTime = DateTime::fromIsoStringGmt(event.value(QStringLiteral("time")).toString().toLocal8Bit().data()); } catch (const ConversionException &) { // ignore conversion error } const QString eventType(event.value(QStringLiteral("type")).toString()); const QJsonObject eventData(event.value(QStringLiteral("data")).toObject()); 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 } } #ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS if (!replyArray.isEmpty()) { cout << 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: // intended disconnect, not an error if (m_reconnecting) { // if reconnection flag is set, instantly etstablish a new connection ... continueReconnecting(); } else { // ... otherwise keep disconnected setStatus(SyncthingStatus::Disconnected); } return; default: emitError(tr("Unable to request Syncthing events: "), SyncthingErrorCategory::OverallConnection, reply); handleFatalConnectionError(); return; } if (m_keepPolling) { requestEvents(); setStatus(SyncthingStatus::Idle); } else { setStatus(SyncthingStatus::Disconnected); } } /*! * \brief Reads results of requestEvents(). */ void SyncthingConnection::readStartingEvent(const QJsonObject &eventData) { const QString configDir(eventData.value(QStringLiteral("home")).toString()); if (configDir != m_configDir) { emit configDirChanged(m_configDir = configDir); } emitMyIdChanged(eventData.value(QStringLiteral("myID")).toString()); } /*! * \brief Reads results of requestEvents(). */ void SyncthingConnection::readStatusChangedEvent(DateTime eventTime, const QJsonObject &eventData) { const QString dir(eventData.value(QStringLiteral("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(QStringLiteral("to")).toString(), eventTime); if (dirInfo->status == SyncthingDirStatus::OutOfSync) { const QString errorMessage(eventData.value(QStringLiteral("error")).toString()); if (!errorMessage.isEmpty()) { dirInfo->globalError = errorMessage; statusChanged = true; } } if (dirAlreadyPresent) { // emit status changed when dir already present if (statusChanged) { emit dirStatusChanged(*dirInfo, index); } } else { // request config for complete meta data of new directory requestConfig(); } } /*! * \brief Reads results of requestEvents(). */ void SyncthingConnection::readDownloadProgressEvent(DateTime eventTime, const QJsonObject &eventData) { VAR_UNUSED(eventTime) for (SyncthingDir &dirInfo : m_dirs) { // disappearing implies that the download has been finished so just wipe old entries dirInfo.downloadingItems.clear(); dirInfo.blocksAlreadyDownloaded = dirInfo.blocksToBeDownloaded = 0; // read progress of currently downloading items const QJsonObject dirObj(eventData.value(dirInfo.id).toObject()); if (!dirObj.isEmpty()) { dirInfo.downloadingItems.reserve(static_cast(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(dirInfo.blocksAlreadyDownloaded) * 100 / static_cast(dirInfo.blocksToBeDownloaded)) : 0; dirInfo.downloadLabel = QStringLiteral("%1 / %2 - %3 %") .arg(QString::fromLatin1(dataSizeToString(dirInfo.blocksAlreadyDownloaded > 0 ? static_cast(dirInfo.blocksAlreadyDownloaded) * SyncthingItemDownloadProgress::syncthingBlockSize : 0) .data()), QString::fromLatin1(dataSizeToString(dirInfo.blocksToBeDownloaded > 0 ? static_cast(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) { // find related dir info QString dir(eventData.value(QStringLiteral("folder")).toString()); if (dir.isEmpty()) { dir = eventData.value(QStringLiteral("id")).toString(); } if (dir.isEmpty()) { return; } int index; SyncthingDir *const dirInfo = findDirInfo(dir, index); if (!dirInfo) { return; } // read specific events if (eventType == QLatin1String("FolderErrors")) { readFolderErrors(eventTime, eventData, *dirInfo, index); } else if (eventType == QLatin1String("FolderSummary")) { readDirSummary(eventTime, eventData.value(QStringLiteral("summary")).toObject(), *dirInfo, index); } else if (eventType == QLatin1String("FolderCompletion") && dirInfo->lastStatisticsUpdate < eventTime) { readFolderCompletion(eventTime, eventData, *dirInfo, index); } else if (eventType == QLatin1String("FolderScanProgress")) { const double current = eventData.value(QStringLiteral("current")).toDouble(0); const double total = eventData.value(QStringLiteral("total")).toDouble(0); const double rate = eventData.value(QStringLiteral("rate")).toDouble(0); if (current > 0 && total > 0) { dirInfo->scanningPercentage = static_cast(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) { if (eventTime.isNull() && m_lastConnectionsUpdate.isNull() && eventTime < m_lastConnectionsUpdate) { return; // ignore device events happened before the last connections update } const QString dev(eventData.value(QStringLiteral("device")).toString()); if (dev.isEmpty()) { return; } // dev status changed, depending on event type int index; if (SyncthingDev *devInfo = findDevInfo(dev, index)) { SyncthingDevStatus status = devInfo->status; bool paused = devInfo->paused; if (eventType == QLatin1String("DeviceConnected")) { status = SyncthingDevStatus::Idle; // TODO: figure out when dev is actually syncing } else if (eventType == QLatin1String("DeviceDisconnected")) { status = SyncthingDevStatus::Disconnected; } else if (eventType == QLatin1String("DevicePaused")) { paused = true; } else if (eventType == QLatin1String("DeviceRejected")) { status = SyncthingDevStatus::Rejected; } else if (eventType == QLatin1String("DeviceResumed")) { paused = false; // FIXME: correct to assume device which has just been resumed is still disconnected? status = SyncthingDevStatus::Disconnected; } else if (eventType == QLatin1String("DeviceDiscovered")) { // we know about this device already, set status anyways because it might still be unknown if (status == SyncthingDevStatus::Unknown) { status = SyncthingDevStatus::Disconnected; } } else { return; // can't handle other event types currently } if (devInfo->status != status || devInfo->paused != paused) { if (devInfo->status != SyncthingDevStatus::OwnDevice) { // don't mess with the status of the own device devInfo->status = status; } devInfo->paused = paused; emit devStatusChanged(*devInfo, index); } } } /*! * \brief Reads results of requestEvents(). * \todo Implement this. */ void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject &eventData) { VAR_UNUSED(eventTime) VAR_UNUSED(eventData) } /*! * \brief Reads results of requestEvents(). */ void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject &eventData) { const auto dir(eventData.value(QLatin1String("folder")).toString()); if (dir.isEmpty()) { return; } int index; auto *const dirInfo = findDirInfo(dir, index); if (!dirInfo) { return; } // handle unsuccessfull operation const auto error(eventData.value(QStringLiteral("error")).toString()), item(eventData.value(QStringLiteral("item")).toString()); if (!error.isEmpty()) { if (dirInfo->status == SyncthingDirStatus::OutOfSync) { // FIXME: find better way to check whether the event is still relevant dirInfo->itemErrors.emplace_back(error, item); // emitNotification will trigger status update, so no need to call setStatus(status()) emit dirStatusChanged(*dirInfo, index); emitNotification(eventTime, error); } return; } // update last file if (dirInfo->lastFileTime.isNull() || eventTime < dirInfo->lastFileTime) { dirInfo->lastFileTime = eventTime; dirInfo->lastFileName = item; dirInfo->lastFileDeleted = (eventData.value(QStringLiteral("action")) != QLatin1String("delete")); if (eventTime > m_lastFileTime) { m_lastFileTime = dirInfo->lastFileTime, m_lastFileName = dirInfo->lastFileName, m_lastFileDeleted = dirInfo->lastFileDeleted; } emit dirStatusChanged(*dirInfo, index); } } void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index) { const QJsonArray errors(eventData.value(QStringLiteral("errors")).toArray()); if (errors.isEmpty()) { return; } for (const QJsonValue &errorVal : errors) { const QJsonObject error(errorVal.toObject()); if (error.isEmpty()) { continue; } auto &errors = dirInfo.itemErrors; SyncthingItemError dirError(error.value(QStringLiteral("error")).toString(), error.value(QStringLiteral("path")).toString()); if (find(errors.cbegin(), errors.cend(), dirError) != errors.cend()) { continue; } errors.emplace_back(move(dirError)); dirInfo.assignStatus(SyncthingDirStatus::OutOfSync, eventTime); // emit newNotification() for new errors const auto &previousErrors = dirInfo.previousItemErrors; if (find(previousErrors.cbegin(), previousErrors.cend(), dirInfo.itemErrors.back()) == previousErrors.cend()) { emitNotification(eventTime, dirInfo.itemErrors.back().message); } } emit dirStatusChanged(dirInfo, index); } void SyncthingConnection::readFolderCompletion(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index) { readFolderCompletion(eventTime, eventData, dirInfo, index, eventData.value(QLatin1String("device")).toString()); } void SyncthingConnection::readFolderCompletion( DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId) { if (devId.isEmpty() || devId == myId()) { readLocalFolderCompletion(eventTime, eventData, dirInfo, index); } else { readRemoteFolderCompletion(eventTime, eventData, dirInfo, index, devId); } } 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((globalStats.bytes - neededStats.bytes) * 100 / globalStats.bytes) : 100; emit dirStatusChanged(dirInfo, index); if (neededStats.isNull() && previouslyUpdated && (neededStats != previouslyNeeded || globalStats != previouslyGlobal)) { emit dirCompleted(eventTime, dirInfo, index); } } void SyncthingConnection::readRemoteFolderCompletion( DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId) { auto &completion = dirInfo.completionByDevice[devId]; auto &needed(completion.needed); const auto previouslyUpdated = !completion.lastUpdate.isNull(); const auto previouslyNeeded = !needed.isNull(); const auto previousGlobalBytes = completion.globalBytes; completion.lastUpdate = eventTime; completion.percentage = eventData.value(QLatin1String("completion")).toDouble(); completion.globalBytes = jsonValueToInt(eventData.value(QLatin1String("globalBytes"))); needed.bytes = jsonValueToInt(eventData.value(QLatin1String("needBytes")), needed.bytes); needed.items = jsonValueToInt(eventData.value(QLatin1String("needItems")), needed.items); needed.deletes = jsonValueToInt(eventData.value(QLatin1String("needDeletes")), needed.deletes); emit dirStatusChanged(dirInfo, index); if (needed.isNull() && previouslyUpdated && (previouslyNeeded || previousGlobalBytes != completion.globalBytes)) { int devIndex; if (const auto *const devInfo = findDevInfo(devId, devIndex)) { emit dirCompleted(DateTime::gmtNow(), dirInfo, index, devInfo); } } } /*! * \brief Reads results of requestEvents(). */ void SyncthingConnection::readRemoteIndexUpdated(DateTime eventTime, const QJsonObject &eventData) { // ignore those events if we're not updating completion automatically if (!m_requestCompletion) { return; } // find dev/dir const auto devId(eventData.value(QLatin1String("device")).toString()); const auto dirId(eventData.value(QLatin1String("folder")).toString()); if (dirId.isEmpty()) { return; } int index; auto *const dirInfo = findDirInfo(dirId, index); if (!dirInfo) { return; } // ignore event if we don't share the directory with the device if (!dirInfo->deviceIds.contains(devId)) { return; } // request completion again if out-of-date const auto &completion = dirInfo->completionByDevice[devId]; if (completion.lastUpdate < eventTime) { requestCompletion(devId, dirId); } } void SyncthingConnection::readPostConfig() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: emit newConfigTriggered(); break; default: emitError(tr("Unable to post config: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads results of rescan(). */ void SyncthingConnection::readRescan() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: emit rescanTriggered(reply->property("dirId").toString()); break; default: emitError(tr("Unable to request rescan: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads results of pauseDevice() and resumeDevice(). */ void SyncthingConnection::readDevPauseResume() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: { const QStringList devIds(reply->property("devIds").toStringList()); const bool resume = reply->property("resume").toBool(); setDevicesPaused(m_rawConfig, devIds, !resume); if (reply->property("resume").toBool()) { emit deviceResumeTriggered(devIds); } else { emit devicePauseTriggered(devIds); } break; } default: emitError(tr("Unable to request device pause/resume: "), SyncthingErrorCategory::SpecificRequest, reply); } } void SyncthingConnection::readDirPauseResume() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: { const QStringList dirIds(reply->property("dirIds").toStringList()); const bool resume = reply->property("resume").toBool(); setDirectoriesPaused(m_rawConfig, dirIds, !resume); if (resume) { emit directoryResumeTriggered(dirIds); } else { emit directoryPauseTriggered(dirIds); } break; } default: emitError(tr("Unable to request directory pause/resume: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads results of restart(). */ void SyncthingConnection::readRestart() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: emit restartTriggered(); break; default: emitError(tr("Unable to request restart: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads results of shutdown(). */ void SyncthingConnection::readShutdown() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: emit shutdownTriggered(); break; default: emitError(tr("Unable to request shutdown: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads data from requestDirStatus(). */ void SyncthingConnection::readDirStatus() { auto *const reply = static_cast(sender()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: { // determine relevant dir int index; const QString dirId(reply->property("dirId").toString()); SyncthingDir *const dir = findDirInfo(dirId, index); if (!dir) { // discard status for unknown dirs return; } // parse JSON const QByteArray response(reply->readAll()); QJsonParseError jsonError; const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError); if (jsonError.error != QJsonParseError::NoError) { emitError(tr("Unable to parse status for directory %1: ").arg(dirId), jsonError, reply, response); return; } if (readDirSummary(DateTime::gmtNow(), replyDoc.object(), *dir, index)) { recalculateStatus(); } break; } default: emitError(tr("Unable to request directory statistics: "), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \brief Reads data from requestDirStatus() and FolderSummary-event and stores them to \a dir. */ bool SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &summary, SyncthingDir &dir, int index) { if (summary.isEmpty() || dir.lastStatisticsUpdate > eventTime) { return false; } // backup previous statistics -> if there's no difference after all, don't emit completed event auto &globalStats(dir.globalStats); auto &localStats(dir.localStats); auto &neededStats(dir.neededStats); const auto previouslyUpdated(!dir.lastStatisticsUpdate.isNull()); const auto previouslyGlobal(globalStats); const auto previouslyNeeded(neededStats); // update statistics globalStats.bytes = jsonValueToInt(summary.value(QStringLiteral("globalBytes"))); globalStats.deletes = jsonValueToInt(summary.value(QStringLiteral("globalDeleted"))); globalStats.files = jsonValueToInt(summary.value(QStringLiteral("globalFiles"))); globalStats.dirs = jsonValueToInt(summary.value(QStringLiteral("globalDirectories"))); globalStats.symlinks = jsonValueToInt(summary.value(QStringLiteral("globalSymlinks"))); localStats.bytes = jsonValueToInt(summary.value(QStringLiteral("localBytes"))); localStats.deletes = jsonValueToInt(summary.value(QStringLiteral("localDeleted"))); localStats.files = jsonValueToInt(summary.value(QStringLiteral("localFiles"))); localStats.dirs = jsonValueToInt(summary.value(QStringLiteral("localDirectories"))); localStats.symlinks = jsonValueToInt(summary.value(QStringLiteral("localSymlinks"))); neededStats.bytes = jsonValueToInt(summary.value(QStringLiteral("needBytes"))); neededStats.deletes = jsonValueToInt(summary.value(QStringLiteral("needDeletes"))); neededStats.files = jsonValueToInt(summary.value(QStringLiteral("needFiles"))); neededStats.dirs = jsonValueToInt(summary.value(QStringLiteral("needDirectories"))); neededStats.symlinks = jsonValueToInt(summary.value(QStringLiteral("needSymlinks"))); dir.ignorePatterns = summary.value(QStringLiteral("ignorePatterns")).toBool(); dir.lastStatisticsUpdate = eventTime; // update status bool stateChanged = false; const QString state(summary.value(QStringLiteral("state")).toString()); if (!state.isEmpty()) { try { dir.assignStatus(state, DateTime::fromIsoStringGmt(summary.value(QStringLiteral("stateChanged")).toString().toUtf8().data())); } catch (const ConversionException &) { // FIXME: warning about invalid stateChanged } } dir.completionPercentage = globalStats.bytes ? static_cast((globalStats.bytes - neededStats.bytes) * 100 / globalStats.bytes) : 100; emit dirStatusChanged(dir, index); if (neededStats.isNull() && previouslyUpdated && (neededStats != previouslyNeeded || globalStats != previouslyGlobal)) { emit dirCompleted(eventTime, dir, index); } return stateChanged; } /*! * \brief Reads data from requestCompletion(). */ void SyncthingConnection::readCompletion() { auto *const reply = static_cast(sender()); const auto devId(reply->property("devId").toString()); const auto dirId(reply->property("dirId").toString()); reply->deleteLater(); switch (reply->error()) { case QNetworkReply::NoError: { // determine relevant dev/dir int index; auto *const dir = findDirInfo(dirId, index); // discard status for unknown dirs if (!dir) { return; } // parse JSON QJsonParseError jsonError; const auto response(reply->readAll()); const auto replyDoc(QJsonDocument::fromJson(response, &jsonError)); if (jsonError.error != QJsonParseError::NoError) { emitError(tr("Unable to parse completion for device/directory %1/%2: ").arg(devId, dirId), jsonError, reply, response); return; } // update the relevant completion info readRemoteFolderCompletion(DateTime::gmtNow(), replyDoc.object(), *dir, index, devId); break; } default: emitError(tr("Unable to request completion for device/directory %1/%2: ").arg(devId, dirId), SyncthingErrorCategory::SpecificRequest, reply); } } /*! * \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: case SyncthingStatus::Reconnecting: // don't consider synchronization finished in this this case 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 or synchronizing bool scanning = false; bool synchronizing = false; for (const SyncthingDir &dir : m_dirs) { switch (dir.status) { case SyncthingDirStatus::Synchronizing: synchronizing = true; break; case SyncthingDirStatus::Scanning: scanning = true; break; default:; } } 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 Interanlly called to emit the notification with the specified \a message. * \remarks Ensures the status is updated and the unread notifications flag is set. */ void SyncthingConnection::emitNotification(DateTime when, const QString &message) { m_unreadNotifications = true; emit newNotification(when, message); recalculateStatus(); } /*! * \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->readAll()); } /*! * \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 handle a fatal error when reading config (dirs/devs), status and events. */ void SyncthingConnection::handleFatalConnectionError() { setStatus(SyncthingStatus::Disconnected); if (m_autoReconnectTimer.interval()) { m_autoReconnectTimer.start(); } } /*! * \brief Internally called to recalculate the overall connection status, eg. 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::error() * \brief Indicates a request (for configuration, events, ...) failed. */ /*! * \fn SyncthingConnection::statusChanged() * \brief Indicates the status of the connection changed. */ /*! * \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