syncthingtray/syncthingconnector/syncthingdir.cpp
Martchus 843f164df1 Avoid potentially losing events
I have observed that Syncthing Tray can get stuck thinking a remote device
still needs data. Likely the update got lost. The code contains certain
conditions in which folder completion events and requesting completion are
supressed. Those conditions are based on timestamps. That is not ideal as
the accuracy is only one second (so different timestamps might be
considered equal as they are rounded to be the same). Additionally, it
makes no sense to assume a timestamp upon receiving a response as the
information might be older than the time it was received.

This change avoids those conditions. It rather uses the event ID to
decide what event/reply is newer.

This change also uses quint64 instead of int for event IDs to avoid running
into an overflow (or rather conversion error) when deserializing the ID
which might be bigger than int. (Not sure how big the ID can become; this
is to be on the safe side.)
2023-04-15 16:18:23 +02:00

255 lines
9.8 KiB
C++

#include "./syncthingdir.h"
#include <c++utilities/conversion/stringconversion.h>
#include <QCoreApplication>
#include <QJsonObject>
#include <QStringBuilder>
using namespace CppUtilities;
namespace Data {
QString statusString(SyncthingDirStatus status)
{
switch (status) {
case SyncthingDirStatus::Unknown:
return QCoreApplication::translate("SyncthingDirStatus", "unknown");
case SyncthingDirStatus::Idle:
return QCoreApplication::translate("SyncthingDirStatus", "idle");
case SyncthingDirStatus::Scanning:
return QCoreApplication::translate("SyncthingDirStatus", "scanning");
case SyncthingDirStatus::WaitingToScan:
return QCoreApplication::translate("SyncthingDirStatus", "waiting to scan");
case SyncthingDirStatus::WaitingToSync:
return QCoreApplication::translate("SyncthingDirStatus", "waiting to sync");
case SyncthingDirStatus::PreparingToSync:
return QCoreApplication::translate("SyncthingDirStatus", "preparing to sync");
case SyncthingDirStatus::Synchronizing:
return QCoreApplication::translate("SyncthingDirStatus", "synchronizing");
case SyncthingDirStatus::Cleaning:
return QCoreApplication::translate("SyncthingDirStatus", "cleaning");
case SyncthingDirStatus::WaitingToClean:
return QCoreApplication::translate("SyncthingDirStatus", "waiting to clean");
case SyncthingDirStatus::OutOfSync:
return QCoreApplication::translate("SyncthingDirStatus", "out of sync");
}
return QString();
}
QString dirTypeString(SyncthingDirType dirType)
{
switch (dirType) {
case SyncthingDirType::Unknown:
return QCoreApplication::translate("SyncthingDirType", "unknown");
case SyncthingDirType::SendReceive:
return QCoreApplication::translate("SyncthingDirType", "Send & Receive");
case SyncthingDirType::SendOnly:
return QCoreApplication::translate("SyncthingDirType", "Send only");
case SyncthingDirType::ReceiveOnly:
return QCoreApplication::translate("SyncthingDirType", "Receive only");
}
return QString();
}
bool SyncthingDir::checkWhetherStatusUpdateRelevant(SyncthingEventId eventId, DateTime time)
{
// ignore old updates
if (lastStatusUpdateEvent > eventId) {
return false;
}
lastStatusUpdateEvent = eventId;
lastStatusUpdateTime = time;
return true;
}
bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, SyncthingEventId eventId, DateTime time)
{
// handle obsoletion of out-of-sync items: no FolderErrors are accepted older than the last "sync" state are accepted
if (newStatus == SyncthingDirStatus::PreparingToSync || newStatus == SyncthingDirStatus::Synchronizing) {
// update time of last "sync" state and obsolete currently assigned errors
lastSyncStartedEvent = eventId;
lastSyncStartedTime = time; // used internally and not displayed, hence keep it GMT
itemErrors.clear();
pullErrorCount = 0;
} else if (lastSyncStartedTime.isNull() && newStatus != SyncthingDirStatus::OutOfSync) {
// prevent adding new errors from "before the first status" if the time of the last "sync" state is unknown
lastSyncStartedEvent = eventId;
lastSyncStartedTime = time;
}
// clear global error if not out-of-sync anymore
if (newStatus != SyncthingDirStatus::OutOfSync) {
globalError.clear();
}
// consider the directory still as out-of-sync if there are still pull errors
// note: Syncthing reports status changes to "idle" despite pull errors. This means we can only rely on reading
// a "FolderSummary" event without pull errors for clearing the out-of-sync status.
if (pullErrorCount && (newStatus == SyncthingDirStatus::Unknown || newStatus == SyncthingDirStatus::Idle)) {
newStatus = SyncthingDirStatus::OutOfSync;
}
if (newStatus == status) {
return false;
}
// update last scan time if the previous status was scanning
if (status == SyncthingDirStatus::Scanning) {
// FIXME: better use \a time and convert it from GMT to local time
lastScanTime = DateTime::now();
}
status = newStatus;
return true;
}
/*!
* \brief Assigns the status from the specified status string.
* \returns Returns whether the status has actually changed.
*/
bool SyncthingDir::assignStatus(const QString &statusStr, SyncthingEventId eventId, CppUtilities::DateTime time)
{
if (!checkWhetherStatusUpdateRelevant(eventId, time)) {
return false;
}
// identify statusStr
SyncthingDirStatus newStatus;
if (statusStr == QLatin1String("idle")) {
completionPercentage = 0;
newStatus = SyncthingDirStatus::Idle;
} else if (statusStr == QLatin1String("scanning")) {
newStatus = SyncthingDirStatus::Scanning;
} else if (statusStr == QLatin1String("scan-waiting")) {
newStatus = SyncthingDirStatus::WaitingToScan;
} else if (statusStr == QLatin1String("sync-waiting")) {
newStatus = SyncthingDirStatus::WaitingToSync;
} else if (statusStr == QLatin1String("sync-preparing")) {
// ensure status changed signal is emitted
if (!itemErrors.empty()) {
status = SyncthingDirStatus::Unknown;
}
newStatus = SyncthingDirStatus::PreparingToSync;
} else if (statusStr == QLatin1String("syncing")) {
// ensure status changed signal is emitted
if (!itemErrors.empty()) {
status = SyncthingDirStatus::Unknown;
}
newStatus = SyncthingDirStatus::Synchronizing;
} else if (statusStr == QLatin1String("cleaning")) {
newStatus = SyncthingDirStatus::Cleaning;
} else if (statusStr == QLatin1String("clean-waiting")) {
newStatus = SyncthingDirStatus::WaitingToClean;
} else if (statusStr == QLatin1String("error")) {
completionPercentage = 0;
newStatus = SyncthingDirStatus::OutOfSync;
} else {
newStatus = SyncthingDirStatus::Idle;
}
rawStatus = statusStr;
return finalizeStatusUpdate(newStatus, eventId, time);
}
bool SyncthingDir::assignDirType(const QString &dirTypeStr)
{
if (dirTypeStr == QLatin1String("sendreceive") || dirTypeStr == QLatin1String("readwrite")) {
dirType = SyncthingDirType::SendReceive;
} else if (dirTypeStr == QLatin1String("sendonly") || dirTypeStr == QLatin1String("readonly")) {
dirType = SyncthingDirType::SendOnly;
} else if (dirTypeStr == QLatin1String("receiveonly")) {
dirType = SyncthingDirType::ReceiveOnly;
} else {
dirType = SyncthingDirType::Unknown;
return false;
}
return true;
}
QString SyncthingDir::statusString() const
{
if (paused) {
return QCoreApplication::translate("SyncthingDir", "paused");
} else if (isUnshared()) {
return QCoreApplication::translate("SyncthingDir", "unshared");
} else if (status == SyncthingDirStatus::Unknown && !rawStatus.isEmpty()) {
return QString(rawStatus);
} else {
return ::Data::statusString(status);
}
}
QtUtilities::StringView SyncthingDir::pathWithoutTrailingSlash() const
{
auto dirPath = QtUtilities::makeStringView(path);
while (dirPath.endsWith(QChar('/'))) {
#if (QT_VERSION >= QT_VERSION_CHECK(5, 8, 0))
dirPath.chop(1);
#else
dirPath = dirPath.left(dirPath.size() - 1);
#endif
}
return dirPath;
}
bool SyncthingDir::areRemotesUpToDate() const
{
for (const auto &completionForDev : completionByDevice) {
if (!completionForDev.second.needed.isNull()) {
return false;
}
}
return true;
}
SyncthingItemDownloadProgress::SyncthingItemDownloadProgress(
const QString &containingDirPath, const QString &relativeItemPath, const QJsonObject &values)
: relativePath(relativeItemPath)
, fileInfo(containingDirPath % QChar('/') % QString(relativeItemPath).replace(QChar('\\'), QChar('/')))
, blocksCurrentlyDownloading(values.value(QLatin1String("Pulling")).toInt())
, blocksAlreadyDownloaded(values.value(QLatin1String("Pulled")).toInt())
, totalNumberOfBlocks(values.value(QLatin1String("Total")).toInt())
, downloadPercentage((blocksAlreadyDownloaded > 0 && totalNumberOfBlocks > 0)
? (static_cast<unsigned int>(blocksAlreadyDownloaded) * 100 / static_cast<unsigned int>(totalNumberOfBlocks))
: 0)
, blocksCopiedFromOrigin(values.value(QLatin1String("CopiedFromOrigin")).toInt())
, blocksCopiedFromElsewhere(values.value(QLatin1String("CopiedFromElsewhere")).toInt())
, blocksReused(values.value(QLatin1String("Reused")).toInt())
, bytesAlreadyHandled(values.value(QLatin1String("BytesDone")).toInt())
, totalNumberOfBytes(values.value(QLatin1String("BytesTotal")).toInt())
, label(QStringLiteral("%1 / %2 - %3 %")
.arg(QString::fromLatin1(
dataSizeToString(blocksAlreadyDownloaded > 0 ? static_cast<std::uint64_t>(blocksAlreadyDownloaded) * syncthingBlockSize : 0)
.data()),
QString::fromLatin1(
dataSizeToString(totalNumberOfBlocks > 0 ? static_cast<std::uint64_t>(totalNumberOfBlocks) * syncthingBlockSize : 0).data()),
QString::number(downloadPercentage)))
{
}
SyncthingStatistics &SyncthingStatistics::operator+=(const SyncthingStatistics &other)
{
bytes += other.bytes;
deletes += other.deletes;
dirs += other.dirs;
files += other.files;
symlinks += other.symlinks;
return *this;
}
/*!
* \brief Computes overall statistics for the specified \a directories.
*/
SyncthingOverallDirStatistics::SyncthingOverallDirStatistics(const std::vector<SyncthingDir> &directories)
{
for (const auto &dir : directories) {
local += dir.localStats;
global += dir.globalStats;
needed += dir.neededStats;
}
}
} // namespace Data