# build the GUI component, when disabled only nextcloudcmd is built
option(BUILD_GUI "BUILD_GUI" ON)
+# build the tests
+option(BUILD_TESTING "BUILD_TESTING" ON)
+
# When this option is enabled, 5xx errors are not added to the blacklist
# Normally you don't want to enable this option because if a particular file
# triggers a bug on the server, you want the file to be blacklisted.
DeleteKeyValueStoreQuery,
GetConflictRecordQuery,
SetConflictRecordQuery,
+ GetCaseClashConflictRecordQuery,
+ GetCaseClashConflictRecordByPathQuery,
+ SetCaseClashConflictRecordQuery,
+ DeleteCaseClashConflictRecordQuery,
+ GetAllCaseClashConflictPathQuery,
DeleteConflictRecordQuery,
GetRawPinStateQuery,
GetEffectivePinStateQuery,
return sqlFail(QStringLiteral("Create table conflicts"), createQuery);
}
+ // create the caseconflicts table.
+ createQuery.prepare("CREATE TABLE IF NOT EXISTS caseconflicts("
+ "path TEXT PRIMARY KEY,"
+ "baseFileId TEXT,"
+ "baseEtag TEXT,"
+ "baseModtime INTEGER,"
+ "basePath TEXT UNIQUE"
+ ");");
+ if (!createQuery.exec()) {
+ return sqlFail(QStringLiteral("Create table caseconflicts"), createQuery);
+ }
+
createQuery.prepare("CREATE TABLE IF NOT EXISTS version("
"major INTEGER(8),"
"minor INTEGER(8),"
return entry;
}
+void SyncJournalDb::setCaseConflictRecord(const ConflictRecord &record)
+{
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect())
+ return;
+
+ const auto query = _queryManager.get(PreparedSqlQueryManager::SetCaseClashConflictRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO caseconflicts "
+ "(path, baseFileId, baseModtime, baseEtag, basePath) "
+ "VALUES (?1, ?2, ?3, ?4, ?5);"),
+ _db);
+ ASSERT(query)
+ query->bindValue(1, record.path);
+ query->bindValue(2, record.baseFileId);
+ query->bindValue(3, record.baseModtime);
+ query->bindValue(4, record.baseEtag);
+ query->bindValue(5, record.initialBasePath);
+ ASSERT(query->exec())
+}
+
+ConflictRecord SyncJournalDb::caseConflictRecordByBasePath(const QString &baseNamePath)
+{
+ ConflictRecord entry;
+
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect()) {
+ return entry;
+ }
+ const auto query = _queryManager.get(PreparedSqlQueryManager::GetCaseClashConflictRecordQuery, QByteArrayLiteral("SELECT path, baseFileId, baseModtime, baseEtag, basePath FROM caseconflicts WHERE basePath=?1;"), _db);
+ ASSERT(query)
+ query->bindValue(1, baseNamePath);
+ ASSERT(query->exec())
+ if (!query->next().hasData)
+ return entry;
+
+ entry.path = query->baValue(0);
+ entry.baseFileId = query->baValue(1);
+ entry.baseModtime = query->int64Value(2);
+ entry.baseEtag = query->baValue(3);
+ entry.initialBasePath = query->baValue(4);
+ return entry;
+}
+
+ConflictRecord SyncJournalDb::caseConflictRecordByPath(const QString &path)
+{
+ ConflictRecord entry;
+
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect()) {
+ return entry;
+ }
+ const auto query = _queryManager.get(PreparedSqlQueryManager::GetCaseClashConflictRecordByPathQuery, QByteArrayLiteral("SELECT path, baseFileId, baseModtime, baseEtag, basePath FROM caseconflicts WHERE path=?1;"), _db);
+ ASSERT(query)
+ query->bindValue(1, path);
+ ASSERT(query->exec())
+ if (!query->next().hasData)
+ return entry;
+
+ entry.path = query->baValue(0);
+ entry.baseFileId = query->baValue(1);
+ entry.baseModtime = query->int64Value(2);
+ entry.baseEtag = query->baValue(3);
+ entry.initialBasePath = query->baValue(4);
+ return entry;
+}
+
+void SyncJournalDb::deleteCaseClashConflictByPathRecord(const QString &path)
+{
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect())
+ return;
+
+ const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteCaseClashConflictRecordQuery, QByteArrayLiteral("DELETE FROM caseconflicts WHERE path=?1;"), _db);
+ ASSERT(query)
+ query->bindValue(1, path);
+ ASSERT(query->exec())
+}
+
+QByteArrayList SyncJournalDb::caseClashConflictRecordPaths()
+{
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect()) {
+ return {};
+ }
+
+ const auto query = _queryManager.get(PreparedSqlQueryManager::GetAllCaseClashConflictPathQuery, QByteArrayLiteral("SELECT path FROM caseconflicts;"), _db);
+ ASSERT(query)
+ ASSERT(query->exec())
+
+ QByteArrayList paths;
+ while (query->next().hasData)
+ paths.append(query->baValue(0));
+
+ return paths;
+}
+
void SyncJournalDb::deleteConflictRecord(const QByteArray &path)
{
QMutexLocker locker(&_mutex);
/// Retrieve a conflict record by path of the file with the conflict tag
ConflictRecord conflictRecord(const QByteArray &path);
+ /// Store a new or updated record in the database
+ void setCaseConflictRecord(const ConflictRecord &record);
+
+ /// Retrieve a conflict record by path of the file with the conflict tag
+ ConflictRecord caseConflictRecordByBasePath(const QString &baseNamePath);
+
+ /// Retrieve a conflict record by path of the file with the conflict tag
+ ConflictRecord caseConflictRecordByPath(const QString &path);
+
+ /// Delete a case clash conflict record by path of the file with the conflict tag
+ void deleteCaseClashConflictByPathRecord(const QString &path);
+
+ /// Return all paths of files with a conflict tag in the name and records in the db
+ QByteArrayList caseClashConflictRecordPaths();
+
/// Delete a conflict record by path of the file with the conflict tag
void deleteConflictRecord(const QByteArray &path);
return conflictFileName;
}
-bool Utility::isConflictFile(const char *name)
-{
- const char *bname = std::strrchr(name, '/');
- if (bname) {
- bname += 1;
- } else {
- bname = name;
- }
-
- // Old pattern
- if (std::strstr(bname, "_conflict-"))
- return true;
-
- // New pattern
- if (std::strstr(bname, "(conflicted copy"))
- return true;
-
- return false;
-}
-
bool Utility::isConflictFile(const QString &name)
{
auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
- if (bname.contains(QStringLiteral("_conflict-")))
+ if (bname.contains(QStringLiteral("_conflict-"))) {
return true;
+ }
- if (bname.contains(QStringLiteral("(conflicted copy")))
+ if (bname.contains(QStringLiteral("(conflicted copy"))) {
return true;
+ }
+
+ if (isCaseClashConflictFile(name)) {
+ return true;
+ }
return false;
}
return result;
}
+QString Utility::makeCaseClashConflictFileName(const QString &filename, const QDateTime &datetime)
+{
+ auto conflictFileName(filename);
+ // Add conflict tag before the extension.
+ auto dotLocation = conflictFileName.lastIndexOf(QLatin1Char('.'));
+ // If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
+ if (dotLocation <= conflictFileName.lastIndexOf(QLatin1Char('/')) + 1) {
+ dotLocation = conflictFileName.size();
+ }
+
+ auto conflictMarker = QStringLiteral(" (case clash from ");
+ conflictMarker += datetime.toString(QStringLiteral("yyyy-MM-dd hhmmss")) + QLatin1Char(')');
+
+ conflictFileName.insert(dotLocation, conflictMarker);
+ return conflictFileName;
+}
+
+bool Utility::isCaseClashConflictFile(const QString &name)
+{
+ auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
+
+ if (bname.contains(QStringLiteral("(case clash from"))) {
+ return true;
+ }
+
+ return false;
+}
+
} // namespace OCC
OCSYNC_EXPORT QString makeConflictFileName(
const QString &fn, const QDateTime &dt, const QString &user);
+ OCSYNC_EXPORT QString makeCaseClashConflictFileName(const QString &filename, const QDateTime &datetime);
+
/** Returns whether a file name indicates a conflict file
*/
- OCSYNC_EXPORT bool isConflictFile(const char *name);
+ bool isConflictFile(const char *name) = delete;
OCSYNC_EXPORT bool isConflictFile(const QString &name);
+ OCSYNC_EXPORT bool isCaseClashConflictFile(const QString &name);
/** Find the base name for a conflict file name, using name pattern only
*
* the csync state of a file.
*/
enum SyncInstructions {
- CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
- CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
- CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
- CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
- CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
- CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
- CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
- CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
- CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
- CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
- CSYNC_INSTRUCTION_ERROR = 1 << 8,
- CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
- Used when the type of something changes from directory to file
- or back. */
- CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
- but without any propagation (UPDATE|RECONCILE) */
+ CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
+ CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
+ CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
+ CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
+ CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
+ CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
+ CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
+ CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
+ CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
+ CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
+ CSYNC_INSTRUCTION_ERROR = 1 << 8,
+ CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
+ Used when the type of something changes from directory to file
+ or back. */
+ CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
+ but without any propagation (UPDATE|RECONCILE) */
+ CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */
};
Q_ENUM_NS(SyncInstructions)
return CSYNC_FILE_SILENTLY_EXCLUDED;
}
-
- if (excludeConflictFiles && OCC::Utility::isConflictFile(path)) {
- return CSYNC_FILE_EXCLUDE_CONFLICT;
+ if (excludeConflictFiles) {
+ if (OCC::Utility::isCaseClashConflictFile(path)) {
+ return CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT;
+ } else if (OCC::Utility::isConflictFile(path)) {
+ return CSYNC_FILE_EXCLUDE_CONFLICT;
+ }
}
+
return CSYNC_NOT_EXCLUDED;
}
CSYNC_FILE_EXCLUDE_HIDDEN,
CSYNC_FILE_EXCLUDE_STAT_FAILED,
CSYNC_FILE_EXCLUDE_CONFLICT,
+ CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT,
CSYNC_FILE_EXCLUDE_CANNOT_ENCODE,
CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED,
CSYNC_FILE_EXCLUDE_LEADING_SPACE,
accountsettings.ui
conflictdialog.ui
invalidfilenamedialog.ui
+ caseclashfilenamedialog.ui
foldercreationdialog.ui
folderwizardsourcepage.ui
folderwizardtargetpage.ui
application.cpp
invalidfilenamedialog.h
invalidfilenamedialog.cpp
+ caseclashfilenamedialog.h
+ caseclashfilenamedialog.cpp
callstatechecker.h
callstatechecker.cpp
conflictdialog.h
--- /dev/null
+/*
+ * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#include "caseclashfilenamedialog.h"
+#include "ui_caseclashfilenamedialog.h"
+
+#include "account.h"
+#include "folder.h"
+
+#include <QPushButton>
+#include <QDir>
+#include <QAbstractButton>
+#include <QDialogButtonBox>
+#include <QFileInfo>
+#include <QPushButton>
+#include <QDirIterator>
+#include <QDesktopServices>
+#include <QLoggingCategory>
+
+#include <array>
+
+namespace {
+constexpr std::array<QChar, 9> caseClashIllegalCharacters({ '\\', '/', ':', '?', '*', '\"', '<', '>', '|' });
+
+QVector<QChar> getCaseClashIllegalCharsFromString(const QString &string)
+{
+ QVector<QChar> result;
+ for (const auto &character : string) {
+ if (std::find(caseClashIllegalCharacters.begin(), caseClashIllegalCharacters.end(), character)
+ != caseClashIllegalCharacters.end()) {
+ result.push_back(character);
+ }
+ }
+ return result;
+}
+
+QString caseClashIllegalCharacterListToString(const QVector<QChar> &illegalCharacters)
+{
+ QString illegalCharactersString;
+ if (illegalCharacters.size() > 0) {
+ illegalCharactersString += illegalCharacters[0];
+ }
+
+ for (int i = 1; i < illegalCharacters.count(); ++i) {
+ if (illegalCharactersString.contains(illegalCharacters[i])) {
+ continue;
+ }
+ illegalCharactersString += " " + illegalCharacters[i];
+ }
+ return illegalCharactersString;
+}
+}
+
+namespace OCC {
+
+Q_LOGGING_CATEGORY(lcCaseClashConflictFialog, "nextcloud.sync.caseclash.dialog", QtInfoMsg)
+
+CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account,
+ Folder *folder,
+ const QString &conflictFilePath,
+ const QString &conflictTaggedPath,
+ QWidget *parent)
+ : QDialog(parent)
+ , _ui(std::make_unique<Ui::CaseClashFilenameDialog>())
+ , _conflictSolver(conflictFilePath, conflictTaggedPath, folder->remotePath(), folder->path(), account, folder->journalDb())
+ , _account(account)
+ , _folder(folder)
+ , _filePath(std::move(filePath))
+{
+ Q_ASSERT(_account);
+ Q_ASSERT(_folder);
+
+ const auto filePathFileInfo = QFileInfo(_filePath);
+ _relativeFilePath = filePathFileInfo.path() + QStringLiteral("/");
+ _relativeFilePath = _relativeFilePath.replace(folder->path(), QLatin1String());
+ _relativeFilePath = _relativeFilePath.isEmpty() ? QString() : _relativeFilePath + QStringLiteral("/");
+
+ _originalFileName = _relativeFilePath + filePathFileInfo.fileName();
+
+ _ui->setupUi(this);
+ _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+ _ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Rename file"));
+
+ _ui->descriptionLabel->setText(tr("The file \"%1\" could not be synced because of a case clash conflict with an existing file on this system.").arg(_originalFileName));
+ _ui->explanationLabel->setText(tr("The system you are using cannot have two file names with only casing differences."));
+ _ui->filenameLineEdit->setText(filePathFileInfo.fileName());
+
+ connect(_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+ _ui->errorLabel->setText({}/*
+ tr("Checking rename permissions …")*/);
+ _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+ _ui->filenameLineEdit->setEnabled(false);
+
+ connect(_ui->filenameLineEdit, &QLineEdit::textChanged, this,
+ &CaseClashFilenameDialog::onFilenameLineEditTextChanged);
+
+ connect(&_conflictSolver, &CaseClashConflictSolver::errorStringChanged, this, [this] () {
+ _ui->errorLabel->setText(_conflictSolver.errorString());
+ });
+
+ connect(&_conflictSolver, &CaseClashConflictSolver::allowedToRenameChanged, this, [this] () {
+ _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() &~ QDialogButtonBox::No);
+ if (_conflictSolver.allowedToRename()) {
+ _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
+ _ui->filenameLineEdit->setEnabled(true);
+ _ui->filenameLineEdit->selectAll();
+ } else {
+ _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() | QDialogButtonBox::No);
+ }
+ });
+
+ connect(&_conflictSolver, &CaseClashConflictSolver::failed, this, [this] () {
+ _ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
+ });
+
+ connect(&_conflictSolver, &CaseClashConflictSolver::done, this, [this] () {
+ Q_EMIT successfulRename(_folder->remotePath() + _newFilename);
+ QDialog::accept();
+ });
+
+ checkIfAllowedToRename();
+}
+
+CaseClashFilenameDialog::~CaseClashFilenameDialog() = default;
+
+QString CaseClashFilenameDialog::caseClashConflictFile(const QString &conflictFilePath)
+{
+ const auto filePathFileInfo = QFileInfo(conflictFilePath);
+ const auto conflictFileName = filePathFileInfo.fileName();
+
+ QDirIterator it(filePathFileInfo.path(), QDirIterator::Subdirectories);
+
+ while(it.hasNext()) {
+ const auto filePath = it.next();
+ qCDebug(lcCaseClashConflictFialog) << filePath;
+ QFileInfo fileInfo(filePath);
+
+ if(fileInfo.isDir()) {
+ continue;
+ }
+
+ const auto currentFileName = fileInfo.fileName();
+ if (currentFileName.compare(conflictFileName, Qt::CaseInsensitive) == 0 &&
+ currentFileName != conflictFileName) {
+
+ return filePath;
+ }
+ }
+
+ return {};
+}
+
+void CaseClashFilenameDialog::updateFileWidgetGroup(const QString &filePath,
+ const QString &linkText,
+ QLabel *filenameLabel,
+ QLabel *linkLabel,
+ QLabel *mtimeLabel,
+ QLabel *sizeLabel,
+ QToolButton *button) const
+{
+ const auto filePathFileInfo = QFileInfo(filePath);
+ const auto filename = filePathFileInfo.fileName();
+ const auto lastModifiedString = filePathFileInfo.lastModified().toString();
+ const auto fileSizeString = locale().formattedDataSize(filePathFileInfo.size());
+ const auto fileUrl = QUrl::fromLocalFile(filePath).toString();
+ const auto linkString = QStringLiteral("<a href='%1'>%2</a>").arg(fileUrl, linkText);
+ const auto mime = QMimeDatabase().mimeTypeForFile(_filePath);
+ QIcon fileTypeIcon;
+
+ qCDebug(lcCaseClashConflictFialog) << filePath << filePathFileInfo.exists() << filename << lastModifiedString << fileSizeString << fileUrl << linkString << mime;
+
+ if (QIcon::hasThemeIcon(mime.iconName())) {
+ fileTypeIcon = QIcon::fromTheme(mime.iconName());
+ } else {
+ fileTypeIcon = QIcon(":/qt-project.org/styles/commonstyle/images/file-128.png");
+ }
+
+ filenameLabel->setText(filename);
+ mtimeLabel->setText(lastModifiedString);
+ sizeLabel->setText(fileSizeString);
+ linkLabel->setText(linkString);
+ button->setIcon(fileTypeIcon);
+}
+
+void CaseClashFilenameDialog::checkIfAllowedToRename()
+{
+ _conflictSolver.checkIfAllowedToRename();
+}
+
+bool CaseClashFilenameDialog::processLeadingOrTrailingSpacesError(const QString &fileName)
+{
+ const auto hasLeadingSpaces = fileName.startsWith(QLatin1Char(' '));
+ const auto hasTrailingSpaces = fileName.endsWith(QLatin1Char(' '));
+
+ _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() &~ QDialogButtonBox::No);
+
+ if (hasLeadingSpaces || hasTrailingSpaces) {
+ if (hasLeadingSpaces && hasTrailingSpaces) {
+ _ui->errorLabel->setText(tr("Filename contains leading and trailing spaces."));
+ }
+ else if (hasLeadingSpaces) {
+ _ui->errorLabel->setText(tr("Filename contains leading spaces."));
+ } else if (hasTrailingSpaces) {
+ _ui->errorLabel->setText(tr("Filename contains trailing spaces."));
+ }
+
+ if (!Utility::isWindows()) {
+ _ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() | QDialogButtonBox::No);
+ _ui->buttonBox->button(QDialogButtonBox::No)->setText(tr("Use invalid name"));
+ }
+
+ return true;
+ }
+
+ return false;
+}
+
+void CaseClashFilenameDialog::accept()
+{
+ _newFilename = _relativeFilePath + _ui->filenameLineEdit->text().trimmed();
+ _conflictSolver.solveConflict(_newFilename);
+}
+
+void CaseClashFilenameDialog::onFilenameLineEditTextChanged(const QString &text)
+{
+ const auto isNewFileNameDifferent = text != _originalFileName;
+ const auto illegalContainedCharacters = getCaseClashIllegalCharsFromString(text);
+ const auto containsIllegalChars = !illegalContainedCharacters.empty() || text.endsWith(QLatin1Char('.'));
+ const auto isTextValid = isNewFileNameDifferent && !containsIllegalChars;
+
+ _ui->errorLabel->setText("");
+
+ if (!processLeadingOrTrailingSpacesError(text) && !isTextValid){
+ _ui->errorLabel->setText(tr("Filename contains illegal characters: %1").arg(caseClashIllegalCharacterListToString(illegalContainedCharacters)));
+ }
+
+ _ui->buttonBox->button(QDialogButtonBox::Ok)
+ ->setEnabled(isTextValid);
+}
+}
--- /dev/null
+/*
+ * Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+#pragma once
+
+#include "accountfwd.h"
+#include "caseclashconflictsolver.h"
+
+#include <QDialog>
+#include <QLabel>
+#include <QToolButton>
+#include <QNetworkReply>
+
+#include <memory>
+
+namespace OCC {
+
+class Folder;
+
+namespace Ui {
+ class CaseClashFilenameDialog;
+}
+
+
+class CaseClashFilenameDialog : public QDialog
+{
+ Q_OBJECT
+
+public:
+ explicit CaseClashFilenameDialog(AccountPtr account,
+ Folder *folder,
+ const QString &conflictFilePath,
+ const QString &conflictTaggedPath,
+ QWidget *parent = nullptr);
+
+ ~CaseClashFilenameDialog() override;
+
+ void accept() override;
+
+signals:
+ void successfulRename(const QString &filePath);
+
+private slots:
+ void updateFileWidgetGroup(const QString &filePath,
+ const QString &linkText,
+ QLabel *filenameLabel,
+ QLabel *linkLabel,
+ QLabel *mtimeLabel,
+ QLabel *sizeLabel,
+ QToolButton *button) const;
+
+private:
+ // Find the conflicting file path
+ static QString caseClashConflictFile(const QString &conflictFilePath);
+
+ void onFilenameLineEditTextChanged(const QString &text);
+ void checkIfAllowedToRename();
+ bool processLeadingOrTrailingSpacesError(const QString &fileName);
+
+ std::unique_ptr<Ui::CaseClashFilenameDialog> _ui;
+ CaseClashConflictSolver _conflictSolver;
+ AccountPtr _account;
+ Folder *_folder = nullptr;
+
+ QString _filePath;
+ QString _relativeFilePath;
+ QString _originalFileName;
+ QString _newFilename;
+};
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>OCC::CaseClashFilenameDialog</class>
+ <widget class="QDialog" name="OCC::CaseClashFilenameDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>411</width>
+ <height>192</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Case Clash Conflict</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="sizeConstraint">
+ <enum>QLayout::SetDefaultConstraint</enum>
+ </property>
+ <item>
+ <widget class="QLabel" name="descriptionLabel">
+ <property name="text">
+ <string>The file could not be synced because it generates a case clash conflict with an existing file on this system.</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="explanationLabel">
+ <property name="text">
+ <string>Error</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="label">
+ <property name="text">
+ <string>Please enter a new name for the remote file:</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
+ </property>
+ <property name="wordWrap">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="filenameLineEdit">
+ <property name="placeholderText">
+ <string>New filename</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="errorLabel">
+ <property name="palette">
+ <palette>
+ <active>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="200">
+ <red>255</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </active>
+ <inactive>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="200">
+ <red>255</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </inactive>
+ <disabled>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="115">
+ <red>255</red>
+ <green>255</green>
+ <blue>255</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </disabled>
+ </palette>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
_engine->addAcceptedInvalidFileName(filePath);
}
+void Folder::acceptCaseClashConflictFileName(const QString &filePath)
+{
+ qCInfo(lcFolder) << "going to delete case clash conflict record" << filePath;
+ _journal.deleteCaseClashConflictByPathRecord(filePath);
+
+ qCInfo(lcFolder) << "going to delete" << path() + filePath;
+ FileSystem::remove(path() + filePath);
+}
+
void Folder::setSaveBackwardsCompatible(bool save)
{
_saveBackwardsCompatible = save;
void acceptInvalidFileName(const QString &filePath);
+ void acceptCaseClashConflictFileName(const QString &filePath);
+
/**
* Migration: When this flag is true, this folder will save to
* the backwards-compatible 'Folders' section in the config file.
};
}
-Q_DECLARE_METATYPE(OCC::SharePtr);
+Q_DECLARE_METATYPE(OCC::SharePtr)
#endif // SHAREMANAGER_H
* for more details.
*/
-#include <QtCore>
-#include <QAbstractListModel>
-#include <QDesktopServices>
-#include <QWidget>
-#include <QJsonObject>
-#include <QJsonDocument>
-#include <qloggingcategory.h>
+#include "activitylistmodel.h"
#include "account.h"
#include "accountstate.h"
#include "accountmanager.h"
#include "conflictdialog.h"
#include "folderman.h"
-#include "iconjob.h"
-#include "accessmanager.h"
#include "owncloudgui.h"
#include "guiutility.h"
#include "invalidfilenamedialog.h"
-
+#include "caseclashfilenamedialog.h"
#include "activitydata.h"
-#include "activitylistmodel.h"
#include "systray.h"
-#include "tray/usermodel.h"
-#include "theme.h"
+#include <QtCore>
+#include <QAbstractListModel>
+#include <QDesktopServices>
+#include <QWidget>
+#include <QJsonObject>
+#include <QJsonDocument>
+#include <qloggingcategory.h>
namespace OCC {
void ActivityListModel::addErrorToActivityList(const Activity &activity)
{
- qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._message << activity._subject;
+ qCInfo(lcActivity) << "Error successfully added to the notification list: " << activity._message << activity._subject << activity._syncResultStatus << activity._syncFileItemStatus;
addEntriesToActivityList({activity});
_notificationErrorsLists.prepend(activity);
}
_currentConflictDialog->open();
ownCloudGui::raiseDialog(_currentConflictDialog);
return;
+ } else if (activity._syncFileItemStatus == SyncFileItem::FileNameClash) {
+ triggerCaseClashAction(activity);
+ return;
} else if (activity._syncFileItemStatus == SyncFileItem::FileNameInvalid) {
if (!_currentInvalidFilenameDialog.isNull()) {
_currentInvalidFilenameDialog->close();
_currentInvalidFilenameDialog->open();
ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
return;
- } else if (activity._syncFileItemStatus == SyncFileItem::FileNameClash) {
- const auto folder = FolderMan::instance()->folder(activity._folder);
- const auto relPath = activity._fileAction == QStringLiteral("file_renamed") ? activity._renamedFile : activity._file;
- SyncJournalFileRecord record;
-
- if (!folder || !folder->journalDb()->getFileRecord(relPath, &record)) {
- return;
- }
-
- fetchPrivateLinkUrl(folder->accountState()->account(),
- relPath,
- record.numericFileId(),
- this,
- [](const QString &link) { Utility::openBrowser(link); }
- );
- return;
}
if (!path.isEmpty()) {
}
}
+void ActivityListModel::triggerCaseClashAction(Activity activity)
+{
+ qCInfo(lcActivity) << "case clash conflict" << activity._file << activity._syncFileItemStatus;
+
+ if (!_currentCaseClashFilenameDialog.isNull()) {
+ _currentCaseClashFilenameDialog->close();
+ }
+
+ auto folder = FolderMan::instance()->folder(activity._folder);
+ const auto conflictedRelativePath = activity._file;
+ const auto conflictRecord = folder->journalDb()->caseConflictRecordByBasePath(conflictedRelativePath);
+
+ const auto dir = QDir(folder->path());
+ const auto conflictedPath = dir.filePath(conflictedRelativePath);
+ const auto conflictTaggedPath = dir.filePath(conflictRecord.path);
+
+ _currentCaseClashFilenameDialog = new CaseClashFilenameDialog(_accountState->account(),
+ folder,
+ conflictedPath,
+ conflictTaggedPath);
+ connect(_currentCaseClashFilenameDialog, &CaseClashFilenameDialog::successfulRename, folder, [folder, activity](const QString& filePath) {
+ qCInfo(lcActivity) << "successfulRename" << filePath << activity._message;
+ folder->acceptCaseClashConflictFileName(activity._message);
+ folder->scheduleThisFolderSoon();
+ });
+ _currentCaseClashFilenameDialog->open();
+ ownCloudGui::raiseDialog(_currentCaseClashFilenameDialog);
+}
+
void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex)
{
if (activityIndex < 0 || activityIndex >= _finalList.size()) {
class AccountState;
class ConflictDialog;
class InvalidFilenameDialog;
+class CaseClashFilenameDialog;
/**
* @brief The ActivityListModel
void ingestActivities(const QJsonArray &activities);
void appendMoreActivitiesAvailableEntry();
void insertOrRemoveDummyFetchingActivity();
+ void triggerCaseClashAction(Activity activity);
Activity _notificationIgnoredFiles;
Activity _dummyFetchingActivities;
QPointer<ConflictDialog> _currentConflictDialog;
QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
+ QPointer<CaseClashFilenameDialog> _currentCaseClashFilenameDialog;
AccountState *_accountState = nullptr;
bool _currentlyFetching = false;
creds/credentialscommon.cpp
creds/keychainchunk.h
creds/keychainchunk.cpp
+ caseclashconflictsolver.h
+ caseclashconflictsolver.cpp
)
if (WIN32)
--- /dev/null
+#include "caseclashconflictsolver.h"
+
+#include "networkjobs.h"
+#include "propagateremotemove.h"
+#include "account.h"
+#include "common/syncjournaldb.h"
+#include "common/filesystembase.h"
+
+#include <QDir>
+#include <QFileInfo>
+#include <QLoggingCategory>
+
+using namespace OCC;
+
+Q_LOGGING_CATEGORY(lcCaseClashConflictSolver, "nextcloud.sync.caseclash.solver", QtInfoMsg)
+
+CaseClashConflictSolver::CaseClashConflictSolver(const QString &targetFilePath,
+ const QString &conflictFilePath,
+ const QString &remotePath,
+ const QString &localPath,
+ AccountPtr account,
+ SyncJournalDb *journal,
+ QObject *parent)
+ : QObject{parent}
+ , _account(account)
+ , _targetFilePath(targetFilePath)
+ , _conflictFilePath(conflictFilePath)
+ , _remotePath(remotePath)
+ , _localPath(localPath)
+ , _journal(journal)
+{
+#if !defined(QT_NO_DEBUG)
+ QFileInfo targetFileInfo(_targetFilePath);
+ Q_ASSERT(targetFileInfo.isAbsolute());
+ Q_ASSERT(QFileInfo::exists(_conflictFilePath));
+#endif
+}
+
+bool CaseClashConflictSolver::allowedToRename() const
+{
+ return _allowedToRename;
+}
+
+QString CaseClashConflictSolver::errorString() const
+{
+ return _errorString;
+}
+
+void CaseClashConflictSolver::solveConflict(const QString &newFilename)
+{
+ _newFilename = newFilename;
+
+ const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(remoteNewFilename()));
+ connect(propfindJob, &PropfindJob::result, this, &CaseClashConflictSolver::onRemoteDestinationFileAlreadyExists);
+ connect(propfindJob, &PropfindJob::finishedWithError, this, &CaseClashConflictSolver::onRemoteDestinationFileDoesNotExist);
+ propfindJob->start();
+}
+
+void CaseClashConflictSolver::onRemoteDestinationFileAlreadyExists()
+{
+ _allowedToRename = false;
+ emit allowedToRenameChanged();
+ _errorString = tr("Cannot rename file because a file with the same name does already exist on the server. Please pick another name.");
+ emit errorStringChanged();
+}
+
+void CaseClashConflictSolver::onRemoteDestinationFileDoesNotExist()
+{
+ const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(remoteTargetFilePath()));
+ connect(propfindJob, &PropfindJob::result, this, &CaseClashConflictSolver::onRemoteSourceFileAlreadyExists);
+ connect(propfindJob, &PropfindJob::finishedWithError, this, &CaseClashConflictSolver::onRemoteSourceFileDoesNotExist);
+ propfindJob->start();
+}
+
+void CaseClashConflictSolver::onPropfindPermissionSuccess(const QVariantMap &values)
+{
+ onCheckIfAllowedToRenameComplete(values);
+}
+
+void CaseClashConflictSolver::onPropfindPermissionError(QNetworkReply *reply)
+{
+ onCheckIfAllowedToRenameComplete({}, reply);
+}
+
+void CaseClashConflictSolver::onRemoteSourceFileAlreadyExists()
+{
+ const auto remoteSource = QDir::cleanPath(remoteTargetFilePath());
+ const auto remoteDestionation = QDir::cleanPath(_account->davUrl().path() + remoteNewFilename());
+ qCInfo(lcCaseClashConflictSolver) << "rename case clashing file from" << remoteSource << "to" << remoteDestionation;
+ const auto moveJob = new MoveJob(_account, remoteSource, remoteDestionation, this);
+ connect(moveJob, &MoveJob::finishedSignal, this, &CaseClashConflictSolver::onMoveJobFinished);
+ moveJob->start();
+}
+
+void CaseClashConflictSolver::onRemoteSourceFileDoesNotExist()
+{
+ Q_EMIT failed();
+}
+
+void CaseClashConflictSolver::onMoveJobFinished()
+{
+ const auto job = qobject_cast<MoveJob *>(sender());
+ const auto error = job->reply()->error();
+
+ if (error != QNetworkReply::NoError) {
+ _errorString = tr("Could not rename file. Please make sure you are connected to the server.");
+ emit errorStringChanged();
+
+ emit failed();
+ return;
+ }
+
+ qCInfo(lcCaseClashConflictSolver) << "going to delete case clash conflict record" << _targetFilePath;
+ _journal->deleteCaseClashConflictByPathRecord(_targetFilePath);
+
+ qCInfo(lcCaseClashConflictSolver) << "going to delete" << _conflictFilePath;
+ FileSystem::remove(_conflictFilePath);
+
+ Q_EMIT done();
+}
+
+QString CaseClashConflictSolver::remoteNewFilename() const
+{
+ if (_remotePath == QStringLiteral("/")) {
+ qCDebug(lcCaseClashConflictSolver) << _newFilename << _remotePath << _newFilename;
+ return _newFilename;
+ } else {
+ const auto result = QString{_remotePath + _newFilename};
+ qCDebug(lcCaseClashConflictSolver) << result << _remotePath << _newFilename;
+ return result;
+ }
+}
+
+QString CaseClashConflictSolver::remoteTargetFilePath() const
+{
+ if (_remotePath == QStringLiteral("/")) {
+ const auto result = QString{_targetFilePath.mid(_localPath.length())};
+ qCDebug(lcCaseClashConflictSolver) << result << _remotePath << _targetFilePath << _localPath;
+ return result;
+ } else {
+ const auto result = QString{_remotePath + _targetFilePath.mid(_localPath.length())};
+ qCDebug(lcCaseClashConflictSolver) << result << _remotePath << _targetFilePath << _localPath;
+ return result;
+ }
+}
+
+void CaseClashConflictSolver::onCheckIfAllowedToRenameComplete(const QVariantMap &values, QNetworkReply *reply)
+{
+ constexpr auto CONTENT_NOT_FOUND_ERROR = 404;
+
+ const auto isAllowedToRename = [](const RemotePermissions remotePermissions) {
+ return remotePermissions.hasPermission(remotePermissions.CanRename)
+ && remotePermissions.hasPermission(remotePermissions.CanMove);
+ };
+
+ if (values.contains("permissions") && !isAllowedToRename(RemotePermissions::fromServerString(values["permissions"].toString()))) {
+ _allowedToRename = false;
+ emit allowedToRenameChanged();
+ _errorString = tr("You don't have the permission to rename this file. Please ask the author of the file to rename it.");
+ emit errorStringChanged();
+
+ return;
+ } else if (reply) {
+ if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != CONTENT_NOT_FOUND_ERROR) {
+ _allowedToRename = false;
+ emit allowedToRenameChanged();
+ _errorString = tr("Failed to fetch permissions with error %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
+ emit errorStringChanged();
+
+ return;
+ }
+ }
+
+ _allowedToRename = true;
+ emit allowedToRenameChanged();
+
+ const auto filePathFileInfo = QFileInfo(_newFilename);
+ const auto fileName = filePathFileInfo.fileName();
+ processLeadingOrTrailingSpacesError(fileName);
+}
+
+void CaseClashConflictSolver::processLeadingOrTrailingSpacesError(const QString &fileName)
+{
+ const auto hasLeadingSpaces = fileName.startsWith(QLatin1Char(' '));
+ const auto hasTrailingSpaces = fileName.endsWith(QLatin1Char(' '));
+
+ if (hasLeadingSpaces || hasTrailingSpaces) {
+ if (hasLeadingSpaces && hasTrailingSpaces) {
+ _errorString = tr("Filename contains leading and trailing spaces.");
+ emit errorStringChanged();
+ }
+ else if (hasLeadingSpaces) {
+ _errorString = tr("Filename contains leading spaces.");
+ emit errorStringChanged();
+ } else if (hasTrailingSpaces) {
+ _errorString = tr("Filename contains trailing spaces.");
+ emit errorStringChanged();
+ }
+
+ _allowedToRename = false;
+ emit allowedToRenameChanged();
+
+ return;
+ }
+
+ _allowedToRename = true;
+ emit allowedToRenameChanged();
+}
+
+void CaseClashConflictSolver::checkIfAllowedToRename()
+{
+ const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(remoteTargetFilePath()));
+ propfindJob->setProperties({ "http://owncloud.org/ns:permissions" });
+ connect(propfindJob, &PropfindJob::result, this, &CaseClashConflictSolver::onPropfindPermissionSuccess);
+ connect(propfindJob, &PropfindJob::finishedWithError, this, &CaseClashConflictSolver::onPropfindPermissionError);
+ propfindJob->start();
+}
--- /dev/null
+#ifndef CASECLASHCONFLICTSOLVER_H
+#define CASECLASHCONFLICTSOLVER_H
+
+#include <QObject>
+
+#include "accountfwd.h"
+#include "owncloudlib.h"
+
+class QNetworkReply;
+
+namespace OCC {
+
+class SyncJournalDb;
+
+class OWNCLOUDSYNC_EXPORT CaseClashConflictSolver : public QObject
+{
+ Q_OBJECT
+
+ Q_PROPERTY(bool allowedToRename READ allowedToRename NOTIFY allowedToRenameChanged)
+
+ Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
+
+public:
+ explicit CaseClashConflictSolver(const QString &targetFilePath,
+ const QString &conflictFilePath,
+ const QString &remotePath,
+ const QString &localPath,
+ AccountPtr account,
+ SyncJournalDb *journal,
+ QObject *parent = nullptr);
+
+ [[nodiscard]] bool allowedToRename() const;
+
+ [[nodiscard]] QString errorString() const;
+
+signals:
+ void allowedToRenameChanged();
+
+ void errorStringChanged();
+
+ void done();
+
+ void failed();
+
+public slots:
+ void solveConflict(const QString &newFilename);
+
+ void checkIfAllowedToRename();
+
+private slots:
+ void onRemoteDestinationFileAlreadyExists();
+
+ void onRemoteDestinationFileDoesNotExist();
+
+ void onPropfindPermissionSuccess(const QVariantMap &values);
+
+ void onPropfindPermissionError(QNetworkReply *reply);
+
+ void onRemoteSourceFileAlreadyExists();
+
+ void onRemoteSourceFileDoesNotExist();
+
+ void onMoveJobFinished();
+
+private:
+ [[nodiscard]] QString remoteNewFilename() const;
+
+ [[nodiscard]] QString remoteTargetFilePath() const;
+
+ void onCheckIfAllowedToRenameComplete(const QVariantMap &values, QNetworkReply *reply = nullptr);
+
+ void processLeadingOrTrailingSpacesError(const QString &fileName);
+
+ AccountPtr _account;
+
+ QString _targetFilePath;
+
+ QString _conflictFilePath;
+
+ QString _newFilename;
+
+ QString _remotePath;
+
+ QString _localPath;
+
+ QString _errorString;
+
+ SyncJournalDb *_journal = nullptr;
+
+ bool _allowedToRename = false;
+};
+
+}
+
+#endif // CASECLASHCONFLICTSOLVER_H
case CSYNC_FILE_EXCLUDE_CONFLICT:
item->_errorString = tr("Conflict: Server version downloaded, local copy renamed and not uploaded.");
item->_status = SyncFileItem::Conflict;
- break;
+ break;
+ case CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT:
+ item->_errorString = tr("Case Clash Conflict: Server file downloaded and renamed to avoid clash.");
+ item->_status = SyncFileItem::FileNameClash;
+ break;
case CSYNC_FILE_EXCLUDE_CANNOT_ENCODE:
item->_errorString = tr("The filename cannot be encoded on your file system.");
break;
item->_modtime = serverEntry.modtime;
item->_size = serverEntry.size;
+ auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByBasePath(item->_file);
+ if (conflictRecord.isValid() && QString::fromUtf8(conflictRecord.path).contains(QStringLiteral("(case clash from"))) {
+ qCInfo(lcDisco) << "should ignore" << item->_file << "has already a case clash conflict record" << conflictRecord.path;
+
+ item->_instruction = CSYNC_INSTRUCTION_IGNORE;
+
+ return;
+ }
+
auto postProcessServerNew = [=]() mutable {
if (item->isDirectory()) {
_pendingAsyncJobs++;
item->_type = localEntry.isDirectory ? ItemTypeDirectory : localEntry.isVirtualFile ? ItemTypeVirtualFile : ItemTypeFile;
_childModified = true;
+ if (!localEntry.caseClashConflictingName.isEmpty()) {
+ qCInfo(lcDisco) << item->_file << "case clash conflict" << localEntry.caseClashConflictingName;
+ item->_instruction = CSYNC_INSTRUCTION_CONFLICT;
+ }
+
+ auto conflictRecord = _discoveryData->_statedb->caseConflictRecordByBasePath(item->_file);
+ if (conflictRecord.isValid() && QString::fromUtf8(conflictRecord.path).contains(QStringLiteral("(case clash from"))) {
+ qCInfo(lcDisco) << "should ignore" << item->_file << "has already a case clash conflict record" << conflictRecord.path;
+
+ item->_instruction = CSYNC_INSTRUCTION_IGNORE;
+
+ return;
+ }
+
auto postProcessLocalNew = [item, localEntry, path, this]() {
// TODO: We may want to execute the same logic for non-VFS mode, as, moving/renaming the same folder by 2 or more clients at the same time is not possible in Web UI.
// Keeping it like this (for VFS files and folders only) just to fix a user issue.
{
/** FileName of the entry (this does not contains any directory or path, just the plain name */
QString name;
+ QString caseClashConflictingName;
time_t modtime = 0;
int64_t size = 0;
uint64_t inode = 0;
time_t FileSystem::getModTime(const QString &filename)
{
csync_file_stat_t stat;
- qint64 result = -1;
- if (csync_vio_local_stat(filename, &stat) != -1
- && (stat.modtime != 0)) {
+ time_t result = -1;
+ if (csync_vio_local_stat(filename, &stat) != -1 && (stat.modtime != 0)) {
result = stat.modtime;
} else {
result = Utility::qDateTimeToTime_t(QFileInfo(filename).lastModified());
}
bool FileSystem::verifyFileUnchanged(const QString &fileName,
- qint64 previousSize,
- time_t previousMtime)
+ qint64 previousSize,
+ time_t previousMtime)
{
- const qint64 actualSize = getSize(fileName);
- const time_t actualMtime = getModTime(fileName);
+ const auto actualSize = getSize(fileName);
+ const auto actualMtime = getModTime(fileName);
if ((actualSize != previousSize && actualMtime > 0) || (actualMtime != previousMtime && previousMtime > 0 && actualMtime > 0)) {
qCInfo(lcFileSystem) << "File" << fileName << "has changed:"
<< "size: " << previousSize << "<->" << actualSize
return true;
}
+OCC::Optional<QString> OwncloudPropagator::createCaseClashConflict(const SyncFileItemPtr &item, const QString &temporaryDownloadedFile)
+{
+ auto filename = QString{};
+
+ if (item->_type == ItemType::ItemTypeFile) {
+ filename = fullLocalPath(item->_file);
+ } else if (item->_type == ItemType::ItemTypeVirtualFileDownload) {
+ filename = fullLocalPath(item->_file + syncOptions()._vfs->fileSuffix());
+ }
+
+ const auto conflictModTime = FileSystem::getModTime(filename);
+ if (conflictModTime <= 0) {
+ return tr("Impossible to get modification time for file in conflict %1").arg(filename);
+ }
+
+ const auto conflictFileName = Utility::makeCaseClashConflictFileName(item->_file, Utility::qDateTimeFromTime_t(conflictModTime));
+ const auto conflictFilePath = fullLocalPath(conflictFileName);
+
+ emit touchedFile(filename);
+ emit touchedFile(conflictFilePath);
+
+ qCInfo(lcPropagator) << "rename from" << temporaryDownloadedFile << "to" << conflictFilePath;
+ if (QString renameError; !FileSystem::rename(temporaryDownloadedFile, conflictFilePath, &renameError)) {
+ // If the rename fails, don't replace it.
+
+ // If the file is locked, we want to retry this sync when it
+ // becomes available again.
+ if (FileSystem::isFileLocked(filename)) {
+ emit seenLockedFile(filename);
+ }
+
+ return renameError;
+ }
+ FileSystem::setFileHidden(conflictFilePath, false);
+ qCInfo(lcPropagator) << "Created case clash conflict file" << filename << "->" << conflictFilePath;
+
+ // Create a new conflict record. To get the base etag, we need to read it from the db.
+ auto conflictBasePath = item->_file.toUtf8();
+ if (!item->_renameTarget.isEmpty()) {
+ conflictBasePath = item->_renameTarget.toUtf8();
+ }
+ auto conflictRecord = ConflictRecord{conflictFileName.toUtf8(), {}, item->_previousModtime, {}, conflictBasePath};
+
+ SyncJournalFileRecord baseRecord;
+ if (_journal->getFileRecord(item->_originalFile, &baseRecord) && baseRecord.isValid()) {
+ conflictRecord.baseEtag = baseRecord._etag;
+ conflictRecord.baseFileId = baseRecord._fileId;
+ }
+
+ _journal->setCaseConflictRecord(conflictRecord);
+
+ // Need a new sync to detect the created copy of the conflicting file
+ _anotherSyncNeeded = true;
+
+ return {};
+}
+
QString OwncloudPropagator::adjustRenamedPath(const QString &original) const
{
return OCC::adjustRenamedPath(_renamedDirectories, original);
return _remoteFolder;
}
+void PropagateIgnoreJob::start()
+{
+ SyncFileItem::Status status = _item->_status;
+ if (status == SyncFileItem::NoStatus) {
+ if (_item->_instruction == CSYNC_INSTRUCTION_ERROR) {
+ status = SyncFileItem::NormalError;
+ } else {
+ status = SyncFileItem::FileIgnored;
+ ASSERT(_item->_instruction == CSYNC_INSTRUCTION_IGNORE);
+ }
+ } else if (status == SyncFileItem::FileNameClash) {
+ const auto conflictRecord = propagator()->_journal->caseConflictRecordByPath(_item->_file);
+ if (conflictRecord.isValid()) {
+ _item->_file = conflictRecord.initialBasePath;
+ }
+ }
+ done(status, _item->_errorString);
+}
+
}
: PropagateItemJob(propagator, item)
{
}
- void start() override
- {
- SyncFileItem::Status status = _item->_status;
- if (status == SyncFileItem::NoStatus) {
- if (_item->_instruction == CSYNC_INSTRUCTION_ERROR) {
- status = SyncFileItem::NormalError;
- } else {
- status = SyncFileItem::FileIgnored;
- ASSERT(_item->_instruction == CSYNC_INSTRUCTION_IGNORE);
- }
- }
- done(status, _item->_errorString);
- }
+ void start() override;
};
class PropagateUploadFileCommon;
bool createConflict(const SyncFileItemPtr &item,
PropagatorCompositeJob *composite, QString *error);
+ /** Handles a case clash conflict by renaming the file 'item'.
+ *
+ * Sets up conflict records.
+ *
+ * Returns true on success, false and error on error.
+ */
+ OCC::Optional<QString> createCaseClashConflict(const SyncFileItemPtr &item, const QString &temporaryDownloadedFile);
+
// Map original path (as in the DB) to target final path
QMap<QString, QString> _renamedDirectories;
[[nodiscard]] QString adjustRenamedPath(const QString &original) const;
}
case CSYNC_INSTRUCTION_CONFLICT:
return QCoreApplication::translate("progress", "Server version downloaded, copied changed local file into conflict file");
+ case CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT:
+ return QCoreApplication::translate("progress", "Server version downloaded, copied changed local file into case conflict conflict file");
case CSYNC_INSTRUCTION_REMOVE:
return QCoreApplication::translate("progress", "Deleted");
case CSYNC_INSTRUCTION_EVAL_RENAME:
{
switch (item._instruction) {
case CSYNC_INSTRUCTION_CONFLICT:
+ case CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT:
case CSYNC_INSTRUCTION_SYNC:
case CSYNC_INSTRUCTION_NEW:
case CSYNC_INSTRUCTION_TYPE_CHANGE:
qCWarning(lcPropagateDownload) << "ignored virtual file type of" << _item->_file;
_item->_type = ItemTypeFile;
}
- if (_item->_type == ItemTypeVirtualFile) {
- if (propagator()->localFileNameClash(_item->_file)) {
- done(SyncFileItem::FileNameClash, tr("File %1 cannot be downloaded because of a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
- return;
- }
-
+ if (_item->_type == ItemTypeVirtualFile && !propagator()->localFileNameClash(_item->_file)) {
qCDebug(lcPropagateDownload) << "creating virtual file" << _item->_file;
// do a klaas' case clash check.
if (propagator()->localFileNameClash(_item->_file)) {
return;
// do a klaas' case clash check.
- if (propagator()->localFileNameClash(_item->_file)) {
- done(SyncFileItem::FileNameClash, tr("File %1 cannot be downloaded because of a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
- return;
+ if (propagator()->localFileNameClash(_item->_file) && _item->_type != ItemTypeVirtualFile) {
+ _item->_instruction = CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
+ qCInfo(lcPropagateDownload) << "setting instruction to" << _item->_instruction << _item->_file;
+ } else if (propagator()->localFileNameClash(_item->_file)) {
+ _item->_instruction = CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
+ _item->_type = CSyncEnums::ItemTypeVirtualFileDownload;
+ qCInfo(lcPropagateDownload) << "setting instruction to" << _item->_instruction << _item->_file << "setting type to" << _item->_type;
+ auto fileName = _item->_file;
+ if (propagator()->syncOptions()._vfs->mode() == Vfs::WithSuffix) {
+ fileName.chop(propagator()->syncOptions()._vfs->fileSuffix().size());
+ _item->_file = fileName;
+ }
}
propagator()->reportProgress(*_item, 0);
void PropagateDownloadFile::downloadFinished()
{
ASSERT(!_tmpFile.isOpen());
- QString fn = propagator()->fullLocalPath(_item->_file);
-
- // In case of file name clash, report an error
- // This can happen if another parallel download saved a clashing file.
- if (propagator()->localFileNameClash(_item->_file)) {
- done(SyncFileItem::FileNameClash, tr("File %1 cannot be saved because of a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
- return;
- }
+ const auto filename = propagator()->fullLocalPath(_item->_file);
if (_item->_modtime <= 0) {
FileSystem::remove(_tmpFile.fileName());
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
}
- bool previousFileExists = FileSystem::fileExists(fn);
+ if (propagator()->localFileNameClash(_item->_file)) {
+ _item->_instruction = CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
+ qCInfo(lcPropagateDownload) << "setting instruction to" << _item->_instruction << _item->_file;
+ }
+
+ bool previousFileExists = FileSystem::fileExists(filename) && _item->_instruction != CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
if (previousFileExists) {
// Preserve the existing file permissions.
- QFileInfo existingFile(fn);
+ const auto existingFile = QFileInfo{filename};
if (existingFile.permissions() != _tmpFile.permissions()) {
_tmpFile.setPermissions(existingFile.permissions());
}
preserveGroupOwnership(_tmpFile.fileName(), existingFile);
// Make the file a hydrated placeholder if possible
- const auto result = propagator()->syncOptions()._vfs->convertToPlaceholder(_tmpFile.fileName(), *_item, fn);
+ const auto result = propagator()->syncOptions()._vfs->convertToPlaceholder(_tmpFile.fileName(), *_item, filename);
if (!result) {
done(SyncFileItem::NormalError, result.error());
return;
// Apply the remote permissions
FileSystem::setFileReadOnlyWeak(_tmpFile.fileName(), !_item->_remotePerm.isNull() && !_item->_remotePerm.hasPermission(RemotePermissions::CanWrite));
- bool isConflict = _item->_instruction == CSYNC_INSTRUCTION_CONFLICT
- && (QFileInfo(fn).isDir() || !FileSystem::fileEquals(fn, _tmpFile.fileName()));
+ const auto isConflict = (_item->_instruction == CSYNC_INSTRUCTION_CONFLICT
+ && (QFileInfo(filename).isDir() || !FileSystem::fileEquals(filename, _tmpFile.fileName()))) ||
+ _item->_instruction == CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
+
if (isConflict) {
- QString error;
- if (!propagator()->createConflict(_item, _associatedComposite, &error)) {
- done(SyncFileItem::SoftError, error);
+ if (_item->_instruction == CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT) {
+ qCInfo(lcPropagateDownload) << "downloading case clashed file" << _item->_file;
+ const auto caseClashConflictResult = propagator()->createCaseClashConflict(_item, _tmpFile.fileName());
+ if (caseClashConflictResult) {
+ done(SyncFileItem::SoftError, *caseClashConflictResult);
+ } else {
+ done(SyncFileItem::FileNameClash, tr("File %1 downloaded but it resulted in a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
+ }
return;
+ } else {
+ QString error;
+ if (!propagator()->createConflict(_item, _associatedComposite, &error)) {
+ done(SyncFileItem::SoftError, error);
+ } else {
+ previousFileExists = false;
+ }
}
- previousFileExists = false;
}
const auto vfs = propagator()->syncOptions()._vfs;
// the discovery phase and now.
const qint64 expectedSize = _item->_previousSize;
const time_t expectedMtime = _item->_previousModtime;
- if (!FileSystem::verifyFileUnchanged(fn, expectedSize, expectedMtime)) {
+ if (!FileSystem::verifyFileUnchanged(filename, expectedSize, expectedMtime)) {
propagator()->_anotherSyncNeeded = true;
done(SyncFileItem::SoftError, tr("File has changed since discovery"));
return;
}
QString error;
- emit propagator()->touchedFile(fn);
+ emit propagator()->touchedFile(filename);
// The fileChanged() check is done above to generate better error messages.
- if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), fn, &error)) {
- qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(fn);
+ if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) {
+ qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(filename);
// If the file is locked, we want to retry this sync when it
// becomes available again, otherwise try again directly
- if (FileSystem::isFileLocked(fn)) {
- emit propagator()->seenLockedFile(fn);
+ if (FileSystem::isFileLocked(filename)) {
+ emit propagator()->seenLockedFile(filename);
} else {
propagator()->_anotherSyncNeeded = true;
}
qCInfo(lcPropagateDownload()) << propagator()->account()->davUser() << propagator()->account()->davDisplayName() << propagator()->account()->displayName();
if (_item->_locked == SyncFileItem::LockStatus::LockedItem && (_item->_lockOwnerType != SyncFileItem::LockOwnerType::UserLock || _item->_lockOwnerId != propagator()->account()->davUser())) {
qCInfo(lcPropagateDownload()) << "file is locked: making it read only";
- FileSystem::setFileReadOnly(fn, true);
+ FileSystem::setFileReadOnly(filename, true);
}
- FileSystem::setFileHidden(fn, false);
+ FileSystem::setFileHidden(filename, false);
// Maybe we downloaded a newer version of the file than we thought we would...
// Get up to date information for the journal.
- _item->_size = FileSystem::getSize(fn);
+ _item->_size = FileSystem::getSize(filename);
// Maybe what we downloaded was a conflict file? If so, set a conflict record.
// (the data was prepared in slotGetFinished above)
if (Utility::fsCasePreserving() && propagator()->localFileNameClash(_item->_file)) {
qCWarning(lcPropagateLocalMkdir) << "New folder to create locally already exists with different case:" << _item->_file;
- done(SyncFileItem::FileNameClash, tr("Attention, possible case sensitivity clash with %1").arg(newDirStr));
+ done(SyncFileItem::FileNameClash, tr("Folder %1 cannot be created because of a local file or folder name clash!").arg(newDirStr));
return;
}
emit propagator()->touchedFile(newDirStr);
if (QString::compare(_item->_file, _item->_renameTarget, Qt::CaseInsensitive) != 0
&& propagator()->localFileNameClash(_item->_renameTarget)) {
- // Only use localFileNameClash for the destination if we know that the source was not
- // the one conflicting (renaming A.txt -> a.txt is OK)
-
- // Fixme: the file that is the reason for the clash could be named here,
- // it would have to come out the localFileNameClash function
- done(SyncFileItem::FileNameClash,
- tr("File %1 cannot be renamed to %2 because of a local file name clash")
- .arg(QDir::toNativeSeparators(_item->_file), QDir::toNativeSeparators(_item->_renameTarget)));
+
+ qCInfo(lcPropagateLocalRename) << "renaming a case clashed file" << _item->_file << _item->_renameTarget;
+ const auto caseClashConflictResult = propagator()->createCaseClashConflict(_item, existingFile);
+ if (caseClashConflictResult) {
+ done(SyncFileItem::SoftError, *caseClashConflictResult);
+ } else {
+ done(SyncFileItem::FileNameClash, tr("File %1 downloaded but it resulted in a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
+ }
return;
}
}
}
+void SyncEngine::caseClashConflictRecordMaintenance()
+{
+ // Remove stale conflict entries from the database
+ // by checking which files still exist and removing the
+ // missing ones.
+ const auto conflictRecordPaths = _journal->caseClashConflictRecordPaths();
+ for (const auto &path : conflictRecordPaths) {
+ const auto fsPath = _propagator->fullLocalPath(QString::fromUtf8(path));
+ if (!QFileInfo::exists(fsPath)) {
+ _journal->deleteCaseClashConflictByPathRecord(path);
+ }
+ }
+}
+
void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item)
{
}
conflictRecordMaintenance();
+ caseClashConflictRecordMaintenance();
_journal->deleteStaleFlagsEntries();
_journal->commit("All Finished.", false);
// Removes stale and adds missing conflict records after sync
void conflictRecordMaintenance();
+ // Removes stale and adds missing conflict records after sync
+ void caseClashConflictRecordMaintenance();
+
// cleanup and emit the finished signal
void finalize(bool success);
Q_ASSERT(!fileName.isEmpty());
fileInfo = remoteRootFileInfo.find(fileName);
if (!fileInfo) {
- qDebug() << "meh;";
+ qDebug() << "url: " << request.url() << " fileName: " << fileName
+ << " meh;";
}
Q_ASSERT_X(fileInfo, Q_FUNC_INFO, "Could not find file on the remote");
QMetaObject::invokeMethod(this, &FakeGetReply::respond, Qt::QueuedConnection);
emit finished();
return;
}
+ if (!fileInfo) {
+ setError(ContentNotFoundError, QStringLiteral("File Not Found"));
+ emit metaDataChanged();
+ emit finished();
+ return;
+ }
payload = fileInfo->contentChar;
size = fileInfo->size;
setHeader(QNetworkRequest::ContentLengthHeader, size);
void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
{
- foreach (const FileInfo &child, templateFi.children) {
+ for(const auto &child : templateFi.children) {
if (child.isDir) {
QDir subDir(dir);
dir.mkdir(child.name);
void FakeFolder::fromDisk(QDir &dir, FileInfo &templateFi)
{
- foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
+ for(const auto &diskChild : dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
if (diskChild.isDir()) {
QDir subDir = dir;
subDir.cd(diskChild.fileName());
E2eFileTransferTest() = default;
private:
- EndToEndTestHelper _helper;
- OCC::Folder *_testFolder;
private slots:
void initTestCase()
{
- QSignalSpy accountReady(&_helper, &EndToEndTestHelper::accountReady);
- _helper.startAccountConfig();
- QVERIFY(accountReady.wait(3000));
-
- const auto accountState = _helper.accountState();
- QSignalSpy accountConnected(accountState.data(), &OCC::AccountState::isConnectedChanged);
- QVERIFY(accountConnected.wait(30000));
-
- _testFolder = _helper.configureSyncFolder();
- QVERIFY(_testFolder);
+ qRegisterMetaType<OCC::SyncResult>("OCC::SyncResult");
}
void testSyncFolder()
{
- // Try the down-sync first
- QSignalSpy folderSyncFinished(_testFolder, &OCC::Folder::syncFinished);
- OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
- QVERIFY(folderSyncFinished.wait(3000));
-
- const auto testFolderPath = _testFolder->path();
- const QString expectedFilePath(testFolderPath + QStringLiteral("welcome.txt"));
- const QFile expectedFile(expectedFilePath);
- qDebug() << "Checking if expected file exists at:" << expectedFilePath;
- QVERIFY(expectedFile.exists());
-
- // Now write a file to test the upload
- const auto fileName = QStringLiteral("test_file.txt");
- const QString localFilePath(_testFolder->path() + fileName);
- QVERIFY(OCC::Utility::writeRandomFile(localFilePath));
-
- OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
- QVERIFY(folderSyncFinished.wait(3000));
- qDebug() << "First folder sync complete";
-
- const auto waitForServerToProcessTime = QTime::currentTime().addSecs(3);
- while (QTime::currentTime() < waitForServerToProcessTime) {
- QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
- }
-
- // Do a propfind to check for this file
- const QString remoteFilePath(_testFolder->remotePathTrailingSlash() + fileName);
- auto checkFileExistsJob = new OCC::PropfindJob(_helper.account(), remoteFilePath, this);
- QSignalSpy result(checkFileExistsJob, &OCC::PropfindJob::result);
-
- checkFileExistsJob->setProperties(QList<QByteArray>() << "getlastmodified");
- checkFileExistsJob->start();
- QVERIFY(result.wait(10000));
-
- // Now try to delete the file and check change is reflected
- QFile createdFile(localFilePath);
- QVERIFY(createdFile.exists());
- createdFile.remove();
-
- OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
- QVERIFY(folderSyncFinished.wait(3000));
-
- while (QTime::currentTime() < waitForServerToProcessTime) {
- QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
+ {
+ EndToEndTestHelper _helper;
+ OCC::Folder *_testFolder;
+
+ QSignalSpy accountReady(&_helper, &EndToEndTestHelper::accountReady);
+ _helper.startAccountConfig();
+ QVERIFY(accountReady.wait(3000));
+
+ const auto accountState = _helper.accountState();
+ QSignalSpy accountConnected(accountState.data(), &OCC::AccountState::isConnectedChanged);
+ QVERIFY(accountConnected.wait(30000));
+
+ _testFolder = _helper.configureSyncFolder();
+ QVERIFY(_testFolder);
+
+ // Try the down-sync first
+ QSignalSpy folderSyncFinished(_testFolder, &OCC::Folder::syncFinished);
+ OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
+ QVERIFY(folderSyncFinished.wait(3000));
+
+ const auto testFolderPath = _testFolder->path();
+ const QString expectedFilePath(testFolderPath + QStringLiteral("welcome.txt"));
+ const QFile expectedFile(expectedFilePath);
+ qDebug() << "Checking if expected file exists at:" << expectedFilePath;
+ QVERIFY(expectedFile.exists());
+
+ // Now write a file to test the upload
+ const auto fileName = QStringLiteral("test_file.txt");
+ const QString localFilePath(_testFolder->path() + fileName);
+ QVERIFY(OCC::Utility::writeRandomFile(localFilePath));
+
+ OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
+ QVERIFY(folderSyncFinished.wait(3000));
+ qDebug() << "First folder sync complete";
+
+ const auto waitForServerToProcessTime = QTime::currentTime().addSecs(3);
+ while (QTime::currentTime() < waitForServerToProcessTime) {
+ QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
+ }
+
+ // Do a propfind to check for this file
+ const QString remoteFilePath(_testFolder->remotePathTrailingSlash() + fileName);
+ auto checkFileExistsJob = new OCC::PropfindJob(_helper.account(), remoteFilePath, this);
+ QSignalSpy result(checkFileExistsJob, &OCC::PropfindJob::result);
+
+ checkFileExistsJob->setProperties(QList<QByteArray>() << "getlastmodified");
+ checkFileExistsJob->start();
+ QVERIFY(result.wait(10000));
+
+ // Now try to delete the file and check change is reflected
+ QFile createdFile(localFilePath);
+ QVERIFY(createdFile.exists());
+ createdFile.remove();
+
+ OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
+ QVERIFY(folderSyncFinished.wait(3000));
+
+ while (QTime::currentTime() < waitForServerToProcessTime) {
+ QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
+ }
+
+ auto checkFileDeletedJob = new OCC::PropfindJob(_helper.account(), remoteFilePath, this);
+ QSignalSpy error(checkFileDeletedJob, &OCC::PropfindJob::finishedWithError);
+
+ checkFileDeletedJob->setProperties(QList<QByteArray>() << "getlastmodified");
+ checkFileDeletedJob->start();
+
+ QVERIFY(error.wait(10000));
}
- auto checkFileDeletedJob = new OCC::PropfindJob(_helper.account(), remoteFilePath, this);
- QSignalSpy error(checkFileDeletedJob, &OCC::PropfindJob::finishedWithError);
-
- checkFileDeletedJob->setProperties(QList<QByteArray>() << "getlastmodified");
- checkFileDeletedJob->start();
-
- QVERIFY(error.wait(10000));
+ QTest::qWait(10000);
}
};
*
*/
-#include <QtTest>
#include "syncenginetestutils.h"
-#include <syncengine.h>
-#include <propagatorjobs.h>
+
+#include "syncengine.h"
+#include "propagatorjobs.h"
+#include "caseclashconflictsolver.h"
+
+#include <QtTest>
using namespace OCC;
-bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
+namespace {
+
+QStringList findCaseClashConflicts(const FileInfo &dir)
{
- if (auto item = spy.findItem(path)) {
- return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA;
+ QStringList conflicts;
+ for (const auto &item : dir.children) {
+ if (item.name.contains("(case clash from")) {
+ conflicts.append(item.path());
+ }
+ }
+ return conflicts;
+}
+
+bool expectConflict(FileInfo state, const QString path)
+{
+ PathComponents pathComponents(path);
+ auto base = state.find(pathComponents.parentDirComponents());
+ if (!base)
+ return false;
+ for (const auto &item : qAsConst(base->children)) {
+ if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(case clash from")) {
+ return true;
+ }
}
return false;
}
-bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
+bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
{
- auto item = spy.findItem(path);
- return item->_instruction == instr;
+ if (auto item = spy.findItem(path)) {
+ return item->_instruction != CSYNC_INSTRUCTION_NONE && item->_instruction != CSYNC_INSTRUCTION_UPDATE_METADATA;
+ }
+ return false;
}
bool itemDidCompleteSuccessfully(const ItemCompletedSpy &spy, const QString &path)
return -1;
}
+}
+
class TestSyncEngine : public QObject
{
Q_OBJECT
auto folderA = fakeFolder.currentLocalState().find("toDelete");
QCOMPARE(folderA, nullptr);
}
+
+ void testServer_caseClash_createConflict()
+ {
+ 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);
+ }
+
+ void testServer_subFolderCaseClash_createConflict()
+ {
+ constexpr auto testLowerCaseFile = "a/b/test";
+ constexpr auto testUpperCaseFile = "a/b/TEST";
+
+#if defined Q_OS_LINUX
+ constexpr auto shouldHaveCaseClashConflict = false;
+#else
+ constexpr auto shouldHaveCaseClashConflict = true;
+#endif
+
+ FakeFolder fakeFolder{ FileInfo{} };
+
+ fakeFolder.remoteModifier().mkdir("a");
+ fakeFolder.remoteModifier().mkdir("a/b");
+ fakeFolder.remoteModifier().insert("a/b/otherFile.txt");
+ fakeFolder.remoteModifier().insert(testLowerCaseFile);
+ fakeFolder.remoteModifier().insert(testUpperCaseFile);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ 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().find("a/b"));
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ }
+
+ void testServer_caseClash_createConflictOnMove()
+ {
+ constexpr auto testLowerCaseFile = "test";
+ constexpr auto testUpperCaseFile = "TEST2";
+ constexpr auto testUpperCaseFileAfterMove = "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(), 0);
+ const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile);
+ QCOMPARE(hasConflict, false);
+
+ fakeFolder.remoteModifier().rename(testUpperCaseFile, testUpperCaseFileAfterMove);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ const auto hasConflictAfterMove = expectConflict(fakeFolder.currentLocalState(), testUpperCaseFileAfterMove);
+ QCOMPARE(hasConflictAfterMove, shouldHaveCaseClashConflict ? true : false);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ }
+
+ void testServer_subFolderCaseClash_createConflictOnMove()
+ {
+ constexpr auto testLowerCaseFile = "a/b/test";
+ constexpr auto testUpperCaseFile = "a/b/TEST2";
+ constexpr auto testUpperCaseFileAfterMove = "a/b/TEST";
+
+#if defined Q_OS_LINUX
+ constexpr auto shouldHaveCaseClashConflict = false;
+#else
+ constexpr auto shouldHaveCaseClashConflict = true;
+#endif
+
+ FakeFolder fakeFolder{ FileInfo{} };
+
+ fakeFolder.remoteModifier().mkdir("a");
+ fakeFolder.remoteModifier().mkdir("a/b");
+ fakeFolder.remoteModifier().insert("a/b/otherFile.txt");
+ fakeFolder.remoteModifier().insert(testLowerCaseFile);
+ fakeFolder.remoteModifier().insert(testUpperCaseFile);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ QCOMPARE(conflicts.size(), 0);
+ const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile);
+ QCOMPARE(hasConflict, false);
+
+ fakeFolder.remoteModifier().rename(testUpperCaseFile, testUpperCaseFileAfterMove);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ const auto hasConflictAfterMove = expectConflict(fakeFolder.currentLocalState(), testUpperCaseFileAfterMove);
+ QCOMPARE(hasConflictAfterMove, shouldHaveCaseClashConflict ? true : false);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ }
+
+ void testServer_caseClash_createConflictAndSolveIt()
+ {
+ 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);
+
+ if (shouldHaveCaseClashConflict) {
+ const auto conflictFileName = QString{conflicts.constFirst()};
+ qDebug() << conflictFileName;
+ CaseClashConflictSolver conflictSolver(fakeFolder.localPath() + testLowerCaseFile,
+ fakeFolder.localPath() + conflictFileName,
+ QStringLiteral("/"),
+ fakeFolder.localPath(),
+ fakeFolder.account(),
+ &fakeFolder.syncJournal());
+
+ QSignalSpy conflictSolverDone(&conflictSolver, &CaseClashConflictSolver::done);
+ QSignalSpy conflictSolverFailed(&conflictSolver, &CaseClashConflictSolver::failed);
+
+ conflictSolver.solveConflict("test2");
+
+ QVERIFY(conflictSolverDone.wait());
+
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+ QCOMPARE(conflicts.size(), 0);
+ }
+ }
+
+ void testServer_subFolderCaseClash_createConflictAndSolveIt()
+ {
+ constexpr auto testLowerCaseFile = "a/b/test";
+ constexpr auto testUpperCaseFile = "a/b/TEST";
+
+#if defined Q_OS_LINUX
+ constexpr auto shouldHaveCaseClashConflict = false;
+#else
+ constexpr auto shouldHaveCaseClashConflict = true;
+#endif
+
+ FakeFolder fakeFolder{ FileInfo{} };
+
+ fakeFolder.remoteModifier().mkdir("a");
+ fakeFolder.remoteModifier().mkdir("a/b");
+ fakeFolder.remoteModifier().insert("a/b/otherFile.txt");
+ fakeFolder.remoteModifier().insert(testLowerCaseFile);
+ fakeFolder.remoteModifier().insert(testUpperCaseFile);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ 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().find("a/b"));
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+
+ if (shouldHaveCaseClashConflict) {
+ CaseClashConflictSolver conflictSolver(fakeFolder.localPath() + testLowerCaseFile,
+ fakeFolder.localPath() + conflicts.constFirst(),
+ QStringLiteral("/"),
+ fakeFolder.localPath(),
+ fakeFolder.account(),
+ &fakeFolder.syncJournal());
+
+ QSignalSpy conflictSolverDone(&conflictSolver, &CaseClashConflictSolver::done);
+ QSignalSpy conflictSolverFailed(&conflictSolver, &CaseClashConflictSolver::failed);
+
+ conflictSolver.solveConflict("a/b/test2");
+
+ QVERIFY(conflictSolverDone.wait());
+
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ QCOMPARE(conflicts.size(), 0);
+ }
+ }
};
QTEST_GUILESS_MAIN(TestSyncEngine)
using namespace OCC;
+namespace {
+
+QStringList findCaseClashConflicts(const FileInfo &dir)
+{
+ QStringList conflicts;
+ for (const auto &item : dir.children) {
+ if (item.name.contains("(case clash from")) {
+ conflicts.append(item.path());
+ }
+ }
+ return conflicts;
+}
+
+bool expectConflict(FileInfo state, const QString path)
+{
+ PathComponents pathComponents(path);
+ auto base = state.find(pathComponents.parentDirComponents());
+ if (!base)
+ return false;
+ for (const auto &item : qAsConst(base->children)) {
+ if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("(case clash from")) {
+ return true;
+ }
+ }
+ return false;
+}
+}
+
#define DVSUFFIX APPLICATION_DOTVIRTUALFILE_SUFFIX
bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
fakeFolder.execUntilBeforePropagation();
QCOMPARE(checkStatus(), SyncFileStatus::StatusError);
+
+ fakeFolder.execUntilFinished();
+ }
+
+ void testServer_caseClash_createConflict()
+ {
+ 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{}};
+ setupVfs(fakeFolder);
+
+ 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);
+ }
+
+ void testServer_subFolderCaseClash_createConflict()
+ {
+ constexpr auto testLowerCaseFile = "a/b/test";
+ constexpr auto testUpperCaseFile = "a/b/TEST";
+
+#if defined Q_OS_LINUX
+ constexpr auto shouldHaveCaseClashConflict = false;
+#else
+ constexpr auto shouldHaveCaseClashConflict = true;
+#endif
+
+ FakeFolder fakeFolder{ FileInfo{} };
+ setupVfs(fakeFolder);
+
+ fakeFolder.remoteModifier().mkdir("a");
+ fakeFolder.remoteModifier().mkdir("a/b");
+ fakeFolder.remoteModifier().insert("a/b/otherFile.txt");
+ fakeFolder.remoteModifier().insert(testLowerCaseFile);
+ fakeFolder.remoteModifier().insert(testUpperCaseFile);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ 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().find("a/b"));
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ }
+
+ void testServer_caseClash_createConflictOnMove()
+ {
+ constexpr auto testLowerCaseFile = "test";
+ constexpr auto testUpperCaseFile = "TEST2";
+ constexpr auto testUpperCaseFileAfterMove = "TEST";
+
+#if defined Q_OS_LINUX
+ constexpr auto shouldHaveCaseClashConflict = false;
+#else
+ constexpr auto shouldHaveCaseClashConflict = true;
+#endif
+
+ FakeFolder fakeFolder{ FileInfo{} };
+ setupVfs(fakeFolder);
+
+ 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(), 0);
+ const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile);
+ QCOMPARE(hasConflict, false);
+
+ fakeFolder.remoteModifier().rename(testUpperCaseFile, testUpperCaseFileAfterMove);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ const auto hasConflictAfterMove = expectConflict(fakeFolder.currentLocalState(), testUpperCaseFileAfterMove);
+ QCOMPARE(hasConflictAfterMove, shouldHaveCaseClashConflict ? true : false);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(fakeFolder.currentLocalState());
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ }
+
+ void testServer_subFolderCaseClash_createConflictOnMove()
+ {
+ constexpr auto testLowerCaseFile = "a/b/test";
+ constexpr auto testUpperCaseFile = "a/b/TEST2";
+ constexpr auto testUpperCaseFileAfterMove = "a/b/TEST";
+
+#if defined Q_OS_LINUX
+ constexpr auto shouldHaveCaseClashConflict = false;
+#else
+ constexpr auto shouldHaveCaseClashConflict = true;
+#endif
+
+ FakeFolder fakeFolder{ FileInfo{} };
+ setupVfs(fakeFolder);
+
+ fakeFolder.remoteModifier().mkdir("a");
+ fakeFolder.remoteModifier().mkdir("a/b");
+ fakeFolder.remoteModifier().insert("a/b/otherFile.txt");
+ fakeFolder.remoteModifier().insert(testLowerCaseFile);
+ fakeFolder.remoteModifier().insert(testUpperCaseFile);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ auto conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ QCOMPARE(conflicts.size(), 0);
+ const auto hasConflict = expectConflict(fakeFolder.currentLocalState(), testLowerCaseFile);
+ QCOMPARE(hasConflict, false);
+
+ fakeFolder.remoteModifier().rename(testUpperCaseFile, testUpperCaseFileAfterMove);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
+ const auto hasConflictAfterMove = expectConflict(fakeFolder.currentLocalState(), testUpperCaseFileAfterMove);
+ QCOMPARE(hasConflictAfterMove, shouldHaveCaseClashConflict ? true : false);
+
+ fakeFolder.syncEngine().setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::DatabaseAndFilesystem);
+ QVERIFY(fakeFolder.syncOnce());
+
+ conflicts = findCaseClashConflicts(*fakeFolder.currentLocalState().find("a/b"));
+ QCOMPARE(conflicts.size(), shouldHaveCaseClashConflict ? 1 : 0);
}
};