From 73c44591d5fccaed302d3e79d26fbeb0f3345ab8 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 4 Aug 2018 22:06:31 +0200 Subject: [PATCH] Consume disk events in SyncthingConnection First step to: * Show history of most recent file changes * Notify about file changes (see https://github.com/Martchus/syncthingtray/issues/7) --- connector/syncthingconnection.cpp | 177 +++++++++++++++++++++++------- connector/syncthingconnection.h | 7 ++ connector/syncthingdir.h | 10 ++ 3 files changed, 155 insertions(+), 39 deletions(-) diff --git a/connector/syncthingconnection.cpp b/connector/syncthingconnection.cpp index 9d3642c..7f66ba2 100644 --- a/connector/syncthingconnection.cpp +++ b/connector/syncthingconnection.cpp @@ -65,6 +65,7 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt , m_reconnecting(false) , m_requestCompletion(false) , m_lastEventId(0) + , m_lastDiskEventId(0) , m_autoReconnectTries(0) , m_totalIncomingTraffic(unknownTraffic) , m_totalOutgoingTraffic(unknownTraffic) @@ -76,6 +77,7 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt , m_errorsReply(nullptr) , m_eventsReply(nullptr) , m_versionReply(nullptr) + , m_diskEventsReply(nullptr) , m_unreadNotifications(false) , m_hasConfig(false) , m_hasStatus(false) @@ -248,6 +250,7 @@ void SyncthingConnection::continueReconnecting() m_keepPolling = true; m_reconnecting = false; m_lastEventId = 0; + m_lastDiskEventId = 0; m_configDir.clear(); m_myId.clear(); m_totalIncomingTraffic = unknownTraffic; @@ -548,6 +551,20 @@ SyncthingDir *SyncthingConnection::findDirInfo(const QString &dirId, int &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. * @@ -721,8 +738,9 @@ void SyncthingConnection::continueConnecting() } } // since config and status could be read successfully, let's poll for events - m_lastEventId = 0; + m_lastEventId = m_lastDiskEventId = 0; requestEvents(); + requestDiskEvents(); } /*! @@ -748,6 +766,9 @@ void SyncthingConnection::abortAllRequests() if (m_versionReply) { m_versionReply->abort(); } + if (m_diskEventsReply) { + m_diskEventsReply->abort(); + } } /*! @@ -866,6 +887,17 @@ void SyncthingConnection::requestVersion() &SyncthingConnection::readVersion); } +void SyncthingConnection::requestDiskEvents(int limit) +{ + QUrlQuery query; + query.addQueryItem(QStringLiteral("limit"), QString::number(limit)); + if (m_lastDiskEventId) { + query.addQueryItem(QStringLiteral("since"), QString::number(m_lastDiskEventId)); + } + QObject::connect( + m_diskEventsReply = requestData(QStringLiteral("events/disk"), query), &QNetworkReply::finished, this, &SyncthingConnection::readDiskEvents); +} + /*! * \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. @@ -1530,40 +1562,9 @@ void SyncthingConnection::readEvents() return; } - const QJsonArray replyArray = replyDoc.array(); + const auto 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(QLatin1String("id")).toInt(m_lastEventId); - DateTime eventTime; - try { - eventTime = DateTime::fromIsoStringGmt(event.value(QLatin1String("time")).toString().toLocal8Bit().data()); - } catch (const ConversionException &) { - // ignore conversion error - } - const QString eventType(event.value(QLatin1String("type")).toString()); - const QJsonObject eventData(event.value(QLatin1String("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 - } - } + readEventsFromJsonArray(replyArray, m_lastEventId); #ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS if (!replyArray.isEmpty()) { @@ -1600,6 +1601,46 @@ void SyncthingConnection::readEvents() } } +void SyncthingConnection::readEventsFromJsonArray(const QJsonArray &events, int &idVariable) +{ + for (const auto &eventVal : events) { + const auto event(eventVal.toObject()); + const auto eventTime([&] { + try { + return DateTime::fromIsoStringGmt(event.value(QLatin1String("time")).toString().toLocal8Bit().data()); + } catch (const ConversionException &) { + return DateTime(); // ignore conversion error + } + }()); + const auto eventType(event.value(QLatin1String("type")).toString()); + const auto eventData(event.value(QLatin1String("data")).toObject()); + + idVariable = event.value(QLatin1String("id")).toInt(idVariable); + + if (eventType == QLatin1String("Starting")) { + readStartingEvent(eventData); + } else if (eventType == QLatin1String("StateChanged")) { + readStatusChangedEvent(eventTime, eventData); + } else if (eventType == QLatin1String("DownloadProgress")) { + readDownloadProgressEvent(eventTime, eventData); + } else if (eventType.startsWith(QLatin1String("Folder"))) { + readDirEvent(eventTime, eventType, eventData); + } else if (eventType.startsWith(QLatin1String("Device"))) { + readDeviceEvent(eventTime, eventType, eventData); + } else if (eventType == QLatin1String("ItemStarted")) { + readItemStarted(eventTime, eventData); + } else if (eventType == QLatin1String("ItemFinished")) { + readItemFinished(eventTime, eventData); + } else if (eventType == QLatin1String("RemoteIndexUpdated")) { + readRemoteIndexUpdated(eventTime, eventData); + } else if (eventType == QLatin1String("ConfigSaved")) { + requestConfig(); // just consider current config as invalidated + } else if (eventType.endsWith(QLatin1String("ChangeDetected"))) { + readChangeEvent(eventTime, eventType, eventData); + } + } +} + /*! * \brief Reads results of requestEvents(). */ @@ -1830,12 +1871,8 @@ void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject */ 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); + auto *const dirInfo = findDirInfo(QLatin1String("folder"), eventData, &index); if (!dirInfo) { return; } @@ -2260,6 +2297,9 @@ void SyncthingConnection::readCompletion() } } +/*! + * \brief Reads data from requestVersion(). + */ void SyncthingConnection::readVersion() { auto *const reply = static_cast(sender()); @@ -2290,6 +2330,65 @@ void SyncthingConnection::readVersion() } } +/*! + * \brief Reads data from requestDiskEvents(). + */ +void SyncthingConnection::readDiskEvents() +{ + auto *const reply = static_cast(sender()); + reply->deleteLater(); + if (reply == m_diskEventsReply) { + m_diskEventsReply = nullptr; + } + + switch (reply->error()) { + case QNetworkReply::NoError: { + const QByteArray response(reply->readAll()); + QJsonParseError jsonError; + const auto replyDoc(QJsonDocument::fromJson(response, &jsonError)); + if (jsonError.error != QJsonParseError::NoError) { + emitError(tr("Unable to parse disk events: "), jsonError, reply, response); + return; + } + + readEventsFromJsonArray(replyDoc.array(), m_lastDiskEventId); + break; + } + case QNetworkReply::TimeoutError: + break; // no new events available, keep polling + case QNetworkReply::OperationCanceledError: + return; // intended, not an error + default: + emitError(tr("Unable to request disk events: "), SyncthingErrorCategory::OverallConnection, reply); + } + + if (m_keepPolling) { + requestDiskEvents(); + } +} + +/*! + * \brief Reads "LocalChangeDetected" and "RemoveChangeDetected" events from requestEvents() and requestDiskEvents(). + */ +void SyncthingConnection::readChangeEvent(DateTime eventTime, const QString &eventType, const QJsonObject &eventData) +{ + int index; + auto *const dirInfo(findDirInfo(QLatin1String("folderID"), eventData, &index)); + if (!dirInfo) { + return; + } + + SyncthingFileChange change; + change.local = eventType.startsWith("Local"); + change.eventTime = eventTime; + change.action = eventData.value(QLatin1String("action")).toString(); + change.type = eventData.value(QLatin1String("type")).toString(); + change.modifiedBy = eventData.value(QLatin1String("modifiedBy")).toString(); + change.path = eventData.value(QLatin1String("path")).toString(); + dirInfo->recentChanges.emplace_back(move(change)); + emit dirStatusChanged(*dirInfo, index); +} + /*! * \brief Sets the connection status. Ensures statusChanged() is emitted. * \param status Specifies the status; should be either SyncthingStatus::Disconnected, SyncthingStatus::Reconnecting, or diff --git a/connector/syncthingconnection.h b/connector/syncthingconnection.h index 0f9edb7..4cf8f1b 100644 --- a/connector/syncthingconnection.h +++ b/connector/syncthingconnection.h @@ -139,6 +139,7 @@ public: QMetaObject::Connection requestLog(std::function &)> callback); const QList &expectedSslErrors() const; SyncthingDir *findDirInfo(const QString &dirId, int &row); + SyncthingDir *findDirInfo(QLatin1String key, const QJsonObject &object, int *row = nullptr); SyncthingDir *findDirInfoByPath(const QString &path, QString &relativePath, int &row); SyncthingDev *findDevInfo(const QString &devId, int &row); SyncthingDev *findDevInfoByName(const QString &devName, int &row); @@ -183,6 +184,7 @@ public Q_SLOTS: void requestCompletion(const QString &devId, const QString &dirId); void requestDeviceStatistics(); void requestVersion(); + void requestDiskEvents(int limit = 25); void postConfigFromJsonObject(const QJsonObject &rawConfig); void postConfigFromByteArray(const QByteArray &rawConfig); @@ -229,6 +231,7 @@ private Q_SLOTS: void readErrors(); void readClearingErrors(); void readEvents(); + void readEventsFromJsonArray(const QJsonArray &events, int &idVariable); void readStartingEvent(const QJsonObject &eventData); void readStatusChangedEvent(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData); void readDownloadProgressEvent(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData); @@ -256,6 +259,8 @@ private Q_SLOTS: void readDevRejected(ChronoUtilities::DateTime eventTime, const QString &devId, const QJsonObject &eventData); void readCompletion(); void readVersion(); + void readDiskEvents(); + void readChangeEvent(ChronoUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData); void continueConnecting(); void continueReconnecting(); @@ -286,6 +291,7 @@ private: bool m_reconnecting; bool m_requestCompletion; int m_lastEventId; + int m_lastDiskEventId; QTimer m_trafficPollTimer; QTimer m_devStatsPollTimer; QTimer m_errorsPollTimer; @@ -303,6 +309,7 @@ private: QNetworkReply *m_errorsReply; QNetworkReply *m_eventsReply; QNetworkReply *m_versionReply; + QNetworkReply *m_diskEventsReply; bool m_unreadNotifications; bool m_hasConfig; bool m_hasStatus; diff --git a/connector/syncthingdir.h b/connector/syncthingdir.h index 7fa9f30..bb84bea 100644 --- a/connector/syncthingdir.h +++ b/connector/syncthingdir.h @@ -36,6 +36,15 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItemError { QString path; }; +struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingFileChange { + QString action; + QString type; + QString modifiedBy; + QString path; + ChronoUtilities::DateTime eventTime; + bool local = false; +}; + struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItemDownloadProgress { SyncthingItemDownloadProgress( const QString &containingDirPath = QString(), const QString &relativeItemPath = QString(), const QJsonObject &values = QJsonObject()); @@ -143,6 +152,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir { QString globalError; std::vector itemErrors; std::vector previousItemErrors; + std::vector recentChanges; SyncthingStatistics globalStats, localStats, neededStats; ChronoUtilities::DateTime lastStatisticsUpdate; ChronoUtilities::DateTime lastScanTime;