Fix and refactor directory error handling

* Rely not only on the FolderErrors event
* Request pull errors when opening the dialog
* Use "pullErrors" from FolderSummary
* Move code for directory error dialog into
  its own class

This should fix that sometimes obsolete errors
were still displayed or actually present errors
missing.
This commit is contained in:
Martchus 2018-11-03 21:30:17 +01:00
parent dcbc19cf8c
commit 4a42a3f0c0
19 changed files with 371 additions and 165 deletions

View File

@ -203,12 +203,6 @@ void SyncthingConnection::connect()
// reset status
m_reconnecting = m_hasConfig = m_hasStatus = m_hasEvents = m_hasDiskEvents = false;
// remove error items (might have been invalidated)
for (SyncthingDir &dir : m_dirs) {
dir.itemErrors.swap(dir.previousItemErrors);
dir.itemErrors.clear();
}
// check configuration
if (m_apiKey.isEmpty() || m_syncthingUrl.isEmpty()) {
emit error(tr("Connection configuration is insufficient."), SyncthingErrorCategory::OverallConnection, QNetworkReply::NoError);

View File

@ -151,9 +151,11 @@ public:
std::vector<const SyncthingDev *> connectedDevices() const;
const QJsonObject &rawConfig() const;
SyncthingDir *findDirInfo(const QString &dirId, int &row);
const SyncthingDir *findDirInfo(const QString &dirId, int &row) const;
SyncthingDir *findDirInfo(QLatin1String key, const QJsonObject &object, int *row = nullptr);
SyncthingDir *findDirInfoByPath(const QString &path, QString &relativePath, int &row);
SyncthingDev *findDevInfo(const QString &devId, int &row);
const SyncthingDev *findDevInfo(const QString &devId, int &row) const;
SyncthingDev *findDevInfoByName(const QString &devName, int &row);
const QList<QSslError> &expectedSslErrors() const;
@ -197,6 +199,7 @@ public Q_SLOTS:
void requestClearingErrors();
void requestDirStatistics();
void requestDirStatus(const QString &dirId);
void requestDirPullErrors(const QString &dirId, int page = 0, int perPage = 0);
void requestCompletion(const QString &devId, const QString &dirId);
void requestDeviceStatistics();
void requestVersion();
@ -273,6 +276,7 @@ private Q_SLOTS:
void readRestart();
void readShutdown();
void readDirStatus();
void readDirPullErrors();
void readDirSummary(ChronoUtilities::DateTime eventTime, const QJsonObject &summary, SyncthingDir &dirInfo, int index);
void readDirRejected(ChronoUtilities::DateTime eventTime, const QString &dirId, const QJsonObject &eventData);
void readDevRejected(ChronoUtilities::DateTime eventTime, const QString &devId, const QJsonObject &eventData);
@ -716,6 +720,27 @@ inline const QJsonObject &SyncthingConnection::rawConfig() const
{
return m_rawConfig;
}
/*!
* \brief Returns the directory info object for the directory with the specified ID.
* \returns Returns a pointer to the object or nullptr if not found.
* \remarks The returned object becomes invalid when the newDirs() signal is emitted or the connection is destroyed.
*/
inline const SyncthingDir *SyncthingConnection::findDirInfo(const QString &dirId, int &row) const
{
return const_cast<SyncthingConnection *>(this)->findDirInfo(dirId, row);
}
/*!
* \brief Returns the device info object for the device with the specified ID.
* \returns Returns a pointer to the object or nullptr if not found.
* \remarks The returned object becomes invalid when the newConfig() signal is emitted or the connection is destroyed.
*/
inline const SyncthingDev *SyncthingConnection::findDevInfo(const QString &devId, int &row) const
{
return const_cast<SyncthingConnection *>(this)->findDevInfo(devId, row);
}
} // namespace Data
Q_DECLARE_METATYPE(Data::SyncthingLogEntry)

View File

@ -952,6 +952,62 @@ void SyncthingConnection::readDirStatus()
}
}
/*!
* \brief Requests pull errors for \a dirId asynchronously.
*
* The dirStatusChanged() signal is emitted on success and error() in the error case.
*/
void SyncthingConnection::requestDirPullErrors(const QString &dirId, int page, int perPage)
{
QUrlQuery query;
query.addQueryItem(QStringLiteral("folder"), dirId);
if (page > 0 && perPage > 0) {
query.addQueryItem(QStringLiteral("page"), QString::number(page));
query.addQueryItem(QStringLiteral("perpage"), QString::number(perPage));
}
auto *const reply = requestData(QStringLiteral("folder/pullerrors"), query);
reply->setProperty("dirId", dirId);
QObject::connect(reply, &QNetworkReply::finished, this, &SyncthingConnection::readDirPullErrors);
}
/*!
* \brief Reads data from requestDirPullErrors().
*/
void SyncthingConnection::readDirPullErrors()
{
auto *const reply = static_cast<QNetworkReply *>(sender());
reply->deleteLater();
// determine relevant dir
int index;
const QString dirId(reply->property("dirId").toString());
SyncthingDir *const dir = findDirInfo(dirId, index);
if (!dir) {
// discard errors for unknown dirs
return;
}
switch (reply->error()) {
case QNetworkReply::NoError: {
// parse JSON
const QByteArray response(reply->readAll());
QJsonParseError jsonError;
const QJsonDocument replyDoc = QJsonDocument::fromJson(response, &jsonError);
if (jsonError.error != QJsonParseError::NoError) {
emitError(tr("Unable to parse pull errors for directory %1: ").arg(dirId), jsonError, reply, response);
return;
}
readFolderErrors(DateTime::gmtNow(), replyDoc.object(), *dir, index);
break;
}
case QNetworkReply::OperationCanceledError:
return;
default:
emitError(tr("Unable to request pull errors for directory %1: ").arg(dirId), SyncthingErrorCategory::SpecificRequest, reply);
}
}
/*!
* \brief Requests completion for \a devId and \a dirId asynchronously.
*/
@ -1279,6 +1335,7 @@ void SyncthingConnection::readDirSummary(DateTime eventTime, const QJsonObject &
neededStats.files = jsonValueToInt(summary.value(QLatin1String("needFiles")));
neededStats.dirs = jsonValueToInt(summary.value(QLatin1String("needDirectories")));
neededStats.symlinks = jsonValueToInt(summary.value(QLatin1String("needSymlinks")));
dir.pullErrorCount = jsonValueToInt(summary.value(QLatin1String("pullErrors")));
dir.ignorePatterns = summary.value(QLatin1String("ignorePatterns")).toBool();
dir.lastStatisticsUpdate = eventTime;
@ -1715,13 +1772,24 @@ void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject
// handle unsuccessful operation
const auto error(eventData.value(QLatin1String("error")).toString()), item(eventData.value(QLatin1String("item")).toString());
if (!error.isEmpty()) {
if (dirInfo->status == SyncthingDirStatus::OutOfSync) {
// add error item if not already present
if (dirInfo->status != SyncthingDirStatus::OutOfSync) {
// FIXME: find better way to check whether the event is still relevant
dirInfo->itemErrors.emplace_back(error, item);
// emitNotification will trigger status update, so no need to call setStatus(status())
emit dirStatusChanged(*dirInfo, index);
emitNotification(eventTime, error);
return;
}
for (const auto &itemError : dirInfo->itemErrors) {
if (itemError.message == error && itemError.path == item) {
return;
}
}
dirInfo->itemErrors.emplace_back(error, item);
if (dirInfo->pullErrorCount < dirInfo->itemErrors.size()) {
dirInfo->pullErrorCount = dirInfo->itemErrors.size();
}
// emitNotification will trigger status update, so no need to call setStatus(status())
emit dirStatusChanged(*dirInfo, index);
emitNotification(eventTime, error);
return;
}
@ -1740,31 +1808,38 @@ void SyncthingConnection::readItemFinished(DateTime eventTime, const QJsonObject
}
/*!
* \brief Reads results of requestEvents().
* \brief Reads results of requestEvents() and requestDirPullErrors().
*/
void SyncthingConnection::readFolderErrors(DateTime eventTime, const QJsonObject &eventData, SyncthingDir &dirInfo, int index)
{
const QJsonArray errors(eventData.value(QLatin1String("errors")).toArray());
if (errors.isEmpty()) {
return;
}
// ignore errors occurred before the last time the directory was in "sync" state (Syncthing re-emits recurring errors)
if (dirInfo.lastSyncStarted > eventTime) {
return;
}
for (const QJsonValue &errorVal : errors) {
// clear previous errors (considering syncthing/lib/model/rwfolder.go it seems that also the event API always returns a
// full list of events and not only new ones)
dirInfo.itemErrors.clear();
// add errors
for (const QJsonValueRef errorVal : eventData.value(QLatin1String("errors")).toArray()) {
const QJsonObject error(errorVal.toObject());
if (error.isEmpty()) {
continue;
}
auto &errors = dirInfo.itemErrors;
SyncthingItemError dirError(error.value(QLatin1String("error")).toString(), error.value(QLatin1String("path")).toString());
if (find(errors.cbegin(), errors.cend(), dirError) != errors.cend()) {
continue;
}
errors.emplace_back(move(dirError));
dirInfo.itemErrors.emplace_back(error.value(QLatin1String("error")).toString(), error.value(QLatin1String("path")).toString());
}
// set pullErrorCount in case it has not already been populated from the FolderSummary event
if (dirInfo.pullErrorCount < dirInfo.itemErrors.size()) {
dirInfo.pullErrorCount = dirInfo.itemErrors.size();
}
// ensure the directory is considered out-of-sync
if (dirInfo.pullErrorCount) {
dirInfo.assignStatus(SyncthingDirStatus::OutOfSync, eventTime);
}
emit dirStatusChanged(dirInfo, index);
}

View File

@ -176,7 +176,7 @@ MockedReply *MockedReply::forRequest(const QString &method, const QString &path,
}
} else if (path == QLatin1String("folder/pullerrors")) {
const QString folder(query.queryItemValue(QStringLiteral("folder")));
if (folder == QLatin1String("GXWxf-3zgnU")) {
if (folder == QLatin1String("GXWxf-3zgnU") && s_eventIndex >= 6) {
buffer = &pullErrors;
}
} else if (path == QLatin1String("system/connections")) {

View File

@ -55,26 +55,15 @@ bool SyncthingDir::checkWhetherStatusUpdateRelevant(DateTime time)
bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, DateTime time)
{
// clear out-of-sync items
switch (newStatus) {
case SyncthingDirStatus::Unknown:
case SyncthingDirStatus::OutOfSync:
break;
default:
if (newStatus == SyncthingDirStatus::Synchronizing || lastSyncStarted.isNull()) {
// errors become obsolete; however errors must be kept as previous errors to be able
// to identify "new errors" as known errors
previousItemErrors.clear();
previousItemErrors.swap(itemErrors);
}
}
// set time of the last "sync" state (used internally and not displayed, hence keep it GMT)
switch (newStatus) {
case SyncthingDirStatus::Synchronizing:
// handle obsoletion of out-of-sync items: no FolderErrors are accepted older than the last "sync" state are accepted
if (newStatus == SyncthingDirStatus::Synchronizing) {
// update time of last "sync" state and obsolete currently assigned errors
lastSyncStarted = time; // used internally and not displayed, hence keep it GMT
itemErrors.clear();
pullErrorCount = 0;
} else if (lastSyncStarted.isNull() && newStatus != SyncthingDirStatus::OutOfSync) {
// prevent adding new errors from "before the first status" if the time of the last "sync" state is unknown
lastSyncStarted = time;
break;
default:;
}
// clear global error if not out-of-sync anymore
@ -86,14 +75,12 @@ bool SyncthingDir::finalizeStatusUpdate(SyncthingDirStatus newStatus, DateTime t
return false;
}
// update last scan time and status
switch (status) {
case SyncthingDirStatus::Scanning:
// 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();
break;
default:;
}
status = newStatus;
return true;
}

View File

@ -13,8 +13,6 @@
namespace Data {
enum class SyncthingDirStatus { Unknown, Idle, Scanning, Synchronizing, OutOfSync };
// note: update "visible: status === 4" in DirectoriesPage.qml (which references OutOfSync by
// its raw value due to limitations of Qt/Qml) when updating this enum
QString LIB_SYNCTHING_CONNECTOR_EXPORT statusString(SyncthingDirStatus status);
@ -153,8 +151,8 @@ struct LIB_SYNCTHING_CONNECTOR_EXPORT SyncthingDir {
double fileSystemWatcherDelay = 0.0;
std::map<QString, SyncthingCompletion> completionByDevice;
QString globalError;
quint64 pullErrorCount = 0;
std::vector<SyncthingItemError> itemErrors;
std::vector<SyncthingItemError> previousItemErrors;
std::vector<SyncthingFileChange> recentChanges;
SyncthingStatistics globalStats, localStats, neededStats;
ChronoUtilities::DateTime lastStatisticsUpdate;

View File

@ -169,12 +169,12 @@ bool SyncthingDirActions::updateStatus(const SyncthingDir &dir)
m_lastScanAction.setText(tr("Last scan time: ") + agoString(dir.lastScanTime));
m_lastScanAction.setIcon(QIcon::fromTheme(QStringLiteral("accept_time_event")));
m_rescanIntervalAction.setText(tr("Rescan interval: %1 seconds").arg(dir.rescanInterval));
if (dir.itemErrors.empty()) {
if (!dir.pullErrorCount) {
m_errorsAction.setVisible(false);
} else {
m_errorsAction.setVisible(true);
m_errorsAction.setIcon(QIcon::fromTheme(QStringLiteral("dialog-error")));
m_errorsAction.setText(tr("%1 item(s) out-of-sync", nullptr, trQuandity(dir.itemErrors.size())).arg(dir.itemErrors.size()));
m_errorsAction.setText(tr("%1 item(s) out-of-sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.pullErrorCount));
}
return true;
}

View File

@ -36,6 +36,7 @@ QHash<int, QByteArray> SyncthingDirectoryModel::roleNames() const
roles[DirectoryPaused] = "paused";
roles[DirectoryId] = "dirId";
roles[DirectoryPath] = "path";
roles[DirectoryPullErrorCount] = "pullErrorCount";
roles[DirectoryDetail] = "detail";
return roles;
}());
@ -167,19 +168,18 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const
case 8:
return dir.lastFileName.isEmpty() ? tr("unknown") : dir.lastFileName;
case 9:
if (!dir.globalError.isEmpty() || !dir.itemErrors.empty()) {
if (dir.itemErrors.empty()) {
return dir.globalError;
}
if (dir.globalError.isEmpty()) {
return tr("%1 item(s) out of sync", nullptr, static_cast<int>(dir.itemErrors.size())).arg(dir.itemErrors.size());
}
return tr("%1 and %2 item(s) out of sync", nullptr, static_cast<int>(dir.itemErrors.size()))
.arg(dir.globalError)
.arg(dir.itemErrors.size());
} else {
if (dir.globalError.isEmpty() && !dir.pullErrorCount) {
return tr("none");
}
if (!dir.pullErrorCount) {
return dir.globalError;
}
if (dir.globalError.isEmpty()) {
return tr("%1 item(s) out of sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.pullErrorCount);
}
return tr("%1 and %2 item(s) out of sync", nullptr, trQuandity(dir.pullErrorCount))
.arg(dir.globalError)
.arg(dir.pullErrorCount);
}
}
break;
@ -202,7 +202,7 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const
return dir.lastFileName.isEmpty() ? Colors::gray(m_brightColors)
: (dir.lastFileDeleted ? Colors::red(m_brightColors) : QVariant());
case 9:
return dir.globalError.isEmpty() && dir.itemErrors.empty() ? Colors::gray(m_brightColors) : Colors::red(m_brightColors);
return dir.globalError.isEmpty() && !dir.pullErrorCount ? Colors::gray(m_brightColors) : Colors::red(m_brightColors);
}
}
break;
@ -315,6 +315,8 @@ QVariant SyncthingDirectoryModel::data(const QModelIndex &index, int role) const
return dir.id;
case DirectoryPath:
return dir.path;
case DirectoryPullErrorCount:
return dir.pullErrorCount;
default:;
}
}
@ -363,7 +365,7 @@ void SyncthingDirectoryModel::dirStatusChanged(const SyncthingDir &, int index)
{
const QModelIndex modelIndex1(this->index(index, 0, QModelIndex()));
static const QVector<int> modelRoles1({ Qt::DisplayRole, Qt::EditRole, Qt::DecorationRole, DirectoryPaused, DirectoryStatus,
DirectoryStatusString, DirectoryStatusColor, DirectoryId, DirectoryPath });
DirectoryStatusString, DirectoryStatusColor, DirectoryId, DirectoryPath, DirectoryPullErrorCount });
emit dataChanged(modelIndex1, modelIndex1, modelRoles1);
const QModelIndex modelIndex2(this->index(index, 1, QModelIndex()));
static const QVector<int> modelRoles2({ Qt::DisplayRole, Qt::EditRole, Qt::ForegroundRole });

View File

@ -21,6 +21,7 @@ public:
DirectoryStatusColor,
DirectoryId,
DirectoryPath,
DirectoryPullErrorCount,
DirectoryDetail,
};

View File

@ -11,10 +11,13 @@ class SyncthingConnection;
class LIB_SYNCTHING_MODEL_EXPORT SyncthingModel : public QAbstractItemModel {
Q_OBJECT
Q_PROPERTY(SyncthingConnection *connection READ connection)
Q_PROPERTY(bool brightColors READ brightColors WRITE setBrightColors)
public:
explicit SyncthingModel(SyncthingConnection &connection, QObject *parent = nullptr);
Data::SyncthingConnection *connection();
const Data::SyncthingConnection *connection() const;
bool brightColors() const;
void setBrightColors(bool brightColors);
@ -25,6 +28,16 @@ protected:
bool m_brightColors;
};
inline SyncthingConnection *SyncthingModel::connection()
{
return &m_connection;
}
inline const SyncthingConnection *SyncthingModel::connection() const
{
return &m_connection;
}
inline bool SyncthingModel::brightColors() const
{
return m_brightColors;

View File

@ -4,6 +4,7 @@
#include "../../connector/syncthingservice.h"
#include "../../connector/utils.h"
#include "../../widgets/misc/direrrorsdialog.h"
#include "../../widgets/misc/internalerrorsdialog.h"
#include "../../widgets/misc/otherdialogs.h"
#include "../../widgets/misc/textviewdialog.h"
@ -313,15 +314,20 @@ void SyncthingApplet::showInternalErrorsDialog()
errorViewDlg->show();
}
void SyncthingApplet::showDirectoryErrors(unsigned int directoryIndex) const
void SyncthingApplet::showDirectoryErrors(unsigned int directoryIndex)
{
const auto &dirs = m_connection.dirInfo();
if (directoryIndex < dirs.size()) {
auto *const dlg = TextViewDialog::forDirectoryErrors(dirs[directoryIndex]);
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
centerWidget(dlg);
dlg->show();
if (directoryIndex >= dirs.size()) {
return;
}
const auto &dir(dirs[directoryIndex]);
m_connection.requestDirPullErrors(dir.id);
auto *const dlg = new DirectoryErrorsDialog(m_connection, dir);
dlg->setAttribute(Qt::WA_DeleteOnClose, true);
centerWidget(dlg);
dlg->show();
}
void SyncthingApplet::copyToClipboard(const QString &text)

View File

@ -104,7 +104,7 @@ public Q_SLOTS:
void showNotificationsDialog();
void dismissNotifications();
void showInternalErrorsDialog();
void showDirectoryErrors(unsigned int directoryIndex) const;
void showDirectoryErrors(unsigned int directoryIndex);
void copyToClipboard(const QString &text);
void updateStatusIconAndTooltip();

View File

@ -73,9 +73,7 @@ ColumnLayout {
id: errorsButton
iconSource: "emblem-important"
tooltip: qsTr("Show errors")
// 4 stands for SyncthingDirStatus::OutOfSync, unfortunately there is currently
// no way to expose this to QML without conflicting SyncthingStatus
visible: status === 4
visible: pullErrorCount > 0
onClicked: {
plasmoid.nativeInterface.showDirectoryErrors(
index)

View File

@ -3,7 +3,7 @@
#include "../../connector/syncthingconnection.h"
#include "../../model/syncthingdirectorymodel.h"
#include "../../widgets/misc/textviewdialog.h"
#include "../../widgets/misc/direrrorsdialog.h"
#include <QClipboard>
#include <QCursor>
@ -31,7 +31,7 @@ void DirView::mouseReleaseEvent(QMouseEvent *event)
QTreeView::mouseReleaseEvent(event);
// get SyncthingDir object
const SyncthingDirectoryModel *dirModel = qobject_cast<const SyncthingDirectoryModel *>(model());
auto *const dirModel = qobject_cast<SyncthingDirectoryModel *>(model());
if (!dirModel) {
return;
}
@ -40,7 +40,7 @@ void DirView::mouseReleaseEvent(QMouseEvent *event)
if (!clickedIndex.isValid() || clickedIndex.column() != 1) {
return;
}
const SyncthingDir *const dir = dirModel->dirInfo(clickedIndex);
const auto *const dir = dirModel->dirInfo(clickedIndex);
if (!dir) {
return;
}
@ -60,8 +60,11 @@ void DirView::mouseReleaseEvent(QMouseEvent *event)
} else {
emit openDir(*dir);
}
} else if (clickedIndex.row() == 9 && !dir->itemErrors.empty()) {
auto *const textViewDlg = TextViewDialog::forDirectoryErrors(*dir);
} else if (clickedIndex.row() == 9 && dir->pullErrorCount) {
auto &connection(*dirModel->connection());
connection.requestDirPullErrors(dir->id);
auto *const textViewDlg = new DirectoryErrorsDialog(connection, *dir);
textViewDlg->setAttribute(Qt::WA_DeleteOnClose);
textViewDlg->show();
}

View File

@ -17,6 +17,7 @@ set(WIDGETS_HEADER_FILES
webview/webviewdialog.h
misc/textviewdialog.h
misc/internalerrorsdialog.h
misc/direrrorsdialog.h
misc/statusinfo.h
misc/dbusstatusnotifier.h
misc/internalerror.h
@ -33,6 +34,7 @@ set(WIDGETS_SRC_FILES
webview/webviewinterceptor.cpp
misc/textviewdialog.cpp
misc/internalerrorsdialog.cpp
misc/direrrorsdialog.cpp
misc/statusinfo.cpp
misc/dbusstatusnotifier.cpp
misc/internalerror.cpp

View File

@ -0,0 +1,141 @@
#include "./direrrorsdialog.h"
#include "../../connector/syncthingconnection.h"
#include "../../connector/syncthingdir.h"
#include "../../connector/utils.h"
#include <QDir>
#include <QHBoxLayout>
#include <QIcon>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#include <QStringBuilder>
#include <QTextBrowser>
#include <QVBoxLayout>
#include <algorithm>
#include <iostream>
#include <limits>
using namespace std;
using namespace ChronoUtilities;
using namespace Data;
namespace QtGui {
DirectoryErrorsDialog::DirectoryErrorsDialog(const Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent)
: TextViewDialog(tr("Errors for directory %1").arg(dir.displayName()), parent)
, m_connection(connection)
, m_dirId(dir.id)
{
// add layout to show status and additional buttons
auto *const buttonLayout = new QHBoxLayout;
buttonLayout->setMargin(0);
layout()->addLayout(buttonLayout);
// add label for overall status
m_statusLabel = new QLabel(this);
QFont boldFont(m_statusLabel->font());
boldFont.setBold(true);
m_statusLabel->setFont(boldFont);
buttonLayout->addWidget(m_statusLabel);
// add a button for removing all non-empty directories
m_rmNonEmptyDirsButton = new QPushButton(this);
m_rmNonEmptyDirsButton->setText(tr("Remove non-empty directories"));
m_rmNonEmptyDirsButton->setIcon(QIcon::fromTheme(QStringLiteral("remove")));
buttonLayout->setMargin(0);
buttonLayout->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
buttonLayout->addWidget(m_rmNonEmptyDirsButton);
// connect signals and slots
connect(&connection, &SyncthingConnection::dirStatusChanged, this, &DirectoryErrorsDialog::handleDirStatusChanged);
connect(&connection, &SyncthingConnection::newDirs, this, &DirectoryErrorsDialog::handleNewDirs);
connect(m_rmNonEmptyDirsButton, &QPushButton::clicked, this, &DirectoryErrorsDialog::removeNonEmptyDirs);
// populate initially available errors
updateErrors(dir);
}
DirectoryErrorsDialog::~DirectoryErrorsDialog()
{
}
void DirectoryErrorsDialog::handleDirStatusChanged(const SyncthingDir &dir)
{
if (dir.id == m_dirId) {
updateErrors(dir);
}
}
void DirectoryErrorsDialog::handleNewDirs()
{
int index;
if (const auto *const dir = m_connection.findDirInfo(m_dirId, index)) {
updateErrors(*dir);
}
}
void DirectoryErrorsDialog::updateErrors(const Data::SyncthingDir &dir)
{
// update status
m_statusLabel->setText(tr("%1 item(s) out-of-sync", nullptr, trQuandity(dir.pullErrorCount)).arg(dir.pullErrorCount));
m_rmNonEmptyDirsButton->setHidden(m_nonEmptyDirs.empty());
// clear previous errors
auto *const textBrowser = browser();
textBrowser->clear();
m_nonEmptyDirs.clear();
// add item errors to textBrowser
for (const SyncthingItemError &error : dir.itemErrors) {
textBrowser->append(error.path % QChar(':') % QChar('\n') % error.message % QChar('\n'));
if (error.message.endsWith(QStringLiteral("directory not empty"))) {
m_nonEmptyDirs << dir.path + error.path;
}
}
}
QString printDirectories(const QString &message, const QStringList &dirs)
{
return QStringLiteral("<p>") % message % QStringLiteral("</p><ul><li>") % dirs.join(QStringLiteral("</li><li>")) % QStringLiteral("</ul>");
}
void DirectoryErrorsDialog::removeNonEmptyDirs()
{
int index;
const auto *const dir = m_connection.findDirInfo(m_dirId, index);
if (!dir) {
return;
}
const QString title(tr("Remove non-empty directories for folder \"%1\"").arg(dir->displayName()));
if (QMessageBox::warning(this, title, printDirectories(tr("Do you really want to remove the following directories:"), m_nonEmptyDirs),
QMessageBox::YesToAll, QMessageBox::NoToAll | QMessageBox::Default | QMessageBox::Escape)
!= QMessageBox::YesToAll) {
return;
}
QStringList removedDirs, failedDirs;
for (const QString &dirPath : m_nonEmptyDirs) {
bool ok = false;
QDir dir(dirPath);
if (!dir.exists() || !dir.removeRecursively()) {
// check whether dir has already been removed by removing its parent
for (const QString &removedDir : removedDirs) {
if (dirPath.startsWith(removedDir)) {
ok = true;
break;
}
}
} else {
ok = true;
}
(ok ? removedDirs : failedDirs) << dirPath;
}
if (!failedDirs.isEmpty()) {
QMessageBox::critical(this, title, printDirectories(tr("Unable to remove the following dirs:"), failedDirs));
}
}
} // namespace QtGui

View File

@ -0,0 +1,38 @@
#ifndef SYNCTHINGWIDGETS_DIRECTORY_ERRORS_DIALOG_H
#define SYNCTHINGWIDGETS_DIRECTORY_ERRORS_DIALOG_H
#include "./textviewdialog.h"
QT_FORWARD_DECLARE_CLASS(QLabel)
QT_FORWARD_DECLARE_CLASS(QPushButton)
namespace Data {
class SyncthingConnection;
struct SyncthingDir;
} // namespace Data
namespace QtGui {
class SYNCTHINGWIDGETS_EXPORT DirectoryErrorsDialog : public TextViewDialog {
Q_OBJECT
public:
explicit DirectoryErrorsDialog(const Data::SyncthingConnection &connection, const Data::SyncthingDir &dir, QWidget *parent = nullptr);
~DirectoryErrorsDialog() override;
private Q_SLOTS:
void handleDirStatusChanged(const Data::SyncthingDir &dir);
void handleNewDirs();
void updateErrors(const Data::SyncthingDir &dir);
void removeNonEmptyDirs();
private:
const Data::SyncthingConnection &m_connection;
QString m_dirId;
QStringList m_nonEmptyDirs;
QLabel *m_statusLabel;
QPushButton *m_rmNonEmptyDirsButton;
};
} // namespace QtGui
#endif // SYNCTHINGWIDGETS_DIRECTORY_ERRORS_DIALOG_H

View File

@ -8,7 +8,6 @@
#include <qtutilities/misc/dialogutils.h>
#include <QDir>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QIcon>
@ -20,10 +19,6 @@
#include <QTextBrowser>
#include <QVBoxLayout>
#include <algorithm>
#include <functional>
#include <limits>
using namespace std;
using namespace std::placeholders;
using namespace Dialogs;
@ -61,84 +56,6 @@ TextViewDialog::TextViewDialog(const QString &title, QWidget *parent)
centerWidget(this);
}
QString printDirectories(const QString &message, const QStringList &dirs)
{
return QStringLiteral("<p>") % message % QStringLiteral("</p><ul><li>") % dirs.join(QStringLiteral("</li><li>")) % QStringLiteral("</ul>");
}
TextViewDialog *TextViewDialog::forDirectoryErrors(const Data::SyncthingDir &dir)
{
// create TextViewDialog
auto *const textViewDlg = new TextViewDialog(tr("Errors of %1").arg(dir.displayName()));
auto *const browser = textViewDlg->browser();
// add errors to text view and find errors about non-empty directories to be removed
QStringList nonEmptyDirs;
for (const SyncthingItemError &error : dir.itemErrors) {
browser->append(error.path % QChar(':') % QChar('\n') % error.message % QChar('\n'));
if (error.message.endsWith(QStringLiteral("directory not empty"))) {
nonEmptyDirs << dir.path + error.path;
}
}
// add layout to show status and additional buttons
auto *const buttonLayout = new QHBoxLayout;
buttonLayout->setMargin(0);
// add label for overall status
auto *const statusLabel = new QLabel(textViewDlg);
statusLabel->setText(tr("%1 item(s) out-of-sync", nullptr, static_cast<int>(min<size_t>(dir.itemErrors.size(), numeric_limits<int>::max())))
.arg(dir.itemErrors.size()));
QFont boldFont(statusLabel->font());
boldFont.setBold(true);
statusLabel->setFont(boldFont);
buttonLayout->addWidget(statusLabel);
// add a button for removing all non-empty directories
if (!nonEmptyDirs.isEmpty()) {
auto *const rmNonEmptyDirsButton = new QPushButton(textViewDlg);
rmNonEmptyDirsButton->setText(tr("Remove non-empty directories"));
rmNonEmptyDirsButton->setIcon(QIcon::fromTheme(QStringLiteral("remove")));
buttonLayout->setMargin(0);
buttonLayout->addItem(new QSpacerItem(40, 20, QSizePolicy::Expanding, QSizePolicy::Minimum));
buttonLayout->addWidget(rmNonEmptyDirsButton);
// define directory removal function
const QString title(tr("Remove non-empty directories for folder \"%1\"").arg(dir.displayName()));
connect(rmNonEmptyDirsButton, &QPushButton::clicked, [textViewDlg, nonEmptyDirs, title] {
if (QMessageBox::warning(textViewDlg, title,
printDirectories(tr("Do you really want to remove the following directories:"), nonEmptyDirs), QMessageBox::YesToAll,
QMessageBox::NoToAll | QMessageBox::Default | QMessageBox::Escape)
== QMessageBox::YesToAll) {
QStringList removedDirs;
QStringList failedDirs;
for (const QString &dirPath : nonEmptyDirs) {
bool ok = false;
QDir dir(dirPath);
if (!dir.exists() || !dir.removeRecursively()) {
// check whether dir has already been removed by removing its parent
for (const QString &removedDir : removedDirs) {
if (dirPath.startsWith(removedDir)) {
ok = true;
break;
}
}
} else {
ok = true;
}
(ok ? removedDirs : failedDirs) << dirPath;
}
if (!failedDirs.isEmpty()) {
QMessageBox::critical(textViewDlg, title, printDirectories(tr("Unable to remove the following dirs:"), failedDirs));
}
}
});
}
textViewDlg->m_layout->addLayout(buttonLayout);
return textViewDlg;
}
TextViewDialog *TextViewDialog::forLogEntries(SyncthingConnection &connection)
{
auto *const dlg = new TextViewDialog(tr("Log"));

View File

@ -22,7 +22,7 @@ public:
TextViewDialog(const QString &title = QString(), QWidget *parent = nullptr);
QTextBrowser *browser();
static TextViewDialog *forDirectoryErrors(const Data::SyncthingDir &dir);
QVBoxLayout *layout();
static TextViewDialog *forLogEntries(Data::SyncthingConnection &connection);
static TextViewDialog *forLogEntries(const std::vector<Data::SyncthingLogEntry> &logEntries, const QString &title = QString());
@ -43,6 +43,12 @@ inline QTextBrowser *TextViewDialog::browser()
{
return m_browser;
}
inline QVBoxLayout *TextViewDialog::layout()
{
return m_layout;
}
} // namespace QtGui
#endif // SYNCTHINGWIDGETS_TEXTVIEWDIALOG_H