From 6828e96b5333480d541d81858b08460d0c24b5f2 Mon Sep 17 00:00:00 2001 From: Martchus Date: Thu, 29 Mar 2018 00:38:21 +0200 Subject: [PATCH] Improve sync complete notifications * Also consider remote updates * Still massive notifications when uploading about local folder completed --- cli/application.cpp | 2 +- connector/syncthingconnection.cpp | 60 ++++++++++++++++++++++--------- connector/syncthingdev.h | 6 ++++ connector/syncthingdir.cpp | 2 +- connector/syncthingdir.h | 38 ++++++++++++++++++-- connector/syncthingnotifier.cpp | 9 ++--- connector/utils.cpp | 19 ++++++---- connector/utils.h | 21 +++++++---- 8 files changed, 115 insertions(+), 42 deletions(-) diff --git a/cli/application.cpp b/cli/application.cpp index 8b7e0d6..5b718df 100644 --- a/cli/application.cpp +++ b/cli/application.cpp @@ -500,7 +500,7 @@ void Application::printDir(const RelevantDir &relevantDir) const printProperty("Remote progress", dir->areRemotesUpToDate() ? "all up-to-date" : "some need bytes"); for (const auto &completionForDev : dir->completionByDevice) { printProperty(m_connection.deviceNameOrId(completionForDev.first).toLocal8Bit().data(), - argsToString(dataSizeToString(completionForDev.second.globalBytes - completionForDev.second.neededBytes), ' ', '/', ' ', + argsToString(dataSizeToString(completionForDev.second.globalBytes - completionForDev.second.needed.bytes), ' ', '/', ' ', dataSizeToString(completionForDev.second.globalBytes), ' ', '(', static_cast(completionForDev.second.percentage), " %)") .data(), nullptr, 6); diff --git a/connector/syncthingconnection.cpp b/connector/syncthingconnection.cpp index 19f361a..4a085db 100644 --- a/connector/syncthingconnection.cpp +++ b/connector/syncthingconnection.cpp @@ -1657,16 +1657,24 @@ void SyncthingConnection::readDirEvent(DateTime eventTime, const QString &eventT emit dirStatusChanged(*dirInfo, index); } else if (eventType == QLatin1String("FolderSummary")) { readDirSummary(eventTime, eventData.value(QStringLiteral("summary")).toObject(), *dirInfo, index); - } else if (eventType == QLatin1String("FolderCompletion")) { - const int percentage = jsonValueToInt(eventData.value(QStringLiteral("completion"))); - dirInfo->globalStats.bytes = jsonValueToInt(eventData.value(QStringLiteral("globalBytes")), dirInfo->globalStats.bytes); - dirInfo->neededStats.bytes = jsonValueToInt(eventData.value(QStringLiteral("needBytes")), dirInfo->neededStats.bytes); - if (percentage >= 0 && percentage <= 100) { - dirInfo->completionPercentage = percentage; - emit dirStatusChanged(*dirInfo, index); - if (percentage == 100) { - emit dirCompleted(eventTime, *dirInfo, index); - } + } else if (eventType == QLatin1String("FolderCompletion") && dirInfo->lastStatisticsUpdate < eventTime) { + 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(QStringLiteral("globalBytes")), globalStats.bytes); + neededStats.bytes = jsonValueToInt(eventData.value(QStringLiteral("needBytes")), neededStats.bytes); + neededStats.deletes = jsonValueToInt(eventData.value(QStringLiteral("needDeletes")), neededStats.deletes); + neededStats.deletes = jsonValueToInt(eventData.value(QStringLiteral("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); } } else if (eventType == QLatin1String("FolderScanProgress")) { const double current = eventData.value(QStringLiteral("current")).toDouble(0); @@ -1963,20 +1971,25 @@ bool SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject & return false; } - // update statistics + // 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"))); - auto &localStats(dir.localStats); 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"))); - auto &neededStats(dir.neededStats); neededStats.bytes = jsonValueToInt(summary.value(QStringLiteral("needBytes"))); neededStats.deletes = jsonValueToInt(summary.value(QStringLiteral("needDeletes"))); neededStats.files = jsonValueToInt(summary.value(QStringLiteral("needFiles"))); @@ -1997,7 +2010,12 @@ bool SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject & } } + 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; } @@ -2033,13 +2051,23 @@ void SyncthingConnection::readCompletion() // update the relevant completion info const auto replyObj(replyDoc.object()); auto &completion = dir->completionByDevice[devId]; + auto &needed(completion.needed); + const auto previouslyUpdated = !completion.lastUpdate.isNull(); + const auto previouslyNeeded = !needed.isNull(); + const auto previousGlobalBytes = completion.globalBytes; completion.lastUpdate = DateTime::gmtNow(); completion.percentage = replyObj.value(QLatin1String("completion")).toDouble(); completion.globalBytes = jsonValueToInt(replyObj.value(QLatin1String("globalBytes"))); - completion.neededBytes = jsonValueToInt(replyObj.value(QLatin1String("needBytes"))); - completion.neededItems = jsonValueToInt(replyObj.value(QLatin1String("needItems"))); - completion.neededDeletes = jsonValueToInt(replyObj.value(QLatin1String("needDeletes"))); + needed.bytes = jsonValueToInt(replyObj.value(QLatin1String("needBytes")), needed.bytes); + needed.items = jsonValueToInt(replyObj.value(QLatin1String("needItems")), needed.items); + needed.deletes = jsonValueToInt(replyObj.value(QLatin1String("needDeletes")), needed.deletes); emit dirStatusChanged(*dir, index); + if (needed.isNull() && previouslyUpdated && (previouslyNeeded || previousGlobalBytes != completion.globalBytes)) { + int devIndex; + if (const auto *devInfo = findDevInfo(devId, devIndex)) { + emit dirCompleted(DateTime::gmtNow(), *dir, index, devInfo); + } + } break; } default: diff --git a/connector/syncthingdev.h b/connector/syncthingdev.h index b4b266b..39ec4d5 100644 --- a/connector/syncthingdev.h +++ b/connector/syncthingdev.h @@ -19,6 +19,7 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDev { SyncthingDev(const QString &id = QString(), const QString &name = QString()); QString statusString() const; bool isConnected() const; + const QString displayName() const; QString id; QString name; @@ -56,6 +57,11 @@ inline bool SyncthingDev::isConnected() const } } +inline const QString SyncthingDev::displayName() const +{ + return name.isEmpty() ? id : name; +} + } // namespace Data Q_DECLARE_METATYPE(Data::SyncthingDev) diff --git a/connector/syncthingdir.cpp b/connector/syncthingdir.cpp index 66a72fd..d008572 100644 --- a/connector/syncthingdir.cpp +++ b/connector/syncthingdir.cpp @@ -135,7 +135,7 @@ QStringRef SyncthingDir::pathWithoutTrailingSlash() const bool SyncthingDir::areRemotesUpToDate() const { for (const auto &completionForDev : completionByDevice) { - if (completionForDev.second.neededBytes) { + if (!completionForDev.second.needed.isNull()) { return false; } } diff --git a/connector/syncthingdir.h b/connector/syncthingdir.h index ee3b352..3d0a651 100644 --- a/connector/syncthingdir.h +++ b/connector/syncthingdir.h @@ -55,11 +55,31 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingCompletion { ChronoUtilities::DateTime lastUpdate; double percentage = 0; quint64 globalBytes = 0; - quint64 neededBytes = 0; - quint64 neededItems = 0; - quint64 neededDeletes = 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 { quint64 bytes = 0; quint64 deletes = 0; @@ -68,6 +88,8 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingStatistics { quint64 symlinks = 0; constexpr bool isNull() const; + constexpr bool operator==(const SyncthingStatistics &other) const; + constexpr bool operator!=(const SyncthingStatistics &other) const; }; constexpr bool SyncthingStatistics::isNull() const @@ -75,6 +97,16 @@ constexpr bool SyncthingStatistics::isNull() const return bytes == 0 && deletes == 0 && dirs == 0 && files == 0 && symlinks == 0; } +constexpr bool SyncthingStatistics::operator==(const SyncthingStatistics &other) const +{ + return bytes == other.bytes && deletes == other.deletes && dirs == other.dirs && files == other.files && symlinks == other.symlinks; +} + +constexpr bool SyncthingStatistics::operator!=(const SyncthingStatistics &other) const +{ + return !(*this == other); +} + struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir { SyncthingDir(const QString &id = QString(), const QString &label = QString(), const QString &path = QString()); bool assignStatus(const QString &statusStr, ChronoUtilities::DateTime time); diff --git a/connector/syncthingnotifier.cpp b/connector/syncthingnotifier.cpp index b2d966e..2edcfb3 100644 --- a/connector/syncthingnotifier.cpp +++ b/connector/syncthingnotifier.cpp @@ -90,21 +90,16 @@ void SyncthingNotifier::emitConnectedAndDisconnected(SyncthingStatus newStatus) */ void SyncthingNotifier::emitSyncComplete(ChronoUtilities::DateTime when, const SyncthingDir &dir, int index, const SyncthingDev *remoteDev) { + VAR_UNUSED(when) VAR_UNUSED(index) - VAR_UNUSED(remoteDev) // discard event if not enabled if ((m_enabledNotifications & SyncthingHighLevelNotification::SyncComplete) == 0 || !m_initialized) { return; } - // discard event if too old so we don't get "sync complete" messages for all dirs on startup - if ((DateTime::gmtNow() - when) > TimeSpan::fromSeconds(5)) { - return; - } - // format the notification message - const auto message(syncCompleteString(std::vector{ &dir })); + const auto message(syncCompleteString(std::vector{ &dir }, remoteDev)); if (!message.isEmpty()) { emit syncComplete(message); } diff --git a/connector/utils.cpp b/connector/utils.cpp index efb3261..7f0eb50 100644 --- a/connector/utils.cpp +++ b/connector/utils.cpp @@ -64,21 +64,26 @@ QString directoryStatusString(const SyncthingStatistics &stats) /*! * \brief Returns the "sync complete" notication message for the specified directories. */ -QString syncCompleteString(const std::vector &completedDirs) +QString syncCompleteString(const std::vector &completedDirs, const SyncthingDev *remoteDevice) { + const auto devName(remoteDevice ? remoteDevice->displayName() : QString()); switch (completedDirs.size()) { case 0: return QString(); case 1: - return QCoreApplication::translate("Data::Utils", "Synchronization of %1 complete").arg(completedDirs.front()->displayName()); + if (devName.isEmpty()) { + return QCoreApplication::translate("Data::Utils", "Synchronization of %1 complete").arg(completedDirs.front()->displayName()); + } + return QCoreApplication::translate("Data::Utils", "Synchronization of %1 on %2 complete").arg(completedDirs.front()->displayName(), devName); default:; } - QStringList names; - names.reserve(static_cast(completedDirs.size())); - for (const auto *dir : completedDirs) { - names << dir->displayName(); + const auto names(things(completedDirs, [](const auto *dir) { return dir->displayName(); })); + if (devName.isEmpty()) { + return QCoreApplication::translate("Data::Utils", "Synchronization of the following directories complete:\n") + + names.join(QStringLiteral(", ")); } - return QCoreApplication::translate("Data::Utils", "Synchronization of the following devices complete:\n") + names.join(QStringLiteral(", ")); + return QCoreApplication::translate("Data::Utils", "Synchronization of the following directories on %1 complete:\n").arg(devName) + + names.join(QStringLiteral(", ")); } /*! diff --git a/connector/utils.h b/connector/utils.h index 42a98bf..3a53b20 100644 --- a/connector/utils.h +++ b/connector/utils.h @@ -23,11 +23,13 @@ namespace Data { struct SyncthingStatistics; struct SyncthingDir; +struct SyncthingDev; QString LIB_SYNCTHING_CONNECTOR_EXPORT agoString(ChronoUtilities::DateTime dateTime); QString LIB_SYNCTHING_CONNECTOR_EXPORT trafficString(uint64 total, double rate); QString LIB_SYNCTHING_CONNECTOR_EXPORT directoryStatusString(const Data::SyncthingStatistics &stats); -QString LIB_SYNCTHING_CONNECTOR_EXPORT syncCompleteString(const std::vector &completedDirs); +QString LIB_SYNCTHING_CONNECTOR_EXPORT syncCompleteString( + const std::vector &completedDirs, const SyncthingDev *remoteDevice = nullptr); bool LIB_SYNCTHING_CONNECTOR_EXPORT isLocal(const QUrl &url); bool LIB_SYNCTHING_CONNECTOR_EXPORT setDirectoriesPaused(QJsonObject &syncthingConfig, const QStringList &dirIds, bool paused); bool LIB_SYNCTHING_CONNECTOR_EXPORT setDevicesPaused(QJsonObject &syncthingConfig, const QStringList &dirs, bool paused); @@ -43,14 +45,19 @@ constexpr int LIB_SYNCTHING_CONNECTOR_EXPORT trQuandity(quint64 quandity) return quandity > std::numeric_limits::max() ? std::numeric_limits::max() : static_cast(quandity); } +template QStringList LIB_SYNCTHING_CONNECTOR_EXPORT things(const Objects &objects, Accessor accessor) +{ + QStringList things; + things.reserve(objects.size()); + for (const auto &object : objects) { + things << accessor(object); + } + return things; +} + template QStringList LIB_SYNCTHING_CONNECTOR_EXPORT ids(const Objects &objects) { - QStringList ids; - ids.reserve(objects.size()); - for (const auto &object : objects) { - ids << object.id; - } - return ids; + return things(objects, [](const auto &object) { return object.id; }); } } // namespace Data