// 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() &&
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;
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.");
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;
// 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);
return;
}
+ processCaseClashConflictsBeforeDiscovery();
+
_stopWatch.start();
_progressInfo->_status = ProgressInfo::Starting;
emit transmissionProgress(*_progressInfo);
_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);
// 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.
//
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)