]> dgit.raspbian.org Git - nextcloud-desktop.git/commitdiff
Remove stale caseclash conflicts when one of conflicting files has been removed from...
authoralex-z <blackslayer4@gmail.com>
Wed, 7 Jun 2023 16:47:24 +0000 (18:47 +0200)
committeralex-z <blackslayer4@gmail.com>
Thu, 15 Jun 2023 08:13:22 +0000 (10:13 +0200)
Signed-off-by: alex-z <blackslayer4@gmail.com>
src/libsync/discovery.cpp
src/libsync/discovery.h
src/libsync/syncengine.cpp
src/libsync/syncengine.h
test/testsyncengine.cpp

index e50817cb369137b6c3ca25d64a7db98fe8a4d2eb..e336cf54811b782be72b4c0f3feb603ad18ab617 100644 (file)
@@ -216,7 +216,7 @@ void ProcessDirectoryJob::process()
         // local stat function.
         // Recall file shall not be ignored (#4420)
         bool isHidden = e.localEntry.isHidden || (!f.first.isEmpty() && f.first[0] == '.' && f.first != QLatin1String(".sys.admin#recall#"));
-        if (handleExcluded(path._target, e, isHidden))
+        if (handleExcluded(path._target, e, entries, isHidden))
             continue;
 
         const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() &&
@@ -243,7 +243,7 @@ void ProcessDirectoryJob::process()
     QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
 }
 
-bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &entries, bool isHidden)
+bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &entries, const std::map<QString, Entries> &allEntries, bool isHidden)
 {
     const auto isDirectory = entries.localEntry.isDirectory || entries.serverEntry.isDirectory;
 
@@ -316,6 +316,14 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent
     item->_originalFile = path;
     item->_instruction = CSYNC_INSTRUCTION_IGNORE;
 
+    if (excluded == CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT && canRemoveCaseClashConflictedCopy(path, allEntries)) {
+        excluded = CSYNC_NOT_EXCLUDED;
+        item->_instruction = CSYNC_INSTRUCTION_REMOVE;
+        item->_direction = SyncFileItem::Down;
+        emit _discoveryData->itemDiscovered(item);
+        return true;
+    }
+
     if (entries.localEntry.isSymLink) {
         /* Symbolic links are ignored. */
         item->_errorString = tr("Symbolic links are not supported in syncing.");
@@ -393,6 +401,38 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent
     return true;
 }
 
+bool ProcessDirectoryJob::canRemoveCaseClashConflictedCopy(const QString &path, const std::map<QString, Entries> &allEntries)
+{
+    const auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByPath(path.toUtf8());
+    const auto originalBaseFileName = QFileInfo(QString(_discoveryData->_localDir + "/" + conflictRecord.initialBasePath)).fileName();
+
+    if (allEntries.find(originalBaseFileName) == allEntries.end()) {
+        // original entry is no longer on the server, remove conflicted copy
+        qCDebug(lcDisco) << "original entry:" << originalBaseFileName << "is no longer on the server, remove conflicted copy:" << path;
+        return true;
+    }
+
+    auto numMatchingEntries = 0;
+    for (auto it = allEntries.cbegin(); it != allEntries.cend(); ++it) {
+        if (it->first.compare(originalBaseFileName, Qt::CaseInsensitive) == 0 && it->second.serverEntry.isValid()) {
+            // only case-insensitive matching entries that are present on the server
+            ++numMatchingEntries;
+        }
+        if (numMatchingEntries >= 2) {
+            break;
+        }
+    }
+
+    if (numMatchingEntries < 2) {
+        // original entry is present on the server but there is no case-clash conflict anymore, remove conflicted copy (only 1 matching file found during case-insensitive search)
+        qCDebug(lcDisco) << "original entry:" << originalBaseFileName << "is present on the server, but there is no case-clas conflict anymore, remove conflicted copy:" << path;
+        _discoveryData->_anotherSyncNeeded = true;
+        return true;
+    }
+
+    return false;
+}
+
 void ProcessDirectoryJob::checkAndUpdateSelectiveSyncListsForE2eeFolders(const QString &path)
 {
     bool ok = false;
index c77e310821f53f4174a559c8002bfedd5bad2a4d..debb4572de40e4ce51869bec4c1ad7ad9db82f3f 100644 (file)
@@ -146,7 +146,9 @@ private:
 
     // return true if the file is excluded.
     // path is the full relative path of the file. localName is the base name of the local entry.
-    bool handleExcluded(const QString &path, const Entries &entries, bool isHidden);
+    bool handleExcluded(const QString &path, const Entries &entries, const std::map<QString, Entries> &allEntries, bool isHidden);
+
+    bool canRemoveCaseClashConflictedCopy(const QString &path, const std::map<QString, Entries> &allEntries);
 
     // check if the path is an e2e encrypted and the e2ee is not set up, and insert it into a corresponding list in the sync journal
     void checkAndUpdateSelectiveSyncListsForE2eeFolders(const QString &path);
index 5f955ac0dd46708438222e77c430e91aeb8d4d89..b6555ce522329c91949d9777aa988af9d122c3a9 100644 (file)
@@ -595,6 +595,8 @@ void SyncEngine::startSync()
         return;
     }
 
+    processCaseClashConflictsBeforeDiscovery();
+
     _stopWatch.start();
     _progressInfo->_status = ProgressInfo::Starting;
     emit transmissionProgress(*_progressInfo);
@@ -978,6 +980,23 @@ void SyncEngine::finalize(bool success)
     _leadingAndTrailingSpacesFilesAllowed.clear();
 }
 
+void SyncEngine::processCaseClashConflictsBeforeDiscovery()
+{
+    QSet<QByteArray> pathsToAppend;
+    const auto caseClashConflictPaths = _journal->caseClashConflictRecordPaths();
+    for (const auto &caseClashConflictPath : caseClashConflictPaths) {
+        auto caseClashPathSplit = caseClashConflictPath.split('/');
+        if (caseClashPathSplit.size() > 1) {
+            caseClashPathSplit.removeLast();
+            pathsToAppend.insert(caseClashPathSplit.join('/'));
+        }
+    }
+
+    for (const auto &pathToAppend : pathsToAppend) {
+        _journal->schedulePathForRemoteDiscovery(pathToAppend);
+    }
+}
+
 void SyncEngine::slotProgress(const SyncFileItem &item, qint64 current)
 {
     _progressInfo->setProgressItem(item, current);
index ba81fe237438d44f52a62044952422ee6559943c..437d2220110f20865e275f915885bbd91cf2c848 100644 (file)
@@ -291,6 +291,8 @@ private:
     // cleanup and emit the finished signal
     void finalize(bool success);
 
+    void processCaseClashConflictsBeforeDiscovery();
+
     // Aggregate scheduled sync runs into interval buckets. Can be used to
     // schedule a sync run per bucket instead of per file, reducing load.
     //
index 16b941638b3fe4bc8f9dd3f20323839ed63b95b7..d1b42bb93df5fddd1cf401c14faca44bde874c90 100644 (file)
@@ -1597,6 +1597,67 @@ private slots:
             QCOMPARE(conflicts.size(), 0);
         }
     }
+
+    void testServer_caseClash_createConflict_thenRemoveOneRemoteFile()
+    {
+        constexpr auto testLowerCaseFile = "test";
+        constexpr auto testUpperCaseFile = "TEST";
+
+#if defined Q_OS_LINUX
+        constexpr auto shouldHaveCaseClashConflict = false;
+#else
+        constexpr auto shouldHaveCaseClashConflict = true;
+#endif
+
+        FakeFolder fakeFolder{FileInfo{}};
+
+        fakeFolder.remoteModifier().insert("otherFile.txt");
+        fakeFolder.remoteModifier().insert(testLowerCaseFile);
+        fakeFolder.remoteModifier().insert(testUpperCaseFile);
+
+        fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+        QVERIFY(fakeFolder.syncOnce());
+
+        auto conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+        QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+        const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile);
+        QCOMPARE(hasConflict, shouldHaveCaseClashConflict ? true : false);
+
+        fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+        QVERIFY(fakeFolder.syncOnce());
+
+        conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+        QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+
+        // remove (UPPERCASE) file
+        fakeFolder.remoteModifier().remove(testUpperCaseFile);
+        QVERIFY(fakeFolder.syncOnce());
+
+        // make sure we got no conflicts now (conflicted copy gets removed)
+        conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+        QCOMPARE(conflicts.size(), 0);
+
+        // insert (UPPERCASE) file back
+        fakeFolder.remoteModifier().insert(testUpperCaseFile);
+        QVERIFY(fakeFolder.syncOnce());
+
+        // we must get conflits
+        conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+        QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+
+        // now remove (lowercase) file
+        fakeFolder.remoteModifier().remove(testLowerCaseFile);
+        QVERIFY(fakeFolder.syncOnce());
+
+        // make sure we got no conflicts now (conflicted copy gets removed)
+        conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+        QCOMPARE(conflicts.size(), 0);
+
+        // remove both files from the server(lower and UPPER case)
+        fakeFolder.remoteModifier().remove(testLowerCaseFile);
+        fakeFolder.remoteModifier().remove(testUpperCaseFile);
+        QVERIFY(fakeFolder.syncOnce());
+    }
 };
 
 QTEST_GUILESS_MAIN(TestSyncEngine)