Remove stale caseclash conflicts when one of conflicting files has been removed from server

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2023-06-07 18:47:24 +02:00
parent 254c4ebf5d
commit e6f003b00b
5 changed files with 127 additions and 3 deletions

View 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;

View 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);

View 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);

View 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.
//

View 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)