mirror of
https://github.com/nextcloud/desktop.git
synced 2024-10-23 12:55:44 +03:00
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:
parent
254c4ebf5d
commit
e6f003b00b
5 changed files with 127 additions and 3 deletions
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue