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.
|
// local stat function.
|
||||||
// Recall file shall not be ignored (#4420)
|
// Recall file shall not be ignored (#4420)
|
||||||
bool isHidden = e.localEntry.isHidden || (!f.first.isEmpty() && f.first[0] == '.' && f.first != QLatin1String(".sys.admin#recall#"));
|
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;
|
continue;
|
||||||
|
|
||||||
const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() &&
|
const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() &&
|
||||||
|
@ -243,7 +243,7 @@ void ProcessDirectoryJob::process()
|
||||||
QTimer::singleShot(0, _discoveryData, &DiscoveryPhase::scheduleMoreJobs);
|
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;
|
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->_originalFile = path;
|
||||||
item->_instruction = CSYNC_INSTRUCTION_IGNORE;
|
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) {
|
if (entries.localEntry.isSymLink) {
|
||||||
/* Symbolic links are ignored. */
|
/* Symbolic links are ignored. */
|
||||||
item->_errorString = tr("Symbolic links are not supported in syncing.");
|
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;
|
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)
|
void ProcessDirectoryJob::checkAndUpdateSelectiveSyncListsForE2eeFolders(const QString &path)
|
||||||
{
|
{
|
||||||
bool ok = false;
|
bool ok = false;
|
||||||
|
|
|
@ -146,7 +146,9 @@ private:
|
||||||
|
|
||||||
// return true if the file is excluded.
|
// 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.
|
// 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
|
// 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);
|
void checkAndUpdateSelectiveSyncListsForE2eeFolders(const QString &path);
|
||||||
|
|
|
@ -595,6 +595,8 @@ void SyncEngine::startSync()
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
processCaseClashConflictsBeforeDiscovery();
|
||||||
|
|
||||||
_stopWatch.start();
|
_stopWatch.start();
|
||||||
_progressInfo->_status = ProgressInfo::Starting;
|
_progressInfo->_status = ProgressInfo::Starting;
|
||||||
emit transmissionProgress(*_progressInfo);
|
emit transmissionProgress(*_progressInfo);
|
||||||
|
@ -978,6 +980,23 @@ void SyncEngine::finalize(bool success)
|
||||||
_leadingAndTrailingSpacesFilesAllowed.clear();
|
_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)
|
void SyncEngine::slotProgress(const SyncFileItem &item, qint64 current)
|
||||||
{
|
{
|
||||||
_progressInfo->setProgressItem(item, current);
|
_progressInfo->setProgressItem(item, current);
|
||||||
|
|
|
@ -291,6 +291,8 @@ private:
|
||||||
// cleanup and emit the finished signal
|
// cleanup and emit the finished signal
|
||||||
void finalize(bool success);
|
void finalize(bool success);
|
||||||
|
|
||||||
|
void processCaseClashConflictsBeforeDiscovery();
|
||||||
|
|
||||||
// Aggregate scheduled sync runs into interval buckets. Can be used to
|
// Aggregate scheduled sync runs into interval buckets. Can be used to
|
||||||
// schedule a sync run per bucket instead of per file, reducing load.
|
// schedule a sync run per bucket instead of per file, reducing load.
|
||||||
//
|
//
|
||||||
|
|
|
@ -1597,6 +1597,67 @@ private slots:
|
||||||
QCOMPARE(conflicts.size(), 0);
|
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)
|
QTEST_GUILESS_MAIN(TestSyncEngine)
|
||||||
|
|
Loading…
Reference in a new issue