mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 23:28:14 +03:00
Merge pull request #5232 from nextcloud/feature/syncWithCaseClashNames
Feature/sync with case clash names
This commit is contained in:
commit
aa74448f75
37 changed files with 2096 additions and 218 deletions
|
@ -168,6 +168,9 @@ option(BUILD_LIBRARIES_ONLY "BUILD_LIBRARIES_ONLY" OFF)
|
|||
# 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.
|
||||
|
|
|
@ -91,6 +91,11 @@ public:
|
|||
DeleteKeyValueStoreQuery,
|
||||
GetConflictRecordQuery,
|
||||
SetConflictRecordQuery,
|
||||
GetCaseClashConflictRecordQuery,
|
||||
GetCaseClashConflictRecordByPathQuery,
|
||||
SetCaseClashConflictRecordQuery,
|
||||
DeleteCaseClashConflictRecordQuery,
|
||||
GetAllCaseClashConflictPathQuery,
|
||||
DeleteConflictRecordQuery,
|
||||
GetRawPinStateQuery,
|
||||
GetEffectivePinStateQuery,
|
||||
|
|
|
@ -519,6 +519,18 @@ bool SyncJournalDb::checkConnect()
|
|||
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),"
|
||||
|
@ -2201,6 +2213,101 @@ ConflictRecord SyncJournalDb::conflictRecord(const QByteArray &path)
|
|||
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);
|
||||
|
|
|
@ -249,6 +249,15 @@ public:
|
|||
/// Retrieve a conflict record by path of the file with the conflict tag
|
||||
ConflictRecord conflictRecord(const QByteArray &path);
|
||||
|
||||
/// 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);
|
||||
|
||||
/// 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);
|
||||
|
||||
|
@ -373,6 +382,13 @@ public:
|
|||
*/
|
||||
int autotestFailCounter = -1;
|
||||
|
||||
public slots:
|
||||
/// Store a new or updated record in the database
|
||||
void setCaseConflictRecord(const ConflictRecord &record);
|
||||
|
||||
/// Delete a case clash conflict record by path of the file with the conflict tag
|
||||
void deleteCaseClashConflictByPathRecord(const QString &path);
|
||||
|
||||
private:
|
||||
int getFileRecordCount();
|
||||
[[nodiscard]] bool updateDatabaseStructure();
|
||||
|
|
|
@ -624,35 +624,21 @@ QString Utility::makeConflictFileName(
|
|||
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;
|
||||
}
|
||||
|
@ -722,4 +708,28 @@ QString Utility::sanitizeForFileName(const QString &name)
|
|||
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)
|
||||
{
|
||||
const auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
|
||||
|
||||
return bname.contains(QStringLiteral("(case clash from"));
|
||||
}
|
||||
|
||||
} // namespace OCC
|
||||
|
|
|
@ -223,10 +223,13 @@ namespace Utility {
|
|||
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
|
||||
*
|
||||
|
|
|
@ -104,22 +104,23 @@ Q_ENUM_NS(csync_status_codes_e)
|
|||
* 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)
|
||||
|
|
|
@ -205,10 +205,14 @@ static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const QString &path, bool exclu
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ enum CSYNC_EXCLUDE_TYPE {
|
|||
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,
|
||||
|
|
|
@ -29,6 +29,7 @@ set(client_UI_SRCS
|
|||
accountsettings.ui
|
||||
conflictdialog.ui
|
||||
invalidfilenamedialog.ui
|
||||
caseclashfilenamedialog.ui
|
||||
foldercreationdialog.ui
|
||||
folderwizardsourcepage.ui
|
||||
folderwizardtargetpage.ui
|
||||
|
@ -73,6 +74,8 @@ set(client_SRCS
|
|||
application.cpp
|
||||
invalidfilenamedialog.h
|
||||
invalidfilenamedialog.cpp
|
||||
caseclashfilenamedialog.h
|
||||
caseclashfilenamedialog.cpp
|
||||
callstatechecker.h
|
||||
callstatechecker.cpp
|
||||
conflictdialog.h
|
||||
|
|
284
src/gui/caseclashfilenamedialog.cpp
Normal file
284
src/gui/caseclashfilenamedialog.cpp
Normal file
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* Copyright 2021 (c) Felix Weilbach <felix.weilbach@nextcloud.com>
|
||||
* Copyright 2022 (c) Matthieu Gallien <matthieu.gallien@nextcloud.com>
|
||||
* Copyright 2022 (c) Claudio Cambra <claudio.cambra@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(conflictFilePath))
|
||||
{
|
||||
Q_ASSERT(_account);
|
||||
Q_ASSERT(_folder);
|
||||
|
||||
const auto filePathFileInfo = QFileInfo(_filePath);
|
||||
const auto conflictFileName = filePathFileInfo.fileName();
|
||||
|
||||
_relativeFilePath = filePathFileInfo.path() + QStringLiteral("/");
|
||||
_relativeFilePath = _relativeFilePath.replace(folder->path(), QLatin1String());
|
||||
_relativeFilePath = _relativeFilePath.isEmpty() ? QString() : _relativeFilePath + QStringLiteral("/");
|
||||
|
||||
_originalFileName = _relativeFilePath + conflictFileName;
|
||||
|
||||
_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("%1 does not support equal file names with only letter casing differences.").arg(QSysInfo::prettyProductName()));
|
||||
_ui->filenameLineEdit->setText(conflictFileName);
|
||||
|
||||
const auto preexistingConflictingFile = caseClashConflictFile(_filePath);
|
||||
updateFileWidgetGroup(preexistingConflictingFile,
|
||||
tr("Open existing file"),
|
||||
_ui->localVersionFilename,
|
||||
_ui->localVersionLink,
|
||||
_ui->localVersionMtime,
|
||||
_ui->localVersionSize,
|
||||
_ui->localVersionButton);
|
||||
|
||||
updateFileWidgetGroup(conflictTaggedPath,
|
||||
tr("Open clashing file"),
|
||||
_ui->remoteVersionFilename,
|
||||
_ui->remoteVersionLink,
|
||||
_ui->remoteVersionMtime,
|
||||
_ui->remoteVersionSize,
|
||||
_ui->remoteVersionButton);
|
||||
// Display incoming conflict filename, not conflict-tagged filename
|
||||
_ui->remoteVersionFilename->setText(filePathFileInfo.fileName());
|
||||
|
||||
adjustSize();
|
||||
|
||||
connect(_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(_ui->localVersionButton, &QToolButton::clicked, this, [preexistingConflictingFile] {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(preexistingConflictingFile));
|
||||
});
|
||||
connect(_ui->remoteVersionButton, &QToolButton::clicked, this, [conflictTaggedPath] {
|
||||
QDesktopServices::openUrl(QUrl::fromLocalFile(conflictTaggedPath));
|
||||
});
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
83
src/gui/caseclashfilenamedialog.h
Normal file
83
src/gui/caseclashfilenamedialog.h
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright 2021 (c) Felix Weilbach <felix.weilbach@nextcloud.com>
|
||||
* Copyright 2022 (c) Matthieu Gallien <matthieu.gallien@nextcloud.com>
|
||||
* Copyright 2022 (c) Claudio Cambra <claudio.cambra@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;
|
||||
};
|
||||
}
|
340
src/gui/caseclashfilenamedialog.ui
Normal file
340
src/gui/caseclashfilenamedialog.ui
Normal file
|
@ -0,0 +1,340 @@
|
|||
<?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>451</width>
|
||||
<height>349</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>true</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>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Existing file</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="localVersionFilename">
|
||||
<property name="text">
|
||||
<string>fileA</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QToolButton" name="localVersionButton">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0,1">
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="localVersionMtime">
|
||||
<property name="text">
|
||||
<string>today</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="localVersionSize">
|
||||
<property name="text">
|
||||
<string>0 byte</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="localVersionLink">
|
||||
<property name="text">
|
||||
<string>Open existing file</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string>Case clashing file</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="remoteVersionFilename">
|
||||
<property name="text">
|
||||
<string>fileB</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<item>
|
||||
<widget class="QToolButton" name="remoteVersionButton">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="iconSize">
|
||||
<size>
|
||||
<width>64</width>
|
||||
<height>64</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_8" stretch="0,0,0,1">
|
||||
<item>
|
||||
<widget class="QLabel" name="remoteVersionMtime">
|
||||
<property name="text">
|
||||
<string>today</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="remoteVersionSize">
|
||||
<property name="text">
|
||||
<string>0 byte</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="remoteVersionLink">
|
||||
<property name="text">
|
||||
<string>Open clashing file</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Please enter a new name for the clashing 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>
|
|
@ -1264,6 +1264,15 @@ void Folder::acceptInvalidFileName(const QString &filePath)
|
|||
_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;
|
||||
|
|
|
@ -256,6 +256,8 @@ public:
|
|||
|
||||
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.
|
||||
|
|
|
@ -431,6 +431,6 @@ private:
|
|||
};
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::SharePtr);
|
||||
Q_DECLARE_METATYPE(OCC::SharePtr)
|
||||
|
||||
#endif // SHAREMANAGER_H
|
||||
|
|
|
@ -45,7 +45,7 @@ ItemDelegate {
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumHeight: Style.minActivityHeight
|
||||
|
||||
showDismissButton: model.links.length > 0
|
||||
showDismissButton: model.isDismissable
|
||||
|
||||
iconSize: root.iconSize
|
||||
|
||||
|
|
|
@ -12,6 +12,20 @@
|
|||
* for more details.
|
||||
*/
|
||||
|
||||
#include "activitylistmodel.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "accountstate.h"
|
||||
#include "accountmanager.h"
|
||||
#include "conflictdialog.h"
|
||||
#include "folderman.h"
|
||||
#include "owncloudgui.h"
|
||||
#include "guiutility.h"
|
||||
#include "invalidfilenamedialog.h"
|
||||
#include "caseclashfilenamedialog.h"
|
||||
#include "activitydata.h"
|
||||
#include "systray.h"
|
||||
|
||||
#include <QtCore>
|
||||
#include <QAbstractListModel>
|
||||
#include <QDesktopServices>
|
||||
|
@ -20,24 +34,6 @@
|
|||
#include <QJsonDocument>
|
||||
#include <qloggingcategory.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 "activitydata.h"
|
||||
#include "activitylistmodel.h"
|
||||
#include "systray.h"
|
||||
#include "tray/usermodel.h"
|
||||
|
||||
#include "theme.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
|
||||
|
@ -77,6 +73,8 @@ QHash<int, QByteArray> ActivityListModel::roleNames() const
|
|||
roles[PointInTimeRole] = "dateTime";
|
||||
roles[DisplayActions] = "displayActions";
|
||||
roles[ShowFileDetailsRole] = "showFileDetails";
|
||||
roles[ShareableRole] = "isShareable";
|
||||
roles[DismissableRole] = "isDismissable";
|
||||
roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity";
|
||||
roles[IsCurrentUserFileActivityRole] = "isCurrentUserFileActivity";
|
||||
roles[ThumbnailRole] = "thumbnail";
|
||||
|
@ -348,6 +346,12 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const
|
|||
_displayActions &&
|
||||
a._fileAction != "file_deleted" &&
|
||||
a._syncFileItemStatus != SyncFileItem::FileIgnored;
|
||||
case DismissableRole:
|
||||
// Do not allow dismissal of things requiring user input regarding syncing
|
||||
return !a._links.isEmpty() &&
|
||||
a._syncFileItemStatus != SyncFileItem::FileNameClash &&
|
||||
a._syncFileItemStatus != SyncFileItem::Conflict &&
|
||||
a._syncFileItemStatus != SyncFileItem::FileNameInvalid;
|
||||
case IsCurrentUserFileActivityRole:
|
||||
return a._isCurrentUserFileActivity;
|
||||
case ThumbnailRole: {
|
||||
|
@ -548,7 +552,7 @@ void ActivityListModel::addEntriesToActivityList(const ActivityList &activityLis
|
|||
|
||||
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);
|
||||
}
|
||||
|
@ -665,6 +669,9 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
|
|||
_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();
|
||||
|
@ -684,22 +691,6 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
|
|||
_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()) {
|
||||
|
@ -710,6 +701,35 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
|
|||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
|
@ -729,6 +749,11 @@ void ActivityListModel::slotTriggerAction(const int activityIndex, const int act
|
|||
if (action._verb == "WEB") {
|
||||
Utility::openBrowser(QUrl(action._link));
|
||||
return;
|
||||
} else if (action._verb == "FIX_CONFLICT_LOCALLY" &&
|
||||
activity._type == Activity::SyncFileItemType &&
|
||||
(activity._syncFileItemStatus == SyncFileItem::Conflict || activity._syncFileItemStatus == SyncFileItem::FileNameClash)) {
|
||||
slotTriggerDefaultAction(activityIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
emit sendNotificationRequest(activity._accName, action._link, action._verb, activityIndex);
|
||||
|
@ -756,10 +781,6 @@ QVariantList ActivityListModel::convertLinksToActionButtons(const Activity &acti
|
|||
{
|
||||
QVariantList customList;
|
||||
|
||||
if (activity._links.size() == 1) {
|
||||
return customList;
|
||||
}
|
||||
|
||||
if (static_cast<quint32>(activity._links.size()) > maxActionButtons()) {
|
||||
customList << ActivityListModel::convertLinkToActionButton(activity._links.first());
|
||||
return customList;
|
||||
|
|
|
@ -28,6 +28,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcActivity)
|
|||
class AccountState;
|
||||
class ConflictDialog;
|
||||
class InvalidFilenameDialog;
|
||||
class CaseClashFilenameDialog;
|
||||
|
||||
/**
|
||||
* @brief The ActivityListModel
|
||||
|
@ -66,6 +67,8 @@ public:
|
|||
AccountConnectedRole,
|
||||
DisplayActions,
|
||||
ShowFileDetailsRole,
|
||||
ShareableRole,
|
||||
DismissableRole,
|
||||
IsCurrentUserFileActivityRole,
|
||||
ThumbnailRole,
|
||||
TalkNotificationConversationTokenRole,
|
||||
|
@ -157,6 +160,7 @@ private:
|
|||
void ingestActivities(const QJsonArray &activities);
|
||||
void appendMoreActivitiesAvailableEntry();
|
||||
void insertOrRemoveDummyFetchingActivity();
|
||||
void triggerCaseClashAction(Activity activity);
|
||||
|
||||
Activity _notificationIgnoredFiles;
|
||||
Activity _dummyFetchingActivities;
|
||||
|
@ -179,6 +183,7 @@ private:
|
|||
|
||||
QPointer<ConflictDialog> _currentConflictDialog;
|
||||
QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
|
||||
QPointer<CaseClashFilenameDialog> _currentCaseClashFilenameDialog;
|
||||
|
||||
AccountState *_accountState = nullptr;
|
||||
bool _currentlyFetching = false;
|
||||
|
|
|
@ -595,6 +595,16 @@ void User::slotAddErrorToGui(const QString &folderAlias, SyncFileItem::Status st
|
|||
activity._accName = folderInstance->accountState()->account()->displayName();
|
||||
activity._folder = folderAlias;
|
||||
|
||||
if (status == SyncFileItem::Conflict || status == SyncFileItem::FileNameClash) {
|
||||
ActivityLink buttonActivityLink;
|
||||
buttonActivityLink._label = tr("Resolve conflict");
|
||||
buttonActivityLink._link = activity._link.toString();
|
||||
buttonActivityLink._verb = "FIX_CONFLICT_LOCALLY";
|
||||
buttonActivityLink._primary = true;
|
||||
|
||||
activity._links = {buttonActivityLink};
|
||||
}
|
||||
|
||||
// Error notifications don't have ids by themselves so we will create one for it
|
||||
activity._id = -static_cast<int>(qHash(activity._subject + activity._message));
|
||||
|
||||
|
@ -708,6 +718,14 @@ void User::processCompletedSyncItem(const Folder *folder, const SyncFileItemPtr
|
|||
// add 'protocol error' to activity list
|
||||
if (item->_status == SyncFileItem::Status::FileNameInvalid) {
|
||||
showDesktopNotification(item->_file, activity._subject, activity._id);
|
||||
} else if (item->_status == SyncFileItem::Conflict || item->_status == SyncFileItem::FileNameClash) {
|
||||
ActivityLink buttonActivityLink;
|
||||
buttonActivityLink._label = tr("Resolve conflict");
|
||||
buttonActivityLink._link = activity._link.toString();
|
||||
buttonActivityLink._verb = "FIX_CONFLICT_LOCALLY";
|
||||
buttonActivityLink._primary = true;
|
||||
|
||||
activity._links = {buttonActivityLink};
|
||||
}
|
||||
_activityModel->addErrorToActivityList(activity);
|
||||
}
|
||||
|
|
|
@ -121,6 +121,8 @@ set(libsync_SRCS
|
|||
creds/credentialscommon.cpp
|
||||
creds/keychainchunk.h
|
||||
creds/keychainchunk.cpp
|
||||
caseclashconflictsolver.h
|
||||
caseclashconflictsolver.cpp
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
|
|
227
src/libsync/caseclashconflictsolver.cpp
Normal file
227
src/libsync/caseclashconflictsolver.cpp
Normal file
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* Copyright 2021 (c) Felix Weilbach <felix.weilbach@nextcloud.com>
|
||||
* Copyright 2022 (c) Matthieu Gallien <matthieu.gallien@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 "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())};
|
||||
return result;
|
||||
} else {
|
||||
const auto result = QString{_remotePath + _targetFilePath.mid(_localPath.length())};
|
||||
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 && 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();
|
||||
}
|
107
src/libsync/caseclashconflictsolver.h
Normal file
107
src/libsync/caseclashconflictsolver.h
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright 2021 (c) Felix Weilbach <felix.weilbach@nextcloud.com>
|
||||
* Copyright 2022 (c) Matthieu Gallien <matthieu.gallien@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 <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;
|
||||
};
|
||||
|
||||
}
|
|
@ -372,7 +372,11 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent
|
|||
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;
|
||||
|
@ -689,6 +693,15 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
|
|||
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++;
|
||||
|
@ -1120,6 +1133,20 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
|||
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.
|
||||
|
|
|
@ -88,6 +88,7 @@ struct LocalInfo
|
|||
{
|
||||
/** 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;
|
||||
|
|
|
@ -58,9 +58,8 @@ bool FileSystem::fileEquals(const QString &fn1, const QString &fn2)
|
|||
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());
|
||||
|
@ -93,11 +92,11 @@ bool FileSystem::fileChanged(const QString &fileName,
|
|||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -913,6 +913,63 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item,
|
|||
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);
|
||||
|
@ -1473,4 +1530,23 @@ QString OwncloudPropagator::remotePath() const
|
|||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -401,19 +401,7 @@ public:
|
|||
: 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;
|
||||
|
@ -586,6 +574,14 @@ public:
|
|||
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;
|
||||
|
|
|
@ -41,6 +41,8 @@ QString Progress::asResultString(const SyncFileItem &item)
|
|||
}
|
||||
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:
|
||||
|
@ -65,6 +67,7 @@ QString Progress::asActionString(const SyncFileItem &item)
|
|||
{
|
||||
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:
|
||||
|
|
|
@ -526,12 +526,7 @@ void PropagateDownloadFile::startAfterIsEncryptedIsChecked()
|
|||
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)) {
|
||||
|
@ -632,9 +627,18 @@ void PropagateDownloadFile::startDownload()
|
|||
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);
|
||||
|
@ -1147,14 +1151,7 @@ void PropagateDownloadFile::finalizeDownload()
|
|||
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());
|
||||
|
@ -1179,17 +1176,22 @@ void PropagateDownloadFile::downloadFinished()
|
|||
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;
|
||||
}
|
||||
|
||||
auto 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;
|
||||
|
@ -1199,15 +1201,28 @@ void PropagateDownloadFile::downloadFinished()
|
|||
// 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;
|
||||
|
@ -1223,7 +1238,7 @@ void PropagateDownloadFile::downloadFinished()
|
|||
// 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;
|
||||
|
@ -1231,14 +1246,14 @@ void PropagateDownloadFile::downloadFinished()
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -1250,14 +1265,14 @@ void PropagateDownloadFile::downloadFinished()
|
|||
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)
|
||||
|
|
|
@ -178,7 +178,7 @@ void PropagateLocalMkdir::startLocalMkdir()
|
|||
|
||||
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);
|
||||
|
@ -245,14 +245,14 @@ void PropagateLocalRename::start()
|
|||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -317,6 +317,20 @@ void SyncEngine::conflictRecordMaintenance()
|
|||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -906,6 +920,7 @@ void SyncEngine::slotPropagationFinished(bool success)
|
|||
}
|
||||
|
||||
conflictRecordMaintenance();
|
||||
caseClashConflictRecordMaintenance();
|
||||
|
||||
_journal->deleteStaleFlagsEntries();
|
||||
_journal->commit("All Finished.", false);
|
||||
|
|
|
@ -283,6 +283,9 @@ private:
|
|||
// 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);
|
||||
|
||||
|
|
|
@ -655,7 +655,8 @@ FakeGetReply::FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::
|
|||
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);
|
||||
|
@ -669,6 +670,12 @@ void FakeGetReply::respond()
|
|||
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);
|
||||
|
@ -1190,7 +1197,7 @@ void FakeFolder::execUntilItemCompleted(const QString &relativePath)
|
|||
|
||||
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);
|
||||
|
@ -1208,7 +1215,7 @@ void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
|
|||
|
||||
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());
|
||||
|
|
|
@ -30,79 +30,86 @@ public:
|
|||
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));
|
||||
{
|
||||
EndToEndTestHelper _helper;
|
||||
OCC::Folder *_testFolder;
|
||||
|
||||
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());
|
||||
QSignalSpy accountReady(&_helper, &EndToEndTestHelper::accountReady);
|
||||
_helper.startAccountConfig();
|
||||
QVERIFY(accountReady.wait(3000));
|
||||
|
||||
// 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));
|
||||
const auto accountState = _helper.accountState();
|
||||
QSignalSpy accountConnected(accountState.data(), &OCC::AccountState::isConnectedChanged);
|
||||
QVERIFY(accountConnected.wait(30000));
|
||||
|
||||
OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
|
||||
QVERIFY(folderSyncFinished.wait(3000));
|
||||
qDebug() << "First folder sync complete";
|
||||
_testFolder = _helper.configureSyncFolder();
|
||||
QVERIFY(_testFolder);
|
||||
|
||||
const auto waitForServerToProcessTime = QTime::currentTime().addSecs(3);
|
||||
while (QTime::currentTime() < waitForServerToProcessTime) {
|
||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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));
|
||||
QTest::qWait(10000);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -5,13 +5,43 @@
|
|||
*
|
||||
*/
|
||||
|
||||
#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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
|
||||
{
|
||||
if (auto item = spy.findItem(path)) {
|
||||
|
@ -20,12 +50,6 @@ bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
|
||||
{
|
||||
auto item = spy.findItem(path);
|
||||
return item->_instruction == instr;
|
||||
}
|
||||
|
||||
bool itemDidCompleteSuccessfully(const ItemCompletedSpy &spy, const QString &path)
|
||||
{
|
||||
if (auto item = spy.findItem(path)) {
|
||||
|
@ -54,6 +78,8 @@ int itemSuccessfullyCompletedGetRank(const ItemCompletedSpy &spy, const QString
|
|||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class TestSyncEngine : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -1307,6 +1333,270 @@ private slots:
|
|||
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)
|
||||
|
|
|
@ -13,6 +13,34 @@
|
|||
|
||||
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)
|
||||
|
@ -1691,6 +1719,166 @@ private slots:
|
|||
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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue