Compute the sync state for remote devices

See https://github.com/Martchus/syncthingtray/issues/62
This commit is contained in:
Martchus 2020-03-01 22:04:30 +01:00
parent 6921f9aa89
commit bafdb22c47
7 changed files with 173 additions and 79 deletions

View File

@ -10,6 +10,7 @@ set(META_PUBLIC_QT_MODULES Core Network)
# add project files
set(HEADER_FILES
syncthingcompletion.h
syncthingdir.h
syncthingdev.h
syncthingconnection.h

View File

@ -0,0 +1,84 @@
#ifndef DATA_SYNCTHING_COMPLETION_H
#define DATA_SYNCTHING_COMPLETION_H
#include "./global.h"
#include <c++utilities/chrono/datetime.h>
#include <QtGlobal>
namespace Data {
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingCompletion {
CppUtilities::DateTime lastUpdate;
double percentage = 0;
quint64 globalBytes = 0;
struct Needed {
quint64 bytes = 0;
quint64 items = 0;
quint64 deletes = 0;
constexpr bool isNull() const;
constexpr bool operator==(const Needed &other) const;
constexpr bool operator!=(const Needed &other) const;
constexpr Needed &operator+=(const Needed &other);
constexpr Needed &operator-=(const Needed &other);
} needed;
constexpr SyncthingCompletion &operator+=(const SyncthingCompletion &other);
constexpr SyncthingCompletion &operator-=(const SyncthingCompletion &other);
void recomputePercentage();
};
constexpr bool SyncthingCompletion::Needed::isNull() const
{
return bytes == 0 && items == 0 && deletes == 0;
}
constexpr bool SyncthingCompletion::Needed::operator==(const SyncthingCompletion::Needed &other) const
{
return bytes == other.bytes && items == other.items && deletes == other.deletes;
}
constexpr bool SyncthingCompletion::Needed::operator!=(const SyncthingCompletion::Needed &other) const
{
return !(*this == other);
}
constexpr SyncthingCompletion::Needed &SyncthingCompletion::Needed::operator+=(const SyncthingCompletion::Needed &other)
{
bytes += other.bytes;
items += other.items;
deletes += other.deletes;
return *this;
}
constexpr SyncthingCompletion::Needed &SyncthingCompletion::Needed::operator-=(const SyncthingCompletion::Needed &other)
{
bytes -= other.bytes;
items -= other.items;
deletes -= other.deletes;
return *this;
}
constexpr SyncthingCompletion &SyncthingCompletion::operator+=(const SyncthingCompletion &other)
{
lastUpdate = std::max(lastUpdate, other.lastUpdate);
globalBytes += other.globalBytes;
needed += other.needed;
return *this;
}
constexpr SyncthingCompletion &SyncthingCompletion::operator-=(const SyncthingCompletion &other)
{
globalBytes -= other.globalBytes;
needed -= other.needed;
return *this;
}
inline void SyncthingCompletion::recomputePercentage()
{
percentage = (static_cast<double>(globalBytes - needed.bytes) / globalBytes) * 100.0;
}
} // namespace Data
#endif // DATA_SYNCTHING_COMPLETION_H

View File

@ -75,7 +75,7 @@ SyncthingConnection::SyncthingConnection(const QString &syncthingUrl, const QByt
, m_keepPolling(false)
, m_abortingAllRequests(false)
, m_abortingToReconnect(false)
, m_requestCompletion(false)
, m_requestCompletion(true)
, m_lastEventId(0)
, m_lastDiskEventId(0)
, m_autoReconnectTries(0)

View File

@ -268,11 +268,13 @@ private Q_SLOTS:
void readItemStarted(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readItemFinished(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readFolderErrors(CppUtilities::DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index);
void readFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index);
void readFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId);
void readFolderCompletion(
CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &dirId, SyncthingDir *dirInfo, int dirIndex);
void readFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &devId, SyncthingDev *devInfo,
int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex);
void readLocalFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index);
void readRemoteFolderCompletion(
CppUtilities::DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId);
void readRemoteFolderCompletion(CppUtilities::DateTime eventTime, const QJsonObject &eventData, const QString &devId, SyncthingDev *devInfo,
int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex);
void readRemoteIndexUpdated(CppUtilities::DateTime eventTime, const QJsonObject &eventData);
void readPostConfig();
void readRescan();

View File

@ -1112,10 +1112,11 @@ void SyncthingConnection::readCompletion()
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) {
int devIndex, dirIndex;
auto *const devInfo = findDevInfo(devId, devIndex);
auto *const dirInfo = findDirInfo(dirId, dirIndex);
// discard status if the related dev and dir are unknown
if (!devInfo && !dirInfo) {
return;
}
@ -1129,7 +1130,7 @@ void SyncthingConnection::readCompletion()
}
// update the relevant completion info
readRemoteFolderCompletion(DateTime::gmtNow(), replyDoc.object(), *dir, index, devId);
readRemoteFolderCompletion(DateTime::gmtNow(), replyDoc.object(), devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
concludeConnection();
break;
@ -1723,6 +1724,10 @@ void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventT
return eventData.value(QLatin1String("id")).toString();
}());
if (dirId.isEmpty()) {
// handle events which don't necessarily require a corresponding dir info
if (eventType == eventType == QLatin1String("FolderCompletion")) {
readFolderCompletion(eventTime, eventData, dirId, nullptr, -1);
}
return;
}
@ -1745,7 +1750,7 @@ void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventT
} else if (eventType == QLatin1String("FolderSummary")) {
readDirSummary(eventTime, eventData.value(QLatin1String("summary")).toObject(), *dirInfo, index);
} else if (eventType == QLatin1String("FolderCompletion") && dirInfo->lastStatisticsUpdate < eventTime) {
readFolderCompletion(eventTime, eventData, *dirInfo, index);
readFolderCompletion(eventTime, eventData, dirId, dirInfo, index);
} else if (eventType == QLatin1String("FolderScanProgress")) {
const double current = eventData.value(QLatin1String("current")).toDouble(0);
const double total = eventData.value(QLatin1String("total")).toDouble(0);
@ -1800,7 +1805,7 @@ void SyncthingConnection::readDeviceEvent(DateTime eventTime, const QString &eve
SyncthingDevStatus status = devInfo->status;
bool paused = devInfo->paused;
if (eventType == QLatin1String("DeviceConnected")) {
status = SyncthingDevStatus::Idle; // TODO: figure out when dev is actually syncing
devInfo->setConnectedStateAccordingToCompletion();
} else if (eventType == QLatin1String("DeviceDisconnected")) {
status = SyncthingDevStatus::Disconnected;
} else if (eventType == QLatin1String("DevicePaused")) {
@ -1929,21 +1934,25 @@ void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
void SyncthingConnection::readFolderCompletion(
DateTime eventTime, const QJsonObject &eventData, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
readFolderCompletion(eventTime, eventData, dirInfo, index, eventData.value(QLatin1String("device")).toString());
const auto devId = eventData.value(QLatin1String("device")).toString();
int devIndex;
auto *const devInfo = findDevInfo(devId, devIndex);
readFolderCompletion(eventTime, eventData, devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
}
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readFolderCompletion(
DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId)
void SyncthingConnection::readFolderCompletion(DateTime eventTime, const QJsonObject &eventData, const QString &devId, SyncthingDev *devInfo,
int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
if (devId.isEmpty() || devId == myId()) {
readLocalFolderCompletion(eventTime, eventData, dirInfo, index);
} else {
readRemoteFolderCompletion(eventTime, eventData, dirInfo, index, devId);
if (devInfo && !devId.isEmpty() && devId != myId()) {
readRemoteFolderCompletion(eventTime, eventData, devId, devInfo, devIndex, dirId, dirInfo, dirIndex);
} else if (dirInfo) {
readLocalFolderCompletion(eventTime, eventData, *dirInfo, dirIndex);
}
}
@ -1975,27 +1984,44 @@ void SyncthingConnection::readLocalFolderCompletion(DateTime eventTime, const QJ
/*!
* \brief Reads results of requestEvents().
*/
void SyncthingConnection::readRemoteFolderCompletion(
DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index, const QString &devId)
void SyncthingConnection::readRemoteFolderCompletion(DateTime eventTime, const QJsonObject &eventData, const QString &devId, SyncthingDev *devInfo,
int devIndex, const QString &dirId, SyncthingDir *dirInfo, int dirIndex)
{
auto &completion = dirInfo.completionByDevice[devId];
// make new completion
auto completion = SyncthingCompletion();
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);
// update dir info
if (dirInfo) {
auto &previousCompletion = dirInfo->completionByDevice[devId];
const auto previouslyUpdated = !previousCompletion.lastUpdate.isNull();
const auto previouslyNeeded = !previousCompletion.needed.isNull();
const auto previousGlobalBytes = previousCompletion.globalBytes;
previousCompletion = completion;
emit dirStatusChanged(*dirInfo, dirIndex);
if (devInfo && needed.isNull() && previouslyUpdated && (previouslyNeeded || previousGlobalBytes != completion.globalBytes)) {
emit dirCompleted(DateTime::gmtNow(), *dirInfo, dirIndex, devInfo);
}
}
// update dev info
if (devInfo) {
auto &previousCompletion = devInfo->completionByDir[dirId];
devInfo->overallCompletion -= previousCompletion;
devInfo->overallCompletion += completion;
devInfo->overallCompletion.recomputePercentage();
previousCompletion = completion;
if (devInfo->isConnected()) {
devInfo->setConnectedStateAccordingToCompletion();
}
emit devStatusChanged(*devInfo, devIndex);
}
}
/*!
@ -2011,22 +2037,25 @@ void SyncthingConnection::readRemoteIndexUpdated(DateTime eventTime, const QJson
// find dev/dir
const auto devId(eventData.value(QLatin1String("device")).toString());
const auto dirId(eventData.value(QLatin1String("folder")).toString());
if (dirId.isEmpty()) {
if (devId.isEmpty() || dirId.isEmpty()) {
return;
}
int index;
auto *const dirInfo = findDirInfo(dirId, index);
if (!dirInfo) {
int devIndex, dirIndex;
auto *const devInfo = findDevInfo(devId, devIndex);
auto *const dirInfo = findDirInfo(dirId, dirIndex);
// discard if the related dev and dir are unknown
if (!devInfo && !dirInfo) {
return;
}
// ignore event if we don't share the directory with the device
if (!dirInfo->deviceIds.contains(devId)) {
// ignore event if we don't know the device and if we don't share the directory with the device
if (!devInfo && !dirInfo->deviceIds.contains(devId)) {
return;
}
// request completion again if out-of-date
const auto &completion = dirInfo->completionByDevice[devId];
const auto &completion = dirInfo ? dirInfo->completionByDevice[devId] : devInfo->completionByDir[dirId];
if (completion.lastUpdate < eventTime) {
requestCompletion(devId, dirId);
}

View File

@ -1,7 +1,7 @@
#ifndef DATA_SYNCTHINGDEV_H
#define DATA_SYNCTHINGDEV_H
#include "./global.h"
#include "./syncthingcompletion.h"
#include <c++utilities/chrono/datetime.h>
@ -19,9 +19,10 @@ enum class SyncthingDevStatus { Unknown, Disconnected, OwnDevice, Idle, Synchron
QString statusString(SyncthingDevStatus status);
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDev {
SyncthingDev(const QString &id = QString(), const QString &name = QString());
explicit SyncthingDev(const QString &id = QString(), const QString &name = QString());
QString statusString() const;
bool isConnected() const;
void setConnectedStateAccordingToCompletion();
const QString displayName() const;
QString id;
@ -30,16 +31,16 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDev {
QString compression;
QString certName;
SyncthingDevStatus status = SyncthingDevStatus::Unknown;
int progressPercentage = 0;
int progressRate = 0;
bool introducer = false;
bool paused = false;
std::uint64_t totalIncomingTraffic = 0;
std::uint64_t totalOutgoingTraffic = 0;
QString connectionAddress;
QString connectionType;
QString clientVersion;
CppUtilities::DateTime lastSeen;
std::unordered_map<QString, SyncthingCompletion> completionByDir;
SyncthingCompletion overallCompletion;
bool introducer = false;
bool paused = false;
};
inline SyncthingDev::SyncthingDev(const QString &id, const QString &name)
@ -54,12 +55,18 @@ inline bool SyncthingDev::isConnected() const
case SyncthingDevStatus::Unknown:
case SyncthingDevStatus::Disconnected:
case SyncthingDevStatus::OwnDevice:
case SyncthingDevStatus::Rejected:
return false;
default:
return true;
}
}
inline void SyncthingDev::setConnectedStateAccordingToCompletion()
{
status = overallCompletion.needed.isNull() ? SyncthingDevStatus::Idle : SyncthingDevStatus::Synchronizing;
}
inline const QString SyncthingDev::displayName() const
{
return name.isEmpty() ? id : name;

View File

@ -1,7 +1,7 @@
#ifndef DATA_SYNCTHINGDIR_H
#define DATA_SYNCTHINGDIR_H
#include "./global.h"
#include "./syncthingcompletion.h"
#include <c++utilities/chrono/datetime.h>
@ -23,7 +23,7 @@ enum class SyncthingDirType { Unknown, SendReceive, SendOnly, ReceiveOnly };
LIB_SYNCTHING_CONNECTOR_EXPORT QString dirTypeString(SyncthingDirType dirType);
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItemError {
SyncthingItemError(const QString &message = QString(), const QString &path = QString())
explicit SyncthingItemError(const QString &message = QString(), const QString &path = QString())
: message(message)
, path(path)
{
@ -48,7 +48,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingFileChange {
};
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItemDownloadProgress {
SyncthingItemDownloadProgress(
explicit SyncthingItemDownloadProgress(
const QString &containingDirPath = QString(), const QString &relativeItemPath = QString(), const QJsonObject &values = QJsonObject());
QString relativePath;
QFileInfo fileInfo;
@ -66,35 +66,6 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingItemDownloadProgress {
static constexpr unsigned int syncthingBlockSize = 128 * 1024;
};
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingCompletion {
CppUtilities::DateTime lastUpdate;
double percentage = 0;
quint64 globalBytes = 0;
struct Needed {
quint64 bytes = 0;
quint64 items = 0;
quint64 deletes = 0;
constexpr bool isNull() const;
constexpr bool operator==(const Needed &other) const;
constexpr bool operator!=(const Needed &other) const;
} needed;
};
constexpr bool SyncthingCompletion::Needed::isNull() const
{
return bytes == 0 && items == 0 && deletes == 0;
}
constexpr bool SyncthingCompletion::Needed::operator==(const SyncthingCompletion::Needed &other) const
{
return bytes == other.bytes && items == other.items && deletes == other.deletes;
}
constexpr bool SyncthingCompletion::Needed::operator!=(const SyncthingCompletion::Needed &other) const
{
return !(*this == other);
}
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingStatistics {
Q_GADGET
Q_PROPERTY(quint64 bytes MEMBER bytes)
@ -132,7 +103,7 @@ constexpr bool SyncthingStatistics::operator!=(const SyncthingStatistics &other)
}
struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
SyncthingDir(const QString &id = QString(), const QString &label = QString(), const QString &path = QString());
explicit SyncthingDir(const QString &id = QString(), const QString &label = QString(), const QString &path = QString());
bool assignStatus(const QString &statusStr, CppUtilities::DateTime time);
bool assignStatus(SyncthingDirStatus newStatus, CppUtilities::DateTime time);
bool assignDirType(const QString &dirType);
@ -231,8 +202,8 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingOverallDirStatistics {
Q_PROPERTY(SyncthingStatistics needed MEMBER needed)
public:
SyncthingOverallDirStatistics();
SyncthingOverallDirStatistics(const std::vector<SyncthingDir> &directories);
explicit SyncthingOverallDirStatistics();
explicit SyncthingOverallDirStatistics(const std::vector<SyncthingDir> &directories);
SyncthingStatistics local;
SyncthingStatistics global;