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)
This commit is contained in:
parent
ccbd6f3386
commit
73c44591d5
|
@ -65,6 +65,7 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt
|
||||||
, m_reconnecting(false)
|
, m_reconnecting(false)
|
||||||
, m_requestCompletion(false)
|
, m_requestCompletion(false)
|
||||||
, m_lastEventId(0)
|
, m_lastEventId(0)
|
||||||
|
, m_lastDiskEventId(0)
|
||||||
, m_autoReconnectTries(0)
|
, m_autoReconnectTries(0)
|
||||||
, m_totalIncomingTraffic(unknownTraffic)
|
, m_totalIncomingTraffic(unknownTraffic)
|
||||||
, m_totalOutgoingTraffic(unknownTraffic)
|
, m_totalOutgoingTraffic(unknownTraffic)
|
||||||
|
@ -76,6 +77,7 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt
|
||||||
, m_errorsReply(nullptr)
|
, m_errorsReply(nullptr)
|
||||||
, m_eventsReply(nullptr)
|
, m_eventsReply(nullptr)
|
||||||
, m_versionReply(nullptr)
|
, m_versionReply(nullptr)
|
||||||
|
, m_diskEventsReply(nullptr)
|
||||||
, m_unreadNotifications(false)
|
, m_unreadNotifications(false)
|
||||||
, m_hasConfig(false)
|
, m_hasConfig(false)
|
||||||
, m_hasStatus(false)
|
, m_hasStatus(false)
|
||||||
|
@ -248,6 +250,7 @@ void SyncthingConnection::continueReconnecting()
|
||||||
m_keepPolling = true;
|
m_keepPolling = true;
|
||||||
m_reconnecting = false;
|
m_reconnecting = false;
|
||||||
m_lastEventId = 0;
|
m_lastEventId = 0;
|
||||||
|
m_lastDiskEventId = 0;
|
||||||
m_configDir.clear();
|
m_configDir.clear();
|
||||||
m_myId.clear();
|
m_myId.clear();
|
||||||
m_totalIncomingTraffic = unknownTraffic;
|
m_totalIncomingTraffic = unknownTraffic;
|
||||||
|
@ -548,6 +551,20 @@ SyncthingDir *SyncthingConnection::findDirInfo(const QString &dirId, int &row)
|
||||||
return nullptr;
|
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.
|
* \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
|
// since config and status could be read successfully, let's poll for events
|
||||||
m_lastEventId = 0;
|
m_lastEventId = m_lastDiskEventId = 0;
|
||||||
requestEvents();
|
requestEvents();
|
||||||
|
requestDiskEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -748,6 +766,9 @@ void SyncthingConnection::abortAllRequests()
|
||||||
if (m_versionReply) {
|
if (m_versionReply) {
|
||||||
m_versionReply->abort();
|
m_versionReply->abort();
|
||||||
}
|
}
|
||||||
|
if (m_diskEventsReply) {
|
||||||
|
m_diskEventsReply->abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
|
@ -866,6 +887,17 @@ void SyncthingConnection::requestVersion()
|
||||||
&SyncthingConnection::readVersion);
|
&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.
|
* \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.
|
* \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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const QJsonArray replyArray = replyDoc.array();
|
const auto replyArray(replyDoc.array());
|
||||||
emit newEvents(replyArray);
|
emit newEvents(replyArray);
|
||||||
// search the array for interesting events
|
readEventsFromJsonArray(replyArray, m_lastEventId);
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS
|
#ifdef LIB_SYNCTHING_CONNECTOR_LOG_SYNCTHING_EVENTS
|
||||||
if (!replyArray.isEmpty()) {
|
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().
|
* \brief Reads results of requestEvents().
|
||||||
*/
|
*/
|
||||||
|
@ -1830,12 +1871,8 @@ void SyncthingConnection::readItemStarted(DateTime eventTime, const QJsonObject
|
||||||
*/
|
*/
|
||||||
void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject &eventData)
|
void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject &eventData)
|
||||||
{
|
{
|
||||||
const auto dir(eventData.value(QLatin1String("folder")).toString());
|
|
||||||
if (dir.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int index;
|
int index;
|
||||||
auto *const dirInfo = findDirInfo(dir, index);
|
auto *const dirInfo = findDirInfo(QLatin1String("folder"), eventData, &index);
|
||||||
if (!dirInfo) {
|
if (!dirInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -2260,6 +2297,9 @@ void SyncthingConnection::readCompletion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Reads data from requestVersion().
|
||||||
|
*/
|
||||||
void SyncthingConnection::readVersion()
|
void SyncthingConnection::readVersion()
|
||||||
{
|
{
|
||||||
auto *const reply = static_cast<QNetworkReply *>(sender());
|
auto *const reply = static_cast<QNetworkReply *>(sender());
|
||||||
|
@ -2290,6 +2330,65 @@ void SyncthingConnection::readVersion()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* \brief Reads data from requestDiskEvents().
|
||||||
|
*/
|
||||||
|
void SyncthingConnection::readDiskEvents()
|
||||||
|
{
|
||||||
|
auto *const reply = static_cast<QNetworkReply *>(sender());
|
||||||
|
reply->deleteLater();
|
||||||
|
if (reply == m_diskEventsReply) {
|
||||||
|
m_diskEventsReply = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (reply->error()) {
|
||||||
|
case QNetworkReply::NoError: {
|
||||||
|
const QByteArray response(reply->readAll());
|
||||||
|
QJsonParseError jsonError;
|
||||||
|
const auto replyDoc(QJsonDocument::fromJson(response, &jsonError));
|
||||||
|
if (jsonError.error != QJsonParseError::NoError) {
|
||||||
|
emitError(tr("Unable to parse disk events: "), jsonError, reply, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
* \brief Sets the connection status. Ensures statusChanged() is emitted.
|
||||||
* \param status Specifies the status; should be either SyncthingStatus::Disconnected, SyncthingStatus::Reconnecting, or
|
* \param status Specifies the status; should be either SyncthingStatus::Disconnected, SyncthingStatus::Reconnecting, or
|
||||||
|
|
|
@ -139,6 +139,7 @@ public:
|
||||||
QMetaObject::Connection requestLog(std::function<void(const std::vector<SyncthingLogEntry> &)> callback);
|
QMetaObject::Connection requestLog(std::function<void(const std::vector<SyncthingLogEntry> &)> callback);
|
||||||
const QList<QSslError> &expectedSslErrors() const;
|
const QList<QSslError> &expectedSslErrors() const;
|
||||||
SyncthingDir *findDirInfo(const QString &dirId, int &row);
|
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);
|
SyncthingDir *findDirInfoByPath(const QString &path, QString &relativePath, int &row);
|
||||||
SyncthingDev *findDevInfo(const QString &devId, int &row);
|
SyncthingDev *findDevInfo(const QString &devId, int &row);
|
||||||
SyncthingDev *findDevInfoByName(const QString &devName, 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 requestCompletion(const QString &devId, const QString &dirId);
|
||||||
void requestDeviceStatistics();
|
void requestDeviceStatistics();
|
||||||
void requestVersion();
|
void requestVersion();
|
||||||
|
void requestDiskEvents(int limit = 25);
|
||||||
void postConfigFromJsonObject(const QJsonObject &rawConfig);
|
void postConfigFromJsonObject(const QJsonObject &rawConfig);
|
||||||
void postConfigFromByteArray(const QByteArray &rawConfig);
|
void postConfigFromByteArray(const QByteArray &rawConfig);
|
||||||
|
|
||||||
|
@ -229,6 +231,7 @@ private Q_SLOTS:
|
||||||
void readErrors();
|
void readErrors();
|
||||||
void readClearingErrors();
|
void readClearingErrors();
|
||||||
void readEvents();
|
void readEvents();
|
||||||
|
void readEventsFromJsonArray(const QJsonArray &events, int &idVariable);
|
||||||
void readStartingEvent(const QJsonObject &eventData);
|
void readStartingEvent(const QJsonObject &eventData);
|
||||||
void readStatusChangedEvent(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData);
|
void readStatusChangedEvent(ChronoUtilities::DateTime eventTime, const QJsonObject &eventData);
|
||||||
void readDownloadProgressEvent(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 readDevRejected(ChronoUtilities::DateTime eventTime, const QString &devId, const QJsonObject &eventData);
|
||||||
void readCompletion();
|
void readCompletion();
|
||||||
void readVersion();
|
void readVersion();
|
||||||
|
void readDiskEvents();
|
||||||
|
void readChangeEvent(ChronoUtilities::DateTime eventTime, const QString &eventType, const QJsonObject &eventData);
|
||||||
|
|
||||||
void continueConnecting();
|
void continueConnecting();
|
||||||
void continueReconnecting();
|
void continueReconnecting();
|
||||||
|
@ -286,6 +291,7 @@ private:
|
||||||
bool m_reconnecting;
|
bool m_reconnecting;
|
||||||
bool m_requestCompletion;
|
bool m_requestCompletion;
|
||||||
int m_lastEventId;
|
int m_lastEventId;
|
||||||
|
int m_lastDiskEventId;
|
||||||
QTimer m_trafficPollTimer;
|
QTimer m_trafficPollTimer;
|
||||||
QTimer m_devStatsPollTimer;
|
QTimer m_devStatsPollTimer;
|
||||||
QTimer m_errorsPollTimer;
|
QTimer m_errorsPollTimer;
|
||||||
|
@ -303,6 +309,7 @@ private:
|
||||||
QNetworkReply *m_errorsReply;
|
QNetworkReply *m_errorsReply;
|
||||||
QNetworkReply *m_eventsReply;
|
QNetworkReply *m_eventsReply;
|
||||||
QNetworkReply *m_versionReply;
|
QNetworkReply *m_versionReply;
|
||||||
|
QNetworkReply *m_diskEventsReply;
|
||||||
bool m_unreadNotifications;
|
bool m_unreadNotifications;
|
||||||
bool m_hasConfig;
|
bool m_hasConfig;
|
||||||
bool m_hasStatus;
|
bool m_hasStatus;
|
||||||
|
|
|
@ -36,6 +36,15 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItemError {
|
||||||
QString path;
|
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 {
|
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItemDownloadProgress {
|
||||||
SyncthingItemDownloadProgress(
|
SyncthingItemDownloadProgress(
|
||||||
const QString &containingDirPath = QString(), const QString &relativeItemPath = QString(), const QJsonObject &values = QJsonObject());
|
const QString &containingDirPath = QString(), const QString &relativeItemPath = QString(), const QJsonObject &values = QJsonObject());
|
||||||
|
@ -143,6 +152,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
|
||||||
QString globalError;
|
QString globalError;
|
||||||
std::vector<SyncthingItemError> itemErrors;
|
std::vector<SyncthingItemError> itemErrors;
|
||||||
std::vector<SyncthingItemError> previousItemErrors;
|
std::vector<SyncthingItemError> previousItemErrors;
|
||||||
|
std::vector<SyncthingFileChange> recentChanges;
|
||||||
SyncthingStatistics globalStats, localStats, neededStats;
|
SyncthingStatistics globalStats, localStats, neededStats;
|
||||||
ChronoUtilities::DateTime lastStatisticsUpdate;
|
ChronoUtilities::DateTime lastStatisticsUpdate;
|
||||||
ChronoUtilities::DateTime lastScanTime;
|
ChronoUtilities::DateTime lastScanTime;
|
||||||
|
|
Loading…
Reference in New Issue