Improve sync complete notifications

* Also consider remote updates
* Still massive notifications when uploading
  about local folder completed
This commit is contained in:
Martchus 2018-03-29 00:38:21 +02:00
parent e6642245df
commit 6828e96b53
8 changed files with 115 additions and 42 deletions

View File

@ -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<int>(completionForDev.second.percentage), " %)")
.data(),
nullptr, 6);

View File

@ -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<int>(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<int>((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<int>((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:

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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<const SyncthingDir *>{ &dir }));
const auto message(syncCompleteString(std::vector<const SyncthingDir *>{ &dir }, remoteDev));
if (!message.isEmpty()) {
emit syncComplete(message);
}

View File

@ -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<const SyncthingDir *> &completedDirs)
QString syncCompleteString(const std::vector<const SyncthingDir *> &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<int>(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(", "));
}
/*!

View File

@ -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<const SyncthingDir *> &completedDirs);
QString LIB_SYNCTHING_CONNECTOR_EXPORT syncCompleteString(
const std::vector<const SyncthingDir *> &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<int>::max() ? std::numeric_limits<int>::max() : static_cast<int>(quandity);
}
template <class Objects, class Accessor> 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 <class Objects> 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