Explorar o código

Merge pull request #314 from nextcloud/upstream/pr/6373

Make sure ignored and conflict files show up in the issues tab even with partial local discovery
Roeland Jago Douma %!s(int64=7) %!d(string=hai) anos
pai
achega
1262cbf4ef

+ 0 - 3
src/csync/csync.cpp

@@ -310,9 +310,6 @@ int csync_s::reinitialize() {
   renames.folder_renamed_from.clear();
   renames.folder_renamed_from.clear();
   renames.folder_renamed_to.clear();
   renames.folder_renamed_to.clear();
 
 
-  local_discovery_style = LocalDiscoveryStyle::FilesystemOnly;
-  locally_touched_dirs.clear();
-
   status = CSYNC_STATUS_INIT;
   status = CSYNC_STATUS_INIT;
   SAFE_FREE(error_string);
   SAFE_FREE(error_string);
 
 

+ 1 - 9
src/csync/csync_private.h

@@ -201,15 +201,7 @@ struct OCSYNC_EXPORT csync_s {
    */
    */
   bool read_remote_from_db = false;
   bool read_remote_from_db = false;
 
 
-  LocalDiscoveryStyle local_discovery_style = LocalDiscoveryStyle::FilesystemOnly;
-
-  /**
-   * List of folder-relative directory paths that should be scanned on the
-   * filesystem if the local_discovery_style suggests it.
-   *
-   * Their parents will be scanned too. The paths don't start with a /.
-   */
-  std::set<QByteArray> locally_touched_dirs;
+  std::function<bool(const QByteArray &)> should_discover_locally_fn;
 
 
   bool ignore_hidden_files = true;
   bool ignore_hidden_files = true;
 
 

+ 2 - 13
src/csync/csync_update.cpp

@@ -602,23 +602,12 @@ int csync_ftw(CSYNC *ctx, const char *uri, csync_walker_fn fn,
   bool do_read_from_db = (ctx->current == REMOTE_REPLICA && ctx->remote.read_from_db);
   bool do_read_from_db = (ctx->current == REMOTE_REPLICA && ctx->remote.read_from_db);
   const char *db_uri = uri;
   const char *db_uri = uri;
 
 
-  if (ctx->current == LOCAL_REPLICA
-      && ctx->local_discovery_style == LocalDiscoveryStyle::DatabaseAndFilesystem) {
+  if (ctx->current == LOCAL_REPLICA && ctx->should_discover_locally_fn) {
       const char *local_uri = uri + strlen(ctx->local.uri);
       const char *local_uri = uri + strlen(ctx->local.uri);
       if (*local_uri == '/')
       if (*local_uri == '/')
           ++local_uri;
           ++local_uri;
       db_uri = local_uri;
       db_uri = local_uri;
-      do_read_from_db = true;
-
-      // Minor bug: local_uri doesn't have a trailing /. Example: Assume it's "d/foo"
-      // and we want to check whether we should read from the db. Assume "d/foo a" is
-      // in locally_touched_dirs. Then this check will say no, don't read from the db!
-      // (because "d/foo" < "d/foo a" < "d/foo/bar")
-      // C++14: Could skip the conversion to QByteArray here.
-      auto it = ctx->locally_touched_dirs.lower_bound(QByteArray(local_uri));
-      if (it != ctx->locally_touched_dirs.end() && it->startsWith(local_uri)) {
-          do_read_from_db = false;
-      }
+      do_read_from_db = !ctx->should_discover_locally_fn(QByteArray(local_uri));
   }
   }
 
 
   if (!depth) {
   if (!depth) {

+ 1 - 1
src/csync/vio/csync_vio.cpp

@@ -40,7 +40,7 @@ csync_vio_handle_t *csync_vio_opendir(CSYNC *ctx, const char *name) {
       break;
       break;
     case LOCAL_REPLICA:
     case LOCAL_REPLICA:
 	if( ctx->callbacks.update_callback ) {
 	if( ctx->callbacks.update_callback ) {
-        ctx->callbacks.update_callback(ctx->current, name, ctx->callbacks.update_callback_userdata);
+        ctx->callbacks.update_callback(/*local=*/true, name, ctx->callbacks.update_callback_userdata);
 	}
 	}
       return csync_vio_local_opendir(name);
       return csync_vio_local_opendir(name);
       break;
       break;

+ 14 - 0
src/gui/folder.cpp

@@ -107,6 +107,9 @@ Folder::Folder(const FolderDefinition &definition,
     _scheduleSelfTimer.setInterval(SyncEngine::minimumFileAgeForUpload);
     _scheduleSelfTimer.setInterval(SyncEngine::minimumFileAgeForUpload);
     connect(&_scheduleSelfTimer, &QTimer::timeout,
     connect(&_scheduleSelfTimer, &QTimer::timeout,
         this, &Folder::slotScheduleThisFolder);
         this, &Folder::slotScheduleThisFolder);
+
+    connect(ProgressDispatcher::instance(), &ProgressDispatcher::folderConflicts,
+        this, &Folder::slotFolderConflicts);
 }
 }
 
 
 Folder::~Folder()
 Folder::~Folder()
@@ -964,6 +967,17 @@ void Folder::slotNextSyncFullLocalDiscovery()
     _timeSinceLastFullLocalDiscovery.invalidate();
     _timeSinceLastFullLocalDiscovery.invalidate();
 }
 }
 
 
+void Folder::slotFolderConflicts(const QString &folder, const QStringList &conflictPaths)
+{
+    if (folder != _definition.alias)
+        return;
+    auto &r = _syncResult;
+
+    // If the number of conflicts is too low, adjust it upwards
+    if (conflictPaths.size() > r.numNewConflictItems() + r.numOldConflictItems())
+        r.setNumOldConflictItems(conflictPaths.size() - r.numNewConflictItems());
+}
+
 void Folder::scheduleThisFolderSoon()
 void Folder::scheduleThisFolderSoon()
 {
 {
     if (!_scheduleSelfTimer.isActive()) {
     if (!_scheduleSelfTimer.isActive()) {

+ 7 - 0
src/gui/folder.h

@@ -314,6 +314,13 @@ private slots:
     /** Ensures that the next sync performs a full local discovery. */
     /** Ensures that the next sync performs a full local discovery. */
     void slotNextSyncFullLocalDiscovery();
     void slotNextSyncFullLocalDiscovery();
 
 
+    /** Adjust sync result based on conflict data from IssuesWidget.
+     *
+     * This is pretty awkward, but IssuesWidget just keeps better track
+     * of conflicts across partial local discovery.
+     */
+    void slotFolderConflicts(const QString &folder, const QStringList &conflictPaths);
+
 private:
 private:
     bool reloadExcludes();
     bool reloadExcludes();
 
 

+ 10 - 5
src/gui/folderstatusmodel.cpp

@@ -905,11 +905,16 @@ void FolderStatusModel::slotSetProgress(const ProgressInfo &progress)
           << FolderStatusDelegate::WarningCount
           << FolderStatusDelegate::WarningCount
           << Qt::ToolTipRole;
           << Qt::ToolTipRole;
 
 
-    if (progress.status() == ProgressInfo::Discovery
-        && !progress._currentDiscoveredFolder.isEmpty()) {
-        pi->_overallSyncString = tr("Checking for changes in '%1'").arg(progress._currentDiscoveredFolder);
-        emit dataChanged(index(folderIndex), index(folderIndex), roles);
-        return;
+    if (progress.status() == ProgressInfo::Discovery) {
+        if (!progress._currentDiscoveredRemoteFolder.isEmpty()) {
+            pi->_overallSyncString = tr("Checking for changes in remote '%1'").arg(progress._currentDiscoveredRemoteFolder);
+            emit dataChanged(index(folderIndex), index(folderIndex), roles);
+            return;
+        } else if (!progress._currentDiscoveredLocalFolder.isEmpty()) {
+            pi->_overallSyncString = tr("Checking for changes in local '%1'").arg(progress._currentDiscoveredLocalFolder);
+            emit dataChanged(index(folderIndex), index(folderIndex), roles);
+            return;
+        }
     }
     }
 
 
     if (progress.status() == ProgressInfo::Reconcile) {
     if (progress.status() == ProgressInfo::Reconcile) {

+ 80 - 12
src/gui/issueswidget.cpp

@@ -18,6 +18,7 @@
 #include "issueswidget.h"
 #include "issueswidget.h"
 #include "configfile.h"
 #include "configfile.h"
 #include "syncresult.h"
 #include "syncresult.h"
+#include "syncengine.h"
 #include "logger.h"
 #include "logger.h"
 #include "theme.h"
 #include "theme.h"
 #include "folderman.h"
 #include "folderman.h"
@@ -45,6 +46,11 @@ namespace OCC {
  */
  */
 static const int maxIssueCount = 50000;
 static const int maxIssueCount = 50000;
 
 
+static QPair<QString, QString> pathsWithIssuesKey(const ProtocolItem::ExtraData &data)
+{
+    return qMakePair(data.folderName, data.path);
+}
+
 IssuesWidget::IssuesWidget(QWidget *parent)
 IssuesWidget::IssuesWidget(QWidget *parent)
     : QWidget(parent)
     : QWidget(parent)
     , _ui(new Ui::IssuesWidget)
     , _ui(new Ui::IssuesWidget)
@@ -145,7 +151,14 @@ void IssuesWidget::hideEvent(QHideEvent *ev)
     QWidget::hideEvent(ev);
     QWidget::hideEvent(ev);
 }
 }
 
 
-void IssuesWidget::cleanItems(const QString &folder)
+static bool persistsUntilLocalDiscovery(QTreeWidgetItem *item)
+{
+    const auto data = ProtocolItem::extraData(item);
+    return data.status == SyncFileItem::Conflict
+        || (data.status == SyncFileItem::FileIgnored && data.direction == SyncFileItem::Up);
+}
+
+void IssuesWidget::cleanItems(const std::function<bool(QTreeWidgetItem *)> &shouldDelete)
 {
 {
     _ui->_treeWidget->setSortingEnabled(false);
     _ui->_treeWidget->setSortingEnabled(false);
 
 
@@ -154,8 +167,8 @@ void IssuesWidget::cleanItems(const QString &folder)
     int itemCnt = _ui->_treeWidget->topLevelItemCount();
     int itemCnt = _ui->_treeWidget->topLevelItemCount();
     for (int cnt = itemCnt - 1; cnt >= 0; cnt--) {
     for (int cnt = itemCnt - 1; cnt >= 0; cnt--) {
         QTreeWidgetItem *item = _ui->_treeWidget->topLevelItem(cnt);
         QTreeWidgetItem *item = _ui->_treeWidget->topLevelItem(cnt);
-        QString itemFolder = ProtocolItem::folderName(item);
-        if (itemFolder == folder) {
+        if (shouldDelete(item)) {
+            _pathsWithIssues.remove(pathsWithIssuesKey(ProtocolItem::extraData(item)));
             delete item;
             delete item;
         }
         }
     }
     }
@@ -190,7 +203,21 @@ void IssuesWidget::addItem(QTreeWidgetItem *item)
         }
         }
     }
     }
 
 
+    // Wipe any existing message for the same folder and path
+    auto newData = ProtocolItem::extraData(item);
+    if (_pathsWithIssues.contains(pathsWithIssuesKey(newData))) {
+        for (int i = 0; i < count; ++i) {
+            auto otherItem = _ui->_treeWidget->topLevelItem(i);
+            auto otherData = ProtocolItem::extraData(otherItem);
+            if (otherData.path == newData.path && otherData.folderName == newData.folderName) {
+                delete otherItem;
+                break;
+            }
+        }
+    }
+
     _ui->_treeWidget->insertTopLevelItem(insertLoc, item);
     _ui->_treeWidget->insertTopLevelItem(insertLoc, item);
+    _pathsWithIssues.insert(pathsWithIssuesKey(newData));
     item->setHidden(!shouldBeVisible(item, currentAccountFilter(), currentFolderFilter()));
     item->setHidden(!shouldBeVisible(item, currentAccountFilter(), currentFolderFilter()));
     emit issueCountUpdated(_ui->_treeWidget->topLevelItemCount());
     emit issueCountUpdated(_ui->_treeWidget->topLevelItemCount());
 }
 }
@@ -209,9 +236,47 @@ void IssuesWidget::slotOpenFile(QTreeWidgetItem *item, int)
 
 
 void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
 void IssuesWidget::slotProgressInfo(const QString &folder, const ProgressInfo &progress)
 {
 {
-    if (progress.status() == ProgressInfo::Starting) {
-        // The sync is restarting, clean the old items
-        cleanItems(folder);
+    if (progress.status() == ProgressInfo::Reconcile) {
+        // Wipe all non-persistent entries - as well as the persistent ones
+        // in cases where a local discovery was done.
+        auto f = FolderMan::instance()->folder(folder);
+        if (!f)
+            return;
+        const auto &engine = f->syncEngine();
+        const auto style = engine.lastLocalDiscoveryStyle();
+        cleanItems([&](QTreeWidgetItem *item) {
+            if (ProtocolItem::extraData(item).folderName != folder)
+                return false;
+            if (style == LocalDiscoveryStyle::FilesystemOnly)
+                return true;
+            if (!persistsUntilLocalDiscovery(item))
+                return true;
+
+            // Definitely wipe the entry if the file no longer exists
+            if (!QFileInfo(f->path() + ProtocolItem::extraData(item).path).exists())
+                return true;
+
+            auto path = QFileInfo(ProtocolItem::extraData(item).path).dir().path().toUtf8();
+            if (path == ".")
+                path.clear();
+
+            return engine.shouldDiscoverLocally(path);
+        });
+    }
+    if (progress.status() == ProgressInfo::Done) {
+        // We keep track very well of pending conflicts.
+        // Inform other components about them.
+        QStringList conflicts;
+        auto tree = _ui->_treeWidget;
+        for (int i = 0; i < tree->topLevelItemCount(); ++i) {
+            auto item = tree->topLevelItem(i);
+            auto data = ProtocolItem::extraData(item);
+            if (data.folderName == folder
+                && data.status == SyncFileItem::Conflict) {
+                conflicts.append(data.path);
+            }
+        }
+        emit ProgressDispatcher::instance()->folderConflicts(folder, conflicts);
     }
     }
 }
 }
 
 
@@ -285,13 +350,14 @@ bool IssuesWidget::shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAc
     const QString &filterFolderAlias) const
     const QString &filterFolderAlias) const
 {
 {
     bool visible = true;
     bool visible = true;
-    auto status = ProtocolItem::status(item);
+    auto data = ProtocolItem::extraData(item);
+    auto status = data.status;
     visible &= (_ui->showIgnores->isChecked() || status != SyncFileItem::FileIgnored);
     visible &= (_ui->showIgnores->isChecked() || status != SyncFileItem::FileIgnored);
     visible &= (_ui->showWarnings->isChecked()
     visible &= (_ui->showWarnings->isChecked()
         || (status != SyncFileItem::SoftError
         || (status != SyncFileItem::SoftError
                && status != SyncFileItem::Restoration));
                && status != SyncFileItem::Restoration));
 
 
-    auto folderalias = ProtocolItem::folderName(item);
+    const auto &folderalias = data.folderName;
     if (filterAccount) {
     if (filterAccount) {
         auto folder = FolderMan::instance()->folder(folderalias);
         auto folder = FolderMan::instance()->folder(folderalias);
         visible &= folder && folder->accountState() == filterAccount;
         visible &= folder && folder->accountState() == filterAccount;
@@ -415,12 +481,14 @@ void IssuesWidget::addError(const QString &folderAlias, const QString &message,
 
 
     QTreeWidgetItem *twitem = new ProtocolItem(columns);
     QTreeWidgetItem *twitem = new ProtocolItem(columns);
     twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
     twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
-    twitem->setData(0, Qt::UserRole, timestamp);
     twitem->setIcon(0, icon);
     twitem->setIcon(0, icon);
     twitem->setToolTip(0, longTimeStr);
     twitem->setToolTip(0, longTimeStr);
-    twitem->setData(2, Qt::UserRole, folderAlias);
     twitem->setToolTip(3, message);
     twitem->setToolTip(3, message);
-    twitem->setData(3, Qt::UserRole, SyncFileItem::NormalError);
+    ProtocolItem::ExtraData data;
+    data.timestamp = timestamp;
+    data.folderName = folderAlias;
+    data.status = SyncFileItem::NormalError;
+    ProtocolItem::setExtraData(twitem, data);
 
 
     addItem(twitem);
     addItem(twitem);
     addErrorWidget(twitem, message, category);
     addErrorWidget(twitem, message, category);
@@ -440,7 +508,7 @@ void IssuesWidget::addErrorWidget(QTreeWidgetItem *item, const QString &message,
 
 
         auto button = new QPushButton("Retry all uploads", widget);
         auto button = new QPushButton("Retry all uploads", widget);
         button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
         button->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Expanding);
-        auto folderAlias = ProtocolItem::folderName(item);
+        auto folderAlias = ProtocolItem::extraData(item).folderName;
         connect(button, &QPushButton::clicked,
         connect(button, &QPushButton::clicked,
             this, [this, folderAlias]() { retryInsufficentRemoteStorageErrors(folderAlias); });
             this, [this, folderAlias]() { retryInsufficentRemoteStorageErrors(folderAlias); });
         layout->addWidget(button);
         layout->addWidget(button);

+ 5 - 1
src/gui/issueswidget.h

@@ -63,6 +63,7 @@ protected:
 signals:
 signals:
     void copyToClipboard();
     void copyToClipboard();
     void issueCountUpdated(int);
     void issueCountUpdated(int);
+    void folderConflicts(QString folder, QStringList conflictPaths);
 
 
 private slots:
 private slots:
     void slotRefreshIssues();
     void slotRefreshIssues();
@@ -77,7 +78,7 @@ private:
     QString currentFolderFilter() const;
     QString currentFolderFilter() const;
     bool shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAccount,
     bool shouldBeVisible(QTreeWidgetItem *item, AccountState *filterAccount,
         const QString &filterFolderAlias) const;
         const QString &filterFolderAlias) const;
-    void cleanItems(const QString &folder);
+    void cleanItems(const std::function<bool(QTreeWidgetItem *)> &shouldDelete);
     void addItem(QTreeWidgetItem *item);
     void addItem(QTreeWidgetItem *item);
 
 
     /// Add the special error widget for the category, if any
     /// Add the special error widget for the category, if any
@@ -89,6 +90,9 @@ private:
     /// Each insert disables sorting, this timer reenables it
     /// Each insert disables sorting, this timer reenables it
     QTimer _reenableSorting;
     QTimer _reenableSorting;
 
 
+    /// Optimization: keep track of all folder/paths pairs that have an associated issue
+    QSet<QPair<QString, QString>> _pathsWithIssues;
+
     Ui::IssuesWidget *_ui;
     Ui::IssuesWidget *_ui;
 };
 };
 }
 }

+ 6 - 3
src/gui/owncloudgui.cpp

@@ -869,9 +869,12 @@ void ownCloudGui::slotUpdateProgress(const QString &folder, const ProgressInfo &
     Q_UNUSED(folder);
     Q_UNUSED(folder);
 
 
     if (progress.status() == ProgressInfo::Discovery) {
     if (progress.status() == ProgressInfo::Discovery) {
-        if (!progress._currentDiscoveredFolder.isEmpty()) {
-            _actionStatus->setText(tr("Checking for changes in '%1'")
-                                       .arg(progress._currentDiscoveredFolder));
+        if (!progress._currentDiscoveredRemoteFolder.isEmpty()) {
+            _actionStatus->setText(tr("Checking for changes in remote '%1'")
+                                       .arg(progress._currentDiscoveredRemoteFolder));
+        } else if (!progress._currentDiscoveredLocalFolder.isEmpty()) {
+            _actionStatus->setText(tr("Checking for changes in local '%1'")
+                                       .arg(progress._currentDiscoveredLocalFolder));
         }
         }
     } else if (progress.status() == ProgressInfo::Done) {
     } else if (progress.status() == ProgressInfo::Done) {
         QTimer::singleShot(2000, this, &ownCloudGui::slotComputeOverallSyncStatus);
         QTimer::singleShot(2000, this, &ownCloudGui::slotComputeOverallSyncStatus);

+ 20 - 54
src/gui/protocolwidget.cpp

@@ -32,6 +32,8 @@
 
 
 #include <climits>
 #include <climits>
 
 
+Q_DECLARE_METATYPE(OCC::ProtocolItem::ExtraData)
+
 namespace OCC {
 namespace OCC {
 
 
 QString ProtocolItem::timeString(QDateTime dt, QLocale::FormatType format)
 QString ProtocolItem::timeString(QDateTime dt, QLocale::FormatType format)
@@ -43,54 +45,14 @@ QString ProtocolItem::timeString(QDateTime dt, QLocale::FormatType format)
     return loc.toString(dt, dtFormat);
     return loc.toString(dt, dtFormat);
 }
 }
 
 
-QString ProtocolItem::folderName(const QTreeWidgetItem *item)
-{
-    return item->data(2, Qt::UserRole).toString();
-}
-
-void ProtocolItem::setFolderName(QTreeWidgetItem *item, const QString &folderName)
-{
-    item->setData(2, Qt::UserRole, folderName);
-}
-
-QString ProtocolItem::filePath(const QTreeWidgetItem *item)
-{
-    return item->toolTip(1);
-}
-
-void ProtocolItem::setFilePath(QTreeWidgetItem *item, const QString &filePath)
-{
-    item->setToolTip(1, filePath);
-}
-
-QDateTime ProtocolItem::timestamp(const QTreeWidgetItem *item)
-{
-    return item->data(0, Qt::UserRole).toDateTime();
-}
-
-void ProtocolItem::setTimestamp(QTreeWidgetItem *item, const QDateTime &timestamp)
-{
-    item->setData(0, Qt::UserRole, timestamp);
-}
-
-SyncFileItem::Status ProtocolItem::status(const QTreeWidgetItem *item)
-{
-    return static_cast<SyncFileItem::Status>(item->data(3, Qt::UserRole).toInt());
-}
-
-void ProtocolItem::setStatus(QTreeWidgetItem *item, SyncFileItem::Status status)
-{
-    item->setData(3, Qt::UserRole, status);
-}
-
-quint64 ProtocolItem::size(const QTreeWidgetItem *item)
+ProtocolItem::ExtraData ProtocolItem::extraData(const QTreeWidgetItem *item)
 {
 {
-    return item->data(4, Qt::UserRole).toULongLong();
+    return item->data(0, Qt::UserRole).value<ExtraData>();
 }
 }
 
 
-void ProtocolItem::setSize(QTreeWidgetItem *item, quint64 size)
+void ProtocolItem::setExtraData(QTreeWidgetItem *item, const ExtraData &data)
 {
 {
-    item->setData(4, Qt::UserRole, size);
+    item->setData(0, Qt::UserRole, QVariant::fromValue(data));
 }
 }
 
 
 ProtocolItem *ProtocolItem::create(const QString &folder, const SyncFileItem &item)
 ProtocolItem *ProtocolItem::create(const QString &folder, const SyncFileItem &item)
@@ -136,12 +98,16 @@ ProtocolItem *ProtocolItem::create(const QString &folder, const SyncFileItem &it
     twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
     twitem->setData(0, Qt::SizeHintRole, QSize(0, ActivityItemDelegate::rowHeight()));
     twitem->setIcon(0, icon);
     twitem->setIcon(0, icon);
     twitem->setToolTip(0, longTimeStr);
     twitem->setToolTip(0, longTimeStr);
+    twitem->setToolTip(1, item._file);
     twitem->setToolTip(3, message);
     twitem->setToolTip(3, message);
-    setTimestamp(twitem, timestamp);
-    setFilePath(twitem, item._file); // also sets toolTip(1)
-    setFolderName(twitem, folder);
-    setStatus(twitem, item._status);
-    setSize(twitem, item._size);
+    ProtocolItem::ExtraData data;
+    data.timestamp = timestamp;
+    data.path = item._file;
+    data.folderName = folder;
+    data.status = item._status;
+    data.size = item._size;
+    data.direction = item._direction;
+    ProtocolItem::setExtraData(twitem, data);
     return twitem;
     return twitem;
 }
 }
 
 
@@ -151,13 +117,13 @@ SyncJournalFileRecord ProtocolItem::syncJournalRecord(QTreeWidgetItem *item)
     auto f = folder(item);
     auto f = folder(item);
     if (!f)
     if (!f)
         return rec;
         return rec;
-    f->journalDb()->getFileRecord(filePath(item), &rec);
+    f->journalDb()->getFileRecord(extraData(item).path, &rec);
     return rec;
     return rec;
 }
 }
 
 
 Folder *ProtocolItem::folder(QTreeWidgetItem *item)
 Folder *ProtocolItem::folder(QTreeWidgetItem *item)
 {
 {
-    return FolderMan::instance()->folder(folderName(item));
+    return FolderMan::instance()->folder(extraData(item).folderName);
 }
 }
 
 
 void ProtocolItem::openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent)
 void ProtocolItem::openContextMenu(QPoint globalPos, QTreeWidgetItem *item, QWidget *parent)
@@ -199,10 +165,10 @@ bool ProtocolItem::operator<(const QTreeWidgetItem &other) const
     if (column == 0) {
     if (column == 0) {
         // Items with empty "File" column are larger than others,
         // Items with empty "File" column are larger than others,
         // otherwise sort by time (this uses lexicographic ordering)
         // otherwise sort by time (this uses lexicographic ordering)
-        return std::forward_as_tuple(text(1).isEmpty(), timestamp(this))
-            < std::forward_as_tuple(other.text(1).isEmpty(), timestamp(&other));
+        return std::forward_as_tuple(text(1).isEmpty(), extraData(this).timestamp)
+            < std::forward_as_tuple(other.text(1).isEmpty(), extraData(&other).timestamp);
     } else if (column == 4) {
     } else if (column == 4) {
-        return size(this) < size(&other);
+        return extraData(this).size < extraData(&other).size;
     }
     }
 
 
     return QTreeWidgetItem::operator<(other);
     return QTreeWidgetItem::operator<(other);

+ 18 - 11
src/gui/protocolwidget.h

@@ -49,17 +49,24 @@ public:
     static ProtocolItem *create(const QString &folder, const SyncFileItem &item);
     static ProtocolItem *create(const QString &folder, const SyncFileItem &item);
     static QString timeString(QDateTime dt, QLocale::FormatType format = QLocale::NarrowFormat);
     static QString timeString(QDateTime dt, QLocale::FormatType format = QLocale::NarrowFormat);
 
 
-    // accessors for extra data stored in the item
-    static QString folderName(const QTreeWidgetItem *item);
-    static void setFolderName(QTreeWidgetItem *item, const QString &folderName);
-    static QString filePath(const QTreeWidgetItem *item);
-    static void setFilePath(QTreeWidgetItem *item, const QString &filePath);
-    static QDateTime timestamp(const QTreeWidgetItem *item);
-    static void setTimestamp(QTreeWidgetItem *item, const QDateTime &timestamp);
-    static SyncFileItem::Status status(const QTreeWidgetItem *item);
-    static void setStatus(QTreeWidgetItem *item, SyncFileItem::Status status);
-    static quint64 size(const QTreeWidgetItem *item);
-    static void setSize(QTreeWidgetItem *item, quint64 size);
+    struct ExtraData
+    {
+        ExtraData()
+            : status(SyncFileItem::NoStatus)
+            , direction(SyncFileItem::None)
+        {
+        }
+
+        QString path;
+        QString folderName;
+        QDateTime timestamp;
+        quint64 size = 0;
+        SyncFileItem::Status status BITFIELD(4);
+        SyncFileItem::Direction direction BITFIELD(3);
+    };
+
+    static ExtraData extraData(const QTreeWidgetItem *item);
+    static void setExtraData(QTreeWidgetItem *item, const ExtraData &data);
 
 
     static SyncJournalFileRecord syncJournalRecord(QTreeWidgetItem *item);
     static SyncJournalFileRecord syncJournalRecord(QTreeWidgetItem *item);
     static Folder *folder(QTreeWidgetItem *item);
     static Folder *folder(QTreeWidgetItem *item);

+ 2 - 1
src/gui/syncrunfilelog.cpp

@@ -162,7 +162,8 @@ void SyncRunFileLog::start(const QString &folderPath)
 void SyncRunFileLog::logItem(const SyncFileItem &item)
 void SyncRunFileLog::logItem(const SyncFileItem &item)
 {
 {
     // don't log the directory items that are in the list
     // don't log the directory items that are in the list
-    if (item._direction == SyncFileItem::None) {
+    if (item._direction == SyncFileItem::None
+        || item._instruction == CSYNC_INSTRUCTION_IGNORE) {
         return;
         return;
     }
     }
     QString ts = QString::fromLatin1(item._responseTimeStamp);
     QString ts = QString::fromLatin1(item._responseTimeStamp);

+ 1 - 2
src/libsync/discoveryphase.cpp

@@ -470,8 +470,7 @@ void DiscoveryMainThread::doOpendirSlot(const QString &subPath, DiscoveryDirecto
         fullPath.chop(1);
         fullPath.chop(1);
     }
     }
 
 
-    // emit _discoveryJob->folderDiscovered(false, subPath);
-    _discoveryJob->update_job_update_callback(false, subPath.toUtf8(), _discoveryJob);
+    _discoveryJob->update_job_update_callback(/*local=*/false, subPath.toUtf8(), _discoveryJob);
 
 
     // Result gets written in there
     // Result gets written in there
     _currentDiscoveryDirectoryResult = r;
     _currentDiscoveryDirectoryResult = r;

+ 2 - 1
src/libsync/progressdispatcher.cpp

@@ -139,7 +139,8 @@ void ProgressInfo::reset()
     _status = Starting;
     _status = Starting;
 
 
     _currentItems.clear();
     _currentItems.clear();
-    _currentDiscoveredFolder.clear();
+    _currentDiscoveredRemoteFolder.clear();
+    _currentDiscoveredLocalFolder.clear();
     _sizeProgress = Progress();
     _sizeProgress = Progress();
     _fileProgress = Progress();
     _fileProgress = Progress();
     _totalSizeOfCompletedJobs = 0;
     _totalSizeOfCompletedJobs = 0;

+ 7 - 1
src/libsync/progressdispatcher.h

@@ -181,7 +181,8 @@ public:
     SyncFileItem _lastCompletedItem;
     SyncFileItem _lastCompletedItem;
 
 
     // Used during local and remote update phase
     // Used during local and remote update phase
-    QString _currentDiscoveredFolder;
+    QString _currentDiscoveredRemoteFolder;
+    QString _currentDiscoveredLocalFolder;
 
 
     void setProgressComplete(const SyncFileItem &item);
     void setProgressComplete(const SyncFileItem &item);
 
 
@@ -295,6 +296,11 @@ signals:
      */
      */
     void syncError(const QString &folder, const QString &message, ErrorCategory category);
     void syncError(const QString &folder, const QString &message, ErrorCategory category);
 
 
+    /**
+     * @brief Emitted for a folder when a sync is done, listing all pending conflicts
+     */
+    void folderConflicts(const QString &folder, const QStringList &conflictPaths);
+
 protected:
 protected:
     void setProgressInfo(const QString &folder, const ProgressInfo &progress);
     void setProgressInfo(const QString &folder, const ProgressInfo &progress);
 
 

+ 72 - 11
src/libsync/syncengine.cpp

@@ -272,13 +272,29 @@ bool SyncEngine::checkErrorBlacklisting(SyncFileItem &item)
     return true;
     return true;
 }
 }
 
 
+static bool isFileTransferInstruction(csync_instructions_e instruction)
+{
+    return instruction == CSYNC_INSTRUCTION_CONFLICT
+        || instruction == CSYNC_INSTRUCTION_NEW
+        || instruction == CSYNC_INSTRUCTION_SYNC
+        || instruction == CSYNC_INSTRUCTION_TYPE_CHANGE;
+}
+
+static bool isFileModifyingInstruction(csync_instructions_e instruction)
+{
+    return isFileTransferInstruction(instruction)
+        || instruction == CSYNC_INSTRUCTION_RENAME
+        || instruction == CSYNC_INSTRUCTION_REMOVE;
+}
+
 void SyncEngine::deleteStaleDownloadInfos(const SyncFileItemVector &syncItems)
 void SyncEngine::deleteStaleDownloadInfos(const SyncFileItemVector &syncItems)
 {
 {
     // Find all downloadinfo paths that we want to preserve.
     // Find all downloadinfo paths that we want to preserve.
     QSet<QString> download_file_paths;
     QSet<QString> download_file_paths;
     foreach (const SyncFileItemPtr &it, syncItems) {
     foreach (const SyncFileItemPtr &it, syncItems) {
         if (it->_direction == SyncFileItem::Down
         if (it->_direction == SyncFileItem::Down
-            && it->_type == ItemTypeFile) {
+            && it->_type == ItemTypeFile
+            && isFileTransferInstruction(it->_instruction)) {
             download_file_paths.insert(it->_file);
             download_file_paths.insert(it->_file);
         }
         }
     }
     }
@@ -299,7 +315,8 @@ void SyncEngine::deleteStaleUploadInfos(const SyncFileItemVector &syncItems)
     QSet<QString> upload_file_paths;
     QSet<QString> upload_file_paths;
     foreach (const SyncFileItemPtr &it, syncItems) {
     foreach (const SyncFileItemPtr &it, syncItems) {
         if (it->_direction == SyncFileItem::Up
         if (it->_direction == SyncFileItem::Up
-            && it->_type == ItemTypeFile) {
+            && it->_type == ItemTypeFile
+            && isFileTransferInstruction(it->_instruction)) {
             upload_file_paths.insert(it->_file);
             upload_file_paths.insert(it->_file);
         }
         }
     }
     }
@@ -676,7 +693,6 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
         dir = !remote ? SyncFileItem::Down : SyncFileItem::Up;
         dir = !remote ? SyncFileItem::Down : SyncFileItem::Up;
         break;
         break;
     case CSYNC_INSTRUCTION_CONFLICT:
     case CSYNC_INSTRUCTION_CONFLICT:
-    case CSYNC_INSTRUCTION_IGNORE:
     case CSYNC_INSTRUCTION_ERROR:
     case CSYNC_INSTRUCTION_ERROR:
         dir = SyncFileItem::None;
         dir = SyncFileItem::None;
         break;
         break;
@@ -704,6 +720,7 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
     case CSYNC_INSTRUCTION_NEW:
     case CSYNC_INSTRUCTION_NEW:
     case CSYNC_INSTRUCTION_EVAL:
     case CSYNC_INSTRUCTION_EVAL:
     case CSYNC_INSTRUCTION_STAT_ERROR:
     case CSYNC_INSTRUCTION_STAT_ERROR:
+    case CSYNC_INSTRUCTION_IGNORE:
     default:
     default:
         dir = remote ? SyncFileItem::Down : SyncFileItem::Up;
         dir = remote ? SyncFileItem::Down : SyncFileItem::Up;
         break;
         break;
@@ -856,7 +873,11 @@ void SyncEngine::startSync()
     _excludedFiles->setExcludeConflictFiles(!_account->capabilities().uploadConflictFiles());
     _excludedFiles->setExcludeConflictFiles(!_account->capabilities().uploadConflictFiles());
 
 
     _csync_ctx->read_remote_from_db = true;
     _csync_ctx->read_remote_from_db = true;
-    _lastLocalDiscoveryStyle = _csync_ctx->local_discovery_style;
+
+    _lastLocalDiscoveryStyle = _localDiscoveryStyle;
+    _csync_ctx->should_discover_locally_fn = [this](const QByteArray &path) {
+        return shouldDiscoverLocally(path);
+    };
 
 
     bool ok;
     bool ok;
     auto selectiveSyncBlackList = _journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
     auto selectiveSyncBlackList = _journal->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
@@ -929,9 +950,18 @@ void SyncEngine::startSync()
     QMetaObject::invokeMethod(discoveryJob, "start", Qt::QueuedConnection);
     QMetaObject::invokeMethod(discoveryJob, "start", Qt::QueuedConnection);
 }
 }
 
 
-void SyncEngine::slotFolderDiscovered(bool /*local*/, const QString &folder)
+void SyncEngine::slotFolderDiscovered(bool local, const QString &folder)
 {
 {
-    _progressInfo->_currentDiscoveredFolder = folder;
+    // Currently remote and local discovery never run in parallel
+    // Note: Currently this slot is only called occasionally! See the throttling
+    //       in DiscoveryJob::update_job_update_callback.
+    if (local) {
+        _progressInfo->_currentDiscoveredLocalFolder = folder;
+        _progressInfo->_currentDiscoveredRemoteFolder.clear();
+    } else {
+        _progressInfo->_currentDiscoveredRemoteFolder = folder;
+        _progressInfo->_currentDiscoveredLocalFolder.clear();
+    }
     emit transmissionProgress(*_progressInfo);
     emit transmissionProgress(*_progressInfo);
 }
 }
 
 
@@ -968,7 +998,8 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
         _journal->commitIfNeededAndStartNewTransaction("Post discovery");
         _journal->commitIfNeededAndStartNewTransaction("Post discovery");
     }
     }
 
 
-    _progressInfo->_currentDiscoveredFolder.clear();
+    _progressInfo->_currentDiscoveredRemoteFolder.clear();
+    _progressInfo->_currentDiscoveredLocalFolder.clear();
     _progressInfo->_status = ProgressInfo::Reconcile;
     _progressInfo->_status = ProgressInfo::Reconcile;
     emit transmissionProgress(*_progressInfo);
     emit transmissionProgress(*_progressInfo);
 
 
@@ -1022,7 +1053,9 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
     if (!invalidFilenamePattern.isEmpty()) {
     if (!invalidFilenamePattern.isEmpty()) {
         const QRegExp invalidFilenameRx(invalidFilenamePattern);
         const QRegExp invalidFilenameRx(invalidFilenamePattern);
         for (auto it = syncItems.begin(); it != syncItems.end(); ++it) {
         for (auto it = syncItems.begin(); it != syncItems.end(); ++it) {
-            if ((*it)->_direction == SyncFileItem::Up && (*it)->destination().contains(invalidFilenameRx)) {
+            if ((*it)->_direction == SyncFileItem::Up
+                && isFileModifyingInstruction((*it)->_instruction)
+                && (*it)->destination().contains(invalidFilenameRx)) {
                 (*it)->_errorString = tr("File name contains at least one invalid character");
                 (*it)->_errorString = tr("File name contains at least one invalid character");
                 (*it)->_instruction = CSYNC_INSTRUCTION_IGNORE;
                 (*it)->_instruction = CSYNC_INSTRUCTION_IGNORE;
             }
             }
@@ -1070,6 +1103,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
 
 
     // Re-init the csync context to free memory
     // Re-init the csync context to free memory
     _csync_ctx->reinitialize();
     _csync_ctx->reinitialize();
+    _localDiscoveryPaths.clear();
 
 
     // To announce the beginning of the sync
     // To announce the beginning of the sync
     emit aboutToPropagate(syncItems);
     emit aboutToPropagate(syncItems);
@@ -1211,6 +1245,8 @@ void SyncEngine::finalize(bool success)
     _temporarilyUnavailablePaths.clear();
     _temporarilyUnavailablePaths.clear();
     _renamedFolders.clear();
     _renamedFolders.clear();
     _uniqueErrors.clear();
     _uniqueErrors.clear();
+    _localDiscoveryPaths.clear();
+    _localDiscoveryStyle = LocalDiscoveryStyle::FilesystemOnly;
 
 
     _clearTouchedFilesTimer.start();
     _clearTouchedFilesTimer.start();
 }
 }
@@ -1248,7 +1284,8 @@ void SyncEngine::checkForPermission(SyncFileItemVector &syncItems)
     SyncFileItemPtr needle;
     SyncFileItemPtr needle;
 
 
     for (SyncFileItemVector::iterator it = syncItems.begin(); it != syncItems.end(); ++it) {
     for (SyncFileItemVector::iterator it = syncItems.begin(); it != syncItems.end(); ++it) {
-        if ((*it)->_direction != SyncFileItem::Up) {
+        if ((*it)->_direction != SyncFileItem::Up
+            || !isFileModifyingInstruction((*it)->_instruction)) {
             // Currently we only check server-side permissions
             // Currently we only check server-side permissions
             continue;
             continue;
         }
         }
@@ -1617,8 +1654,32 @@ AccountPtr SyncEngine::account() const
 
 
 void SyncEngine::setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs)
 void SyncEngine::setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs)
 {
 {
-    _csync_ctx->local_discovery_style = style;
-    _csync_ctx->locally_touched_dirs = std::move(dirs);
+    _localDiscoveryStyle = style;
+    _localDiscoveryPaths = std::move(dirs);
+}
+
+bool SyncEngine::shouldDiscoverLocally(const QByteArray &path) const
+{
+    if (_localDiscoveryStyle == LocalDiscoveryStyle::FilesystemOnly)
+        return true;
+
+    auto it = _localDiscoveryPaths.lower_bound(path);
+    if (it == _localDiscoveryPaths.end() || !it->startsWith(path))
+        return false;
+
+    // maybe an exact match or an empty path?
+    if (it->size() == path.size() || path.isEmpty())
+        return true;
+
+    // check for a prefix + / match
+    forever {
+        if (it->size() > path.size() && it->at(path.size()) == '/')
+            return true;
+        ++it;
+        if (it == _localDiscoveryPaths.end() || !it->startsWith(path))
+            return false;
+    }
+    return false;
 }
 }
 
 
 void SyncEngine::abort()
 void SyncEngine::abort()

+ 13 - 2
src/libsync/syncengine.h

@@ -103,7 +103,7 @@ public:
     /**
     /**
      * Control whether local discovery should read from filesystem or db.
      * Control whether local discovery should read from filesystem or db.
      *
      *
-     * If style is Partial, the paths is a set of file paths relative to
+     * If style is DatabaseAndFilesystem, dirs a set of file paths relative to
      * the synced folder. All the parent directories of these paths will not
      * the synced folder. All the parent directories of these paths will not
      * be read from the db and scanned on the filesystem.
      * be read from the db and scanned on the filesystem.
      *
      *
@@ -113,6 +113,15 @@ public:
      */
      */
     void setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs = {});
     void setLocalDiscoveryOptions(LocalDiscoveryStyle style, std::set<QByteArray> dirs = {});
 
 
+    /**
+     * Returns whether the given folder-relative path should be locally discovered
+     * given the local discovery options.
+     *
+     * Example: If path is 'foo/bar' and style is DatabaseAndFilesystem and dirs contains
+     *     'foo/bar/touched_file', then the result will be true.
+     */
+    bool shouldDiscoverLocally(const QByteArray &path) const;
+
     /** Access the last sync run's local discovery style */
     /** Access the last sync run's local discovery style */
     LocalDiscoveryStyle lastLocalDiscoveryStyle() const { return _lastLocalDiscoveryStyle; }
     LocalDiscoveryStyle lastLocalDiscoveryStyle() const { return _lastLocalDiscoveryStyle; }
 
 
@@ -301,7 +310,9 @@ private:
     QSet<QString> _uniqueErrors;
     QSet<QString> _uniqueErrors;
 
 
     /** The kind of local discovery the last sync run used */
     /** The kind of local discovery the last sync run used */
-    LocalDiscoveryStyle _lastLocalDiscoveryStyle = LocalDiscoveryStyle::DatabaseAndFilesystem;
+    LocalDiscoveryStyle _lastLocalDiscoveryStyle = LocalDiscoveryStyle::FilesystemOnly;
+    LocalDiscoveryStyle _localDiscoveryStyle = LocalDiscoveryStyle::FilesystemOnly;
+    std::set<QByteArray> _localDiscoveryPaths;
 };
 };
 }
 }
 
 

+ 2 - 4
src/libsync/syncresult.cpp

@@ -185,10 +185,8 @@ void SyncResult::processCompletedItem(const SyncFileItemPtr &item)
                 // nothing.
                 // nothing.
                 break;
                 break;
             }
             }
-        } else if (item->_direction == SyncFileItem::None) {
-            if (item->_instruction == CSYNC_INSTRUCTION_IGNORE) {
-                _foundFilesNotSynced = true;
-            }
+        } else if (item->_instruction == CSYNC_INSTRUCTION_IGNORE) {
+            _foundFilesNotSynced = true;
         }
         }
     }
     }
 }
 }

+ 1 - 0
src/libsync/syncresult.h

@@ -68,6 +68,7 @@ public:
     int numRenamedItems() const { return _numRenamedItems; }
     int numRenamedItems() const { return _numRenamedItems; }
     int numNewConflictItems() const { return _numNewConflictItems; }
     int numNewConflictItems() const { return _numNewConflictItems; }
     int numOldConflictItems() const { return _numOldConflictItems; }
     int numOldConflictItems() const { return _numOldConflictItems; }
+    void setNumOldConflictItems(int n) { _numOldConflictItems = n; }
     int numErrorItems() const { return _numErrorItems; }
     int numErrorItems() const { return _numErrorItems; }
     bool hasUnresolvedConflicts() const { return _numNewConflictItems + _numOldConflictItems > 0; }
     bool hasUnresolvedConflicts() const { return _numNewConflictItems + _numOldConflictItems > 0; }
 
 

+ 32 - 0
test/testsyncengine.cpp

@@ -605,6 +605,38 @@ private slots:
         QCOMPARE(fakeFolder.syncEngine().lastLocalDiscoveryStyle(), LocalDiscoveryStyle::FilesystemOnly);
         QCOMPARE(fakeFolder.syncEngine().lastLocalDiscoveryStyle(), LocalDiscoveryStyle::FilesystemOnly);
     }
     }
 
 
+    void testLocalDiscoveryDecision()
+    {
+        FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+        auto &engine = fakeFolder.syncEngine();
+
+        QVERIFY(engine.shouldDiscoverLocally(""));
+        QVERIFY(engine.shouldDiscoverLocally("A"));
+        QVERIFY(engine.shouldDiscoverLocally("A/X"));
+
+        fakeFolder.syncEngine().setLocalDiscoveryOptions(
+            LocalDiscoveryStyle::DatabaseAndFilesystem,
+            { "A/X", "foo bar space/touch", "foo/", "zzz" });
+
+        QVERIFY(engine.shouldDiscoverLocally(""));
+        QVERIFY(engine.shouldDiscoverLocally("A"));
+        QVERIFY(engine.shouldDiscoverLocally("A/X"));
+        QVERIFY(!engine.shouldDiscoverLocally("B"));
+        QVERIFY(!engine.shouldDiscoverLocally("A B"));
+        QVERIFY(!engine.shouldDiscoverLocally("B/X"));
+        QVERIFY(!engine.shouldDiscoverLocally("A/X/Y"));
+        QVERIFY(engine.shouldDiscoverLocally("foo bar space"));
+        QVERIFY(engine.shouldDiscoverLocally("foo"));
+        QVERIFY(!engine.shouldDiscoverLocally("foo bar"));
+        QVERIFY(!engine.shouldDiscoverLocally("foo bar/touch"));
+
+        fakeFolder.syncEngine().setLocalDiscoveryOptions(
+            LocalDiscoveryStyle::DatabaseAndFilesystem,
+            {});
+
+        QVERIFY(!engine.shouldDiscoverLocally(""));
+    }
+
     void testDiscoveryHiddenFile()
     void testDiscoveryHiddenFile()
     {
     {
         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
         FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };