mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-28 11:48:56 +03:00
handle case clash conflicts in a similar way to content conflicts
introduce a new type of conflict for case clash filename conflicts add proper handling including a new utility class to solve them and a new dialog for the user to pick a fix Signed-off-by: Matthieu Gallien <matthieu.gallien@nextcloud.com>
This commit is contained in:
parent
5c42da4de5
commit
602b8db5e2
35 changed files with 1791 additions and 213 deletions
|
@ -168,6 +168,9 @@ option(BUILD_LIBRARIES_ONLY "BUILD_LIBRARIES_ONLY" OFF)
|
||||||
# build the GUI component, when disabled only nextcloudcmd is built
|
# build the GUI component, when disabled only nextcloudcmd is built
|
||||||
option(BUILD_GUI "BUILD_GUI" ON)
|
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
|
# 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
|
# 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.
|
# triggers a bug on the server, you want the file to be blacklisted.
|
||||||
|
|
|
@ -91,6 +91,11 @@ public:
|
||||||
DeleteKeyValueStoreQuery,
|
DeleteKeyValueStoreQuery,
|
||||||
GetConflictRecordQuery,
|
GetConflictRecordQuery,
|
||||||
SetConflictRecordQuery,
|
SetConflictRecordQuery,
|
||||||
|
GetCaseClashConflictRecordQuery,
|
||||||
|
GetCaseClashConflictRecordByPathQuery,
|
||||||
|
SetCaseClashConflictRecordQuery,
|
||||||
|
DeleteCaseClashConflictRecordQuery,
|
||||||
|
GetAllCaseClashConflictPathQuery,
|
||||||
DeleteConflictRecordQuery,
|
DeleteConflictRecordQuery,
|
||||||
GetRawPinStateQuery,
|
GetRawPinStateQuery,
|
||||||
GetEffectivePinStateQuery,
|
GetEffectivePinStateQuery,
|
||||||
|
|
|
@ -519,6 +519,18 @@ bool SyncJournalDb::checkConnect()
|
||||||
return sqlFail(QStringLiteral("Create table conflicts"), createQuery);
|
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("
|
createQuery.prepare("CREATE TABLE IF NOT EXISTS version("
|
||||||
"major INTEGER(8),"
|
"major INTEGER(8),"
|
||||||
"minor INTEGER(8),"
|
"minor INTEGER(8),"
|
||||||
|
@ -2201,6 +2213,101 @@ ConflictRecord SyncJournalDb::conflictRecord(const QByteArray &path)
|
||||||
return entry;
|
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)
|
void SyncJournalDb::deleteConflictRecord(const QByteArray &path)
|
||||||
{
|
{
|
||||||
QMutexLocker locker(&_mutex);
|
QMutexLocker locker(&_mutex);
|
||||||
|
|
|
@ -249,6 +249,21 @@ public:
|
||||||
/// Retrieve a conflict record by path of the file with the conflict tag
|
/// Retrieve a conflict record by path of the file with the conflict tag
|
||||||
ConflictRecord conflictRecord(const QByteArray &path);
|
ConflictRecord conflictRecord(const QByteArray &path);
|
||||||
|
|
||||||
|
/// Store a new or updated record in the database
|
||||||
|
void setCaseConflictRecord(const ConflictRecord &record);
|
||||||
|
|
||||||
|
/// Retrieve a conflict record by path of the file with the conflict tag
|
||||||
|
ConflictRecord caseConflictRecordByBasePath(const QString &baseNamePath);
|
||||||
|
|
||||||
|
/// Retrieve a conflict record by path of the file with the conflict tag
|
||||||
|
ConflictRecord caseConflictRecordByPath(const QString &path);
|
||||||
|
|
||||||
|
/// Delete a case clash conflict record by path of the file with the conflict tag
|
||||||
|
void deleteCaseClashConflictByPathRecord(const QString &path);
|
||||||
|
|
||||||
|
/// Return all paths of files with a conflict tag in the name and records in the db
|
||||||
|
QByteArrayList caseClashConflictRecordPaths();
|
||||||
|
|
||||||
/// Delete a conflict record by path of the file with the conflict tag
|
/// Delete a conflict record by path of the file with the conflict tag
|
||||||
void deleteConflictRecord(const QByteArray &path);
|
void deleteConflictRecord(const QByteArray &path);
|
||||||
|
|
||||||
|
|
|
@ -624,35 +624,21 @@ QString Utility::makeConflictFileName(
|
||||||
return conflictFileName;
|
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)
|
bool Utility::isConflictFile(const QString &name)
|
||||||
{
|
{
|
||||||
auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
|
auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
|
||||||
|
|
||||||
if (bname.contains(QStringLiteral("_conflict-")))
|
if (bname.contains(QStringLiteral("_conflict-"))) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (bname.contains(QStringLiteral("(conflicted copy")))
|
if (bname.contains(QStringLiteral("(conflicted copy"))) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCaseClashConflictFile(name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -722,4 +708,32 @@ QString Utility::sanitizeForFileName(const QString &name)
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString Utility::makeCaseClashConflictFileName(const QString &filename, const QDateTime &datetime)
|
||||||
|
{
|
||||||
|
auto conflictFileName(filename);
|
||||||
|
// Add conflict tag before the extension.
|
||||||
|
auto dotLocation = conflictFileName.lastIndexOf(QLatin1Char('.'));
|
||||||
|
// If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
|
||||||
|
if (dotLocation <= conflictFileName.lastIndexOf(QLatin1Char('/')) + 1) {
|
||||||
|
dotLocation = conflictFileName.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto conflictMarker = QStringLiteral(" (case clash from ");
|
||||||
|
conflictMarker += datetime.toString(QStringLiteral("yyyy-MM-dd hhmmss")) + QLatin1Char(')');
|
||||||
|
|
||||||
|
conflictFileName.insert(dotLocation, conflictMarker);
|
||||||
|
return conflictFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Utility::isCaseClashConflictFile(const QString &name)
|
||||||
|
{
|
||||||
|
auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);
|
||||||
|
|
||||||
|
if (bname.contains(QStringLiteral("(case clash from"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace OCC
|
} // namespace OCC
|
||||||
|
|
|
@ -223,10 +223,13 @@ namespace Utility {
|
||||||
OCSYNC_EXPORT QString makeConflictFileName(
|
OCSYNC_EXPORT QString makeConflictFileName(
|
||||||
const QString &fn, const QDateTime &dt, const QString &user);
|
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
|
/** 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 isConflictFile(const QString &name);
|
||||||
|
OCSYNC_EXPORT bool isCaseClashConflictFile(const QString &name);
|
||||||
|
|
||||||
/** Find the base name for a conflict file name, using name pattern only
|
/** 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.
|
* the csync state of a file.
|
||||||
*/
|
*/
|
||||||
enum SyncInstructions {
|
enum SyncInstructions {
|
||||||
CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (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_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_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
|
||||||
CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (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_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_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_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_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_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
|
||||||
CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
|
CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
|
||||||
CSYNC_INSTRUCTION_ERROR = 1 << 8,
|
CSYNC_INSTRUCTION_ERROR = 1 << 8,
|
||||||
CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
|
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
|
Used when the type of something changes from directory to file
|
||||||
or back. */
|
or back. */
|
||||||
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
|
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) */
|
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)
|
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;
|
return CSYNC_FILE_SILENTLY_EXCLUDED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (excludeConflictFiles) {
|
||||||
if (excludeConflictFiles && OCC::Utility::isConflictFile(path)) {
|
if (OCC::Utility::isCaseClashConflictFile(path)) {
|
||||||
return CSYNC_FILE_EXCLUDE_CONFLICT;
|
return CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT;
|
||||||
|
} else if (OCC::Utility::isConflictFile(path)) {
|
||||||
|
return CSYNC_FILE_EXCLUDE_CONFLICT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return CSYNC_NOT_EXCLUDED;
|
return CSYNC_NOT_EXCLUDED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ enum CSYNC_EXCLUDE_TYPE {
|
||||||
CSYNC_FILE_EXCLUDE_HIDDEN,
|
CSYNC_FILE_EXCLUDE_HIDDEN,
|
||||||
CSYNC_FILE_EXCLUDE_STAT_FAILED,
|
CSYNC_FILE_EXCLUDE_STAT_FAILED,
|
||||||
CSYNC_FILE_EXCLUDE_CONFLICT,
|
CSYNC_FILE_EXCLUDE_CONFLICT,
|
||||||
|
CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT,
|
||||||
CSYNC_FILE_EXCLUDE_CANNOT_ENCODE,
|
CSYNC_FILE_EXCLUDE_CANNOT_ENCODE,
|
||||||
CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED,
|
CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED,
|
||||||
CSYNC_FILE_EXCLUDE_LEADING_SPACE,
|
CSYNC_FILE_EXCLUDE_LEADING_SPACE,
|
||||||
|
|
|
@ -29,6 +29,7 @@ set(client_UI_SRCS
|
||||||
accountsettings.ui
|
accountsettings.ui
|
||||||
conflictdialog.ui
|
conflictdialog.ui
|
||||||
invalidfilenamedialog.ui
|
invalidfilenamedialog.ui
|
||||||
|
caseclashfilenamedialog.ui
|
||||||
foldercreationdialog.ui
|
foldercreationdialog.ui
|
||||||
folderwizardsourcepage.ui
|
folderwizardsourcepage.ui
|
||||||
folderwizardtargetpage.ui
|
folderwizardtargetpage.ui
|
||||||
|
@ -73,6 +74,8 @@ set(client_SRCS
|
||||||
application.cpp
|
application.cpp
|
||||||
invalidfilenamedialog.h
|
invalidfilenamedialog.h
|
||||||
invalidfilenamedialog.cpp
|
invalidfilenamedialog.cpp
|
||||||
|
caseclashfilenamedialog.h
|
||||||
|
caseclashfilenamedialog.cpp
|
||||||
callstatechecker.h
|
callstatechecker.h
|
||||||
callstatechecker.cpp
|
callstatechecker.cpp
|
||||||
conflictdialog.h
|
conflictdialog.h
|
||||||
|
|
253
src/gui/caseclashfilenamedialog.cpp
Normal file
253
src/gui/caseclashfilenamedialog.cpp
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "caseclashfilenamedialog.h"
|
||||||
|
#include "ui_caseclashfilenamedialog.h"
|
||||||
|
|
||||||
|
#include "account.h"
|
||||||
|
#include "folder.h"
|
||||||
|
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QDir>
|
||||||
|
#include <QAbstractButton>
|
||||||
|
#include <QDialogButtonBox>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QPushButton>
|
||||||
|
#include <QDirIterator>
|
||||||
|
#include <QDesktopServices>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr std::array<QChar, 9> caseClashIllegalCharacters({ '\\', '/', ':', '?', '*', '\"', '<', '>', '|' });
|
||||||
|
|
||||||
|
QVector<QChar> getCaseClashIllegalCharsFromString(const QString &string)
|
||||||
|
{
|
||||||
|
QVector<QChar> result;
|
||||||
|
for (const auto &character : string) {
|
||||||
|
if (std::find(caseClashIllegalCharacters.begin(), caseClashIllegalCharacters.end(), character)
|
||||||
|
!= caseClashIllegalCharacters.end()) {
|
||||||
|
result.push_back(character);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString caseClashIllegalCharacterListToString(const QVector<QChar> &illegalCharacters)
|
||||||
|
{
|
||||||
|
QString illegalCharactersString;
|
||||||
|
if (illegalCharacters.size() > 0) {
|
||||||
|
illegalCharactersString += illegalCharacters[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 1; i < illegalCharacters.count(); ++i) {
|
||||||
|
if (illegalCharactersString.contains(illegalCharacters[i])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
illegalCharactersString += " " + illegalCharacters[i];
|
||||||
|
}
|
||||||
|
return illegalCharactersString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcCaseClashConflictFialog, "nextcloud.sync.caseclash.dialog", QtInfoMsg)
|
||||||
|
|
||||||
|
CaseClashFilenameDialog::CaseClashFilenameDialog(AccountPtr account,
|
||||||
|
Folder *folder,
|
||||||
|
const QString &conflictFilePath,
|
||||||
|
const QString &conflictTaggedPath,
|
||||||
|
QWidget *parent)
|
||||||
|
: QDialog(parent)
|
||||||
|
, _ui(std::make_unique<Ui::CaseClashFilenameDialog>())
|
||||||
|
, _conflictSolver(conflictFilePath, conflictTaggedPath, folder->remotePath(), folder->path(), account, folder->journalDb())
|
||||||
|
, _account(account)
|
||||||
|
, _folder(folder)
|
||||||
|
, _filePath(std::move(filePath))
|
||||||
|
{
|
||||||
|
Q_ASSERT(_account);
|
||||||
|
Q_ASSERT(_folder);
|
||||||
|
|
||||||
|
const auto filePathFileInfo = QFileInfo(_filePath);
|
||||||
|
_relativeFilePath = filePathFileInfo.path() + QStringLiteral("/");
|
||||||
|
_relativeFilePath = _relativeFilePath.replace(folder->path(), QLatin1String());
|
||||||
|
_relativeFilePath = _relativeFilePath.isEmpty() ? QString() : _relativeFilePath + QStringLiteral("/");
|
||||||
|
|
||||||
|
_originalFileName = _relativeFilePath + filePathFileInfo.fileName();
|
||||||
|
|
||||||
|
_ui->setupUi(this);
|
||||||
|
_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||||
|
_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Rename file"));
|
||||||
|
|
||||||
|
_ui->descriptionLabel->setText(tr("The file \"%1\" could not be synced because of a case clash conflict with an existing file on this system.").arg(_originalFileName));
|
||||||
|
_ui->explanationLabel->setText(tr("The system you are using cannot have two file names with only casing differences."));
|
||||||
|
_ui->filenameLineEdit->setText(filePathFileInfo.fileName());
|
||||||
|
|
||||||
|
connect(_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||||
|
connect(_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||||
|
|
||||||
|
_ui->errorLabel->setText({}/*
|
||||||
|
tr("Checking rename permissions …")*/);
|
||||||
|
_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||||
|
_ui->filenameLineEdit->setEnabled(false);
|
||||||
|
|
||||||
|
connect(_ui->filenameLineEdit, &QLineEdit::textChanged, this,
|
||||||
|
&CaseClashFilenameDialog::onFilenameLineEditTextChanged);
|
||||||
|
|
||||||
|
connect(&_conflictSolver, &CaseClashConflictSolver::errorStringChanged, this, [this] () {
|
||||||
|
_ui->errorLabel->setText(_conflictSolver.errorString());
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(&_conflictSolver, &CaseClashConflictSolver::allowedToRenameChanged, this, [this] () {
|
||||||
|
_ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() &~ QDialogButtonBox::No);
|
||||||
|
if (_conflictSolver.allowedToRename()) {
|
||||||
|
_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(true);
|
||||||
|
_ui->filenameLineEdit->setEnabled(true);
|
||||||
|
_ui->filenameLineEdit->selectAll();
|
||||||
|
} else {
|
||||||
|
_ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() | QDialogButtonBox::No);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(&_conflictSolver, &CaseClashConflictSolver::failed, this, [this] () {
|
||||||
|
_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(&_conflictSolver, &CaseClashConflictSolver::done, this, [this] () {
|
||||||
|
Q_EMIT successfulRename(_folder->remotePath() + _newFilename);
|
||||||
|
QDialog::accept();
|
||||||
|
});
|
||||||
|
|
||||||
|
checkIfAllowedToRename();
|
||||||
|
}
|
||||||
|
|
||||||
|
CaseClashFilenameDialog::~CaseClashFilenameDialog() = default;
|
||||||
|
|
||||||
|
QString CaseClashFilenameDialog::caseClashConflictFile(const QString &conflictFilePath)
|
||||||
|
{
|
||||||
|
const auto filePathFileInfo = QFileInfo(conflictFilePath);
|
||||||
|
const auto conflictFileName = filePathFileInfo.fileName();
|
||||||
|
|
||||||
|
QDirIterator it(filePathFileInfo.path(), QDirIterator::Subdirectories);
|
||||||
|
|
||||||
|
while(it.hasNext()) {
|
||||||
|
const auto filePath = it.next();
|
||||||
|
qCDebug(lcCaseClashConflictFialog) << filePath;
|
||||||
|
QFileInfo fileInfo(filePath);
|
||||||
|
|
||||||
|
if(fileInfo.isDir()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto currentFileName = fileInfo.fileName();
|
||||||
|
if (currentFileName.compare(conflictFileName, Qt::CaseInsensitive) == 0 &&
|
||||||
|
currentFileName != conflictFileName) {
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashFilenameDialog::updateFileWidgetGroup(const QString &filePath,
|
||||||
|
const QString &linkText,
|
||||||
|
QLabel *filenameLabel,
|
||||||
|
QLabel *linkLabel,
|
||||||
|
QLabel *mtimeLabel,
|
||||||
|
QLabel *sizeLabel,
|
||||||
|
QToolButton *button) const
|
||||||
|
{
|
||||||
|
const auto filePathFileInfo = QFileInfo(filePath);
|
||||||
|
const auto filename = filePathFileInfo.fileName();
|
||||||
|
const auto lastModifiedString = filePathFileInfo.lastModified().toString();
|
||||||
|
const auto fileSizeString = locale().formattedDataSize(filePathFileInfo.size());
|
||||||
|
const auto fileUrl = QUrl::fromLocalFile(filePath).toString();
|
||||||
|
const auto linkString = QStringLiteral("<a href='%1'>%2</a>").arg(fileUrl, linkText);
|
||||||
|
const auto mime = QMimeDatabase().mimeTypeForFile(_filePath);
|
||||||
|
QIcon fileTypeIcon;
|
||||||
|
|
||||||
|
qCDebug(lcCaseClashConflictFialog) << filePath << filePathFileInfo.exists() << filename << lastModifiedString << fileSizeString << fileUrl << linkString << mime;
|
||||||
|
|
||||||
|
if (QIcon::hasThemeIcon(mime.iconName())) {
|
||||||
|
fileTypeIcon = QIcon::fromTheme(mime.iconName());
|
||||||
|
} else {
|
||||||
|
fileTypeIcon = QIcon(":/qt-project.org/styles/commonstyle/images/file-128.png");
|
||||||
|
}
|
||||||
|
|
||||||
|
filenameLabel->setText(filename);
|
||||||
|
mtimeLabel->setText(lastModifiedString);
|
||||||
|
sizeLabel->setText(fileSizeString);
|
||||||
|
linkLabel->setText(linkString);
|
||||||
|
button->setIcon(fileTypeIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashFilenameDialog::checkIfAllowedToRename()
|
||||||
|
{
|
||||||
|
_conflictSolver.checkIfAllowedToRename();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CaseClashFilenameDialog::processLeadingOrTrailingSpacesError(const QString &fileName)
|
||||||
|
{
|
||||||
|
const auto hasLeadingSpaces = fileName.startsWith(QLatin1Char(' '));
|
||||||
|
const auto hasTrailingSpaces = fileName.endsWith(QLatin1Char(' '));
|
||||||
|
|
||||||
|
_ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() &~ QDialogButtonBox::No);
|
||||||
|
|
||||||
|
if (hasLeadingSpaces || hasTrailingSpaces) {
|
||||||
|
if (hasLeadingSpaces && hasTrailingSpaces) {
|
||||||
|
_ui->errorLabel->setText(tr("Filename contains leading and trailing spaces."));
|
||||||
|
}
|
||||||
|
else if (hasLeadingSpaces) {
|
||||||
|
_ui->errorLabel->setText(tr("Filename contains leading spaces."));
|
||||||
|
} else if (hasTrailingSpaces) {
|
||||||
|
_ui->errorLabel->setText(tr("Filename contains trailing spaces."));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utility::isWindows()) {
|
||||||
|
_ui->buttonBox->setStandardButtons(_ui->buttonBox->standardButtons() | QDialogButtonBox::No);
|
||||||
|
_ui->buttonBox->button(QDialogButtonBox::No)->setText(tr("Use invalid name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashFilenameDialog::accept()
|
||||||
|
{
|
||||||
|
_newFilename = _relativeFilePath + _ui->filenameLineEdit->text().trimmed();
|
||||||
|
_conflictSolver.solveConflict(_newFilename);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashFilenameDialog::onFilenameLineEditTextChanged(const QString &text)
|
||||||
|
{
|
||||||
|
const auto isNewFileNameDifferent = text != _originalFileName;
|
||||||
|
const auto illegalContainedCharacters = getCaseClashIllegalCharsFromString(text);
|
||||||
|
const auto containsIllegalChars = !illegalContainedCharacters.empty() || text.endsWith(QLatin1Char('.'));
|
||||||
|
const auto isTextValid = isNewFileNameDifferent && !containsIllegalChars;
|
||||||
|
|
||||||
|
_ui->errorLabel->setText("");
|
||||||
|
|
||||||
|
if (!processLeadingOrTrailingSpacesError(text) && !isTextValid){
|
||||||
|
_ui->errorLabel->setText(tr("Filename contains illegal characters: %1").arg(caseClashIllegalCharacterListToString(illegalContainedCharacters)));
|
||||||
|
}
|
||||||
|
|
||||||
|
_ui->buttonBox->button(QDialogButtonBox::Ok)
|
||||||
|
->setEnabled(isTextValid);
|
||||||
|
}
|
||||||
|
}
|
81
src/gui/caseclashfilenamedialog.h
Normal file
81
src/gui/caseclashfilenamedialog.h
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) by Felix Weilbach <felix.weilbach@nextcloud.com>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation; either version 2 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful, but
|
||||||
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||||
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||||
|
* for more details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "accountfwd.h"
|
||||||
|
#include "caseclashconflictsolver.h"
|
||||||
|
|
||||||
|
#include <QDialog>
|
||||||
|
#include <QLabel>
|
||||||
|
#include <QToolButton>
|
||||||
|
#include <QNetworkReply>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
class Folder;
|
||||||
|
|
||||||
|
namespace Ui {
|
||||||
|
class CaseClashFilenameDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CaseClashFilenameDialog : public QDialog
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CaseClashFilenameDialog(AccountPtr account,
|
||||||
|
Folder *folder,
|
||||||
|
const QString &conflictFilePath,
|
||||||
|
const QString &conflictTaggedPath,
|
||||||
|
QWidget *parent = nullptr);
|
||||||
|
|
||||||
|
~CaseClashFilenameDialog() override;
|
||||||
|
|
||||||
|
void accept() override;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void successfulRename(const QString &filePath);
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void updateFileWidgetGroup(const QString &filePath,
|
||||||
|
const QString &linkText,
|
||||||
|
QLabel *filenameLabel,
|
||||||
|
QLabel *linkLabel,
|
||||||
|
QLabel *mtimeLabel,
|
||||||
|
QLabel *sizeLabel,
|
||||||
|
QToolButton *button) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Find the conflicting file path
|
||||||
|
static QString caseClashConflictFile(const QString &conflictFilePath);
|
||||||
|
|
||||||
|
void onFilenameLineEditTextChanged(const QString &text);
|
||||||
|
void checkIfAllowedToRename();
|
||||||
|
bool processLeadingOrTrailingSpacesError(const QString &fileName);
|
||||||
|
|
||||||
|
std::unique_ptr<Ui::CaseClashFilenameDialog> _ui;
|
||||||
|
CaseClashConflictSolver _conflictSolver;
|
||||||
|
AccountPtr _account;
|
||||||
|
Folder *_folder = nullptr;
|
||||||
|
|
||||||
|
QString _filePath;
|
||||||
|
QString _relativeFilePath;
|
||||||
|
QString _originalFileName;
|
||||||
|
QString _newFilename;
|
||||||
|
};
|
||||||
|
}
|
121
src/gui/caseclashfilenamedialog.ui
Normal file
121
src/gui/caseclashfilenamedialog.ui
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>OCC::CaseClashFilenameDialog</class>
|
||||||
|
<widget class="QDialog" name="OCC::CaseClashFilenameDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>411</width>
|
||||||
|
<height>192</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Case Clash Conflict</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<property name="sizeConstraint">
|
||||||
|
<enum>QLayout::SetDefaultConstraint</enum>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="descriptionLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>The file could not be synced because it generates a case clash conflict with an existing file on this system.</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="explanationLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>Error</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Please enter a new name for the remote file:</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>false</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="filenameLineEdit">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>New filename</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="errorLabel">
|
||||||
|
<property name="palette">
|
||||||
|
<palette>
|
||||||
|
<active>
|
||||||
|
<colorrole role="WindowText">
|
||||||
|
<brush brushstyle="SolidPattern">
|
||||||
|
<color alpha="200">
|
||||||
|
<red>255</red>
|
||||||
|
<green>0</green>
|
||||||
|
<blue>0</blue>
|
||||||
|
</color>
|
||||||
|
</brush>
|
||||||
|
</colorrole>
|
||||||
|
</active>
|
||||||
|
<inactive>
|
||||||
|
<colorrole role="WindowText">
|
||||||
|
<brush brushstyle="SolidPattern">
|
||||||
|
<color alpha="200">
|
||||||
|
<red>255</red>
|
||||||
|
<green>0</green>
|
||||||
|
<blue>0</blue>
|
||||||
|
</color>
|
||||||
|
</brush>
|
||||||
|
</colorrole>
|
||||||
|
</inactive>
|
||||||
|
<disabled>
|
||||||
|
<colorrole role="WindowText">
|
||||||
|
<brush brushstyle="SolidPattern">
|
||||||
|
<color alpha="115">
|
||||||
|
<red>255</red>
|
||||||
|
<green>255</green>
|
||||||
|
<blue>255</blue>
|
||||||
|
</color>
|
||||||
|
</brush>
|
||||||
|
</colorrole>
|
||||||
|
</disabled>
|
||||||
|
</palette>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
|
@ -1264,6 +1264,15 @@ void Folder::acceptInvalidFileName(const QString &filePath)
|
||||||
_engine->addAcceptedInvalidFileName(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)
|
void Folder::setSaveBackwardsCompatible(bool save)
|
||||||
{
|
{
|
||||||
_saveBackwardsCompatible = save;
|
_saveBackwardsCompatible = save;
|
||||||
|
|
|
@ -256,6 +256,8 @@ public:
|
||||||
|
|
||||||
void acceptInvalidFileName(const QString &filePath);
|
void acceptInvalidFileName(const QString &filePath);
|
||||||
|
|
||||||
|
void acceptCaseClashConflictFileName(const QString &filePath);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Migration: When this flag is true, this folder will save to
|
* Migration: When this flag is true, this folder will save to
|
||||||
* the backwards-compatible 'Folders' section in the config file.
|
* 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
|
#endif // SHAREMANAGER_H
|
||||||
|
|
|
@ -12,6 +12,20 @@
|
||||||
* for more details.
|
* 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 <QtCore>
|
||||||
#include <QAbstractListModel>
|
#include <QAbstractListModel>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
@ -20,24 +34,6 @@
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <qloggingcategory.h>
|
#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 {
|
namespace OCC {
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
|
Q_LOGGING_CATEGORY(lcActivity, "nextcloud.gui.activity", QtInfoMsg)
|
||||||
|
@ -548,7 +544,7 @@ void ActivityListModel::addEntriesToActivityList(const ActivityList &activityLis
|
||||||
|
|
||||||
void ActivityListModel::addErrorToActivityList(const Activity &activity)
|
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});
|
addEntriesToActivityList({activity});
|
||||||
_notificationErrorsLists.prepend(activity);
|
_notificationErrorsLists.prepend(activity);
|
||||||
}
|
}
|
||||||
|
@ -665,6 +661,9 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
|
||||||
_currentConflictDialog->open();
|
_currentConflictDialog->open();
|
||||||
ownCloudGui::raiseDialog(_currentConflictDialog);
|
ownCloudGui::raiseDialog(_currentConflictDialog);
|
||||||
return;
|
return;
|
||||||
|
} else if (activity._syncFileItemStatus == SyncFileItem::FileNameClash) {
|
||||||
|
triggerCaseClashAction(activity);
|
||||||
|
return;
|
||||||
} else if (activity._syncFileItemStatus == SyncFileItem::FileNameInvalid) {
|
} else if (activity._syncFileItemStatus == SyncFileItem::FileNameInvalid) {
|
||||||
if (!_currentInvalidFilenameDialog.isNull()) {
|
if (!_currentInvalidFilenameDialog.isNull()) {
|
||||||
_currentInvalidFilenameDialog->close();
|
_currentInvalidFilenameDialog->close();
|
||||||
|
@ -684,22 +683,6 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex)
|
||||||
_currentInvalidFilenameDialog->open();
|
_currentInvalidFilenameDialog->open();
|
||||||
ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
|
ownCloudGui::raiseDialog(_currentInvalidFilenameDialog);
|
||||||
return;
|
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()) {
|
if (!path.isEmpty()) {
|
||||||
|
@ -710,6 +693,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)
|
void ActivityListModel::slotTriggerAction(const int activityIndex, const int actionIndex)
|
||||||
{
|
{
|
||||||
if (activityIndex < 0 || activityIndex >= _finalList.size()) {
|
if (activityIndex < 0 || activityIndex >= _finalList.size()) {
|
||||||
|
|
|
@ -28,6 +28,7 @@ Q_DECLARE_LOGGING_CATEGORY(lcActivity)
|
||||||
class AccountState;
|
class AccountState;
|
||||||
class ConflictDialog;
|
class ConflictDialog;
|
||||||
class InvalidFilenameDialog;
|
class InvalidFilenameDialog;
|
||||||
|
class CaseClashFilenameDialog;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief The ActivityListModel
|
* @brief The ActivityListModel
|
||||||
|
@ -157,6 +158,7 @@ private:
|
||||||
void ingestActivities(const QJsonArray &activities);
|
void ingestActivities(const QJsonArray &activities);
|
||||||
void appendMoreActivitiesAvailableEntry();
|
void appendMoreActivitiesAvailableEntry();
|
||||||
void insertOrRemoveDummyFetchingActivity();
|
void insertOrRemoveDummyFetchingActivity();
|
||||||
|
void triggerCaseClashAction(Activity activity);
|
||||||
|
|
||||||
Activity _notificationIgnoredFiles;
|
Activity _notificationIgnoredFiles;
|
||||||
Activity _dummyFetchingActivities;
|
Activity _dummyFetchingActivities;
|
||||||
|
@ -179,6 +181,7 @@ private:
|
||||||
|
|
||||||
QPointer<ConflictDialog> _currentConflictDialog;
|
QPointer<ConflictDialog> _currentConflictDialog;
|
||||||
QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
|
QPointer<InvalidFilenameDialog> _currentInvalidFilenameDialog;
|
||||||
|
QPointer<CaseClashFilenameDialog> _currentCaseClashFilenameDialog;
|
||||||
|
|
||||||
AccountState *_accountState = nullptr;
|
AccountState *_accountState = nullptr;
|
||||||
bool _currentlyFetching = false;
|
bool _currentlyFetching = false;
|
||||||
|
|
|
@ -121,6 +121,8 @@ set(libsync_SRCS
|
||||||
creds/credentialscommon.cpp
|
creds/credentialscommon.cpp
|
||||||
creds/keychainchunk.h
|
creds/keychainchunk.h
|
||||||
creds/keychainchunk.cpp
|
creds/keychainchunk.cpp
|
||||||
|
caseclashconflictsolver.h
|
||||||
|
caseclashconflictsolver.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
|
|
217
src/libsync/caseclashconflictsolver.cpp
Normal file
217
src/libsync/caseclashconflictsolver.cpp
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
#include "caseclashconflictsolver.h"
|
||||||
|
|
||||||
|
#include "networkjobs.h"
|
||||||
|
#include "propagateremotemove.h"
|
||||||
|
#include "account.h"
|
||||||
|
#include "common/syncjournaldb.h"
|
||||||
|
#include "common/filesystembase.h"
|
||||||
|
|
||||||
|
#include <QDir>
|
||||||
|
#include <QFileInfo>
|
||||||
|
#include <QLoggingCategory>
|
||||||
|
|
||||||
|
using namespace OCC;
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(lcCaseClashConflictSolver, "nextcloud.sync.caseclash.solver", QtInfoMsg)
|
||||||
|
|
||||||
|
CaseClashConflictSolver::CaseClashConflictSolver(const QString &targetFilePath,
|
||||||
|
const QString &conflictFilePath,
|
||||||
|
const QString &remotePath,
|
||||||
|
const QString &localPath,
|
||||||
|
AccountPtr account,
|
||||||
|
SyncJournalDb *journal,
|
||||||
|
QObject *parent)
|
||||||
|
: QObject{parent}
|
||||||
|
, _account(account)
|
||||||
|
, _targetFilePath(targetFilePath)
|
||||||
|
, _conflictFilePath(conflictFilePath)
|
||||||
|
, _remotePath(remotePath)
|
||||||
|
, _localPath(localPath)
|
||||||
|
, _journal(journal)
|
||||||
|
{
|
||||||
|
#if !defined(QT_NO_DEBUG)
|
||||||
|
QFileInfo targetFileInfo(_targetFilePath);
|
||||||
|
Q_ASSERT(targetFileInfo.isAbsolute());
|
||||||
|
Q_ASSERT(QFileInfo::exists(_conflictFilePath));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CaseClashConflictSolver::allowedToRename() const
|
||||||
|
{
|
||||||
|
return _allowedToRename;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CaseClashConflictSolver::errorString() const
|
||||||
|
{
|
||||||
|
return _errorString;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::solveConflict(const QString &newFilename)
|
||||||
|
{
|
||||||
|
_newFilename = newFilename;
|
||||||
|
|
||||||
|
const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(remoteNewFilename()));
|
||||||
|
connect(propfindJob, &PropfindJob::result, this, &CaseClashConflictSolver::onRemoteDestinationFileAlreadyExists);
|
||||||
|
connect(propfindJob, &PropfindJob::finishedWithError, this, &CaseClashConflictSolver::onRemoteDestinationFileDoesNotExist);
|
||||||
|
propfindJob->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onRemoteDestinationFileAlreadyExists()
|
||||||
|
{
|
||||||
|
_allowedToRename = false;
|
||||||
|
emit allowedToRenameChanged();
|
||||||
|
_errorString = tr("Cannot rename file because a file with the same name does already exist on the server. Please pick another name.");
|
||||||
|
emit errorStringChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onRemoteDestinationFileDoesNotExist()
|
||||||
|
{
|
||||||
|
const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(remoteTargetFilePath()));
|
||||||
|
connect(propfindJob, &PropfindJob::result, this, &CaseClashConflictSolver::onRemoteSourceFileAlreadyExists);
|
||||||
|
connect(propfindJob, &PropfindJob::finishedWithError, this, &CaseClashConflictSolver::onRemoteSourceFileDoesNotExist);
|
||||||
|
propfindJob->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onPropfindPermissionSuccess(const QVariantMap &values)
|
||||||
|
{
|
||||||
|
onCheckIfAllowedToRenameComplete(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onPropfindPermissionError(QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
onCheckIfAllowedToRenameComplete({}, reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onRemoteSourceFileAlreadyExists()
|
||||||
|
{
|
||||||
|
const auto remoteSource = QDir::cleanPath(remoteTargetFilePath());
|
||||||
|
const auto remoteDestionation = QDir::cleanPath(_account->davUrl().path() + remoteNewFilename());
|
||||||
|
qCInfo(lcCaseClashConflictSolver) << "rename case clashing file from" << remoteSource << "to" << remoteDestionation;
|
||||||
|
const auto moveJob = new MoveJob(_account, remoteSource, remoteDestionation, this);
|
||||||
|
connect(moveJob, &MoveJob::finishedSignal, this, &CaseClashConflictSolver::onMoveJobFinished);
|
||||||
|
moveJob->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onRemoteSourceFileDoesNotExist()
|
||||||
|
{
|
||||||
|
Q_EMIT failed();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onMoveJobFinished()
|
||||||
|
{
|
||||||
|
const auto job = qobject_cast<MoveJob *>(sender());
|
||||||
|
const auto error = job->reply()->error();
|
||||||
|
|
||||||
|
if (error != QNetworkReply::NoError) {
|
||||||
|
_errorString = tr("Could not rename file. Please make sure you are connected to the server.");
|
||||||
|
emit errorStringChanged();
|
||||||
|
|
||||||
|
emit failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qCInfo(lcCaseClashConflictSolver) << "going to delete case clash conflict record" << _targetFilePath;
|
||||||
|
_journal->deleteCaseClashConflictByPathRecord(_targetFilePath);
|
||||||
|
|
||||||
|
qCInfo(lcCaseClashConflictSolver) << "going to delete" << _conflictFilePath;
|
||||||
|
FileSystem::remove(_conflictFilePath);
|
||||||
|
|
||||||
|
Q_EMIT done();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CaseClashConflictSolver::remoteNewFilename() const
|
||||||
|
{
|
||||||
|
if (_remotePath == QStringLiteral("/")) {
|
||||||
|
qCDebug(lcCaseClashConflictSolver) << _newFilename << _remotePath << _newFilename;
|
||||||
|
return _newFilename;
|
||||||
|
} else {
|
||||||
|
const auto result = QString{_remotePath + _newFilename};
|
||||||
|
qCDebug(lcCaseClashConflictSolver) << result << _remotePath << _newFilename;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QString CaseClashConflictSolver::remoteTargetFilePath() const
|
||||||
|
{
|
||||||
|
if (_remotePath == QStringLiteral("/")) {
|
||||||
|
const auto result = QString{_targetFilePath.mid(_localPath.length())};
|
||||||
|
qCDebug(lcCaseClashConflictSolver) << result << _remotePath << _targetFilePath << _localPath;
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
const auto result = QString{_remotePath + _targetFilePath.mid(_localPath.length())};
|
||||||
|
qCDebug(lcCaseClashConflictSolver) << result << _remotePath << _targetFilePath << _localPath;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::onCheckIfAllowedToRenameComplete(const QVariantMap &values, QNetworkReply *reply)
|
||||||
|
{
|
||||||
|
constexpr auto CONTENT_NOT_FOUND_ERROR = 404;
|
||||||
|
|
||||||
|
const auto isAllowedToRename = [](const RemotePermissions remotePermissions) {
|
||||||
|
return remotePermissions.hasPermission(remotePermissions.CanRename)
|
||||||
|
&& remotePermissions.hasPermission(remotePermissions.CanMove);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (values.contains("permissions") && !isAllowedToRename(RemotePermissions::fromServerString(values["permissions"].toString()))) {
|
||||||
|
_allowedToRename = false;
|
||||||
|
emit allowedToRenameChanged();
|
||||||
|
_errorString = tr("You don't have the permission to rename this file. Please ask the author of the file to rename it.");
|
||||||
|
emit errorStringChanged();
|
||||||
|
|
||||||
|
return;
|
||||||
|
} else if (reply) {
|
||||||
|
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != CONTENT_NOT_FOUND_ERROR) {
|
||||||
|
_allowedToRename = false;
|
||||||
|
emit allowedToRenameChanged();
|
||||||
|
_errorString = tr("Failed to fetch permissions with error %1").arg(reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
|
||||||
|
emit errorStringChanged();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_allowedToRename = true;
|
||||||
|
emit allowedToRenameChanged();
|
||||||
|
|
||||||
|
const auto filePathFileInfo = QFileInfo(_newFilename);
|
||||||
|
const auto fileName = filePathFileInfo.fileName();
|
||||||
|
processLeadingOrTrailingSpacesError(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::processLeadingOrTrailingSpacesError(const QString &fileName)
|
||||||
|
{
|
||||||
|
const auto hasLeadingSpaces = fileName.startsWith(QLatin1Char(' '));
|
||||||
|
const auto hasTrailingSpaces = fileName.endsWith(QLatin1Char(' '));
|
||||||
|
|
||||||
|
if (hasLeadingSpaces || hasTrailingSpaces) {
|
||||||
|
if (hasLeadingSpaces && hasTrailingSpaces) {
|
||||||
|
_errorString = tr("Filename contains leading and trailing spaces.");
|
||||||
|
emit errorStringChanged();
|
||||||
|
}
|
||||||
|
else if (hasLeadingSpaces) {
|
||||||
|
_errorString = tr("Filename contains leading spaces.");
|
||||||
|
emit errorStringChanged();
|
||||||
|
} else if (hasTrailingSpaces) {
|
||||||
|
_errorString = tr("Filename contains trailing spaces.");
|
||||||
|
emit errorStringChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
_allowedToRename = false;
|
||||||
|
emit allowedToRenameChanged();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_allowedToRename = true;
|
||||||
|
emit allowedToRenameChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CaseClashConflictSolver::checkIfAllowedToRename()
|
||||||
|
{
|
||||||
|
const auto propfindJob = new PropfindJob(_account, QDir::cleanPath(remoteTargetFilePath()));
|
||||||
|
propfindJob->setProperties({ "http://owncloud.org/ns:permissions" });
|
||||||
|
connect(propfindJob, &PropfindJob::result, this, &CaseClashConflictSolver::onPropfindPermissionSuccess);
|
||||||
|
connect(propfindJob, &PropfindJob::finishedWithError, this, &CaseClashConflictSolver::onPropfindPermissionError);
|
||||||
|
propfindJob->start();
|
||||||
|
}
|
95
src/libsync/caseclashconflictsolver.h
Normal file
95
src/libsync/caseclashconflictsolver.h
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#ifndef CASECLASHCONFLICTSOLVER_H
|
||||||
|
#define CASECLASHCONFLICTSOLVER_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
#include "accountfwd.h"
|
||||||
|
#include "owncloudlib.h"
|
||||||
|
|
||||||
|
class QNetworkReply;
|
||||||
|
|
||||||
|
namespace OCC {
|
||||||
|
|
||||||
|
class SyncJournalDb;
|
||||||
|
|
||||||
|
class OWNCLOUDSYNC_EXPORT CaseClashConflictSolver : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(bool allowedToRename READ allowedToRename NOTIFY allowedToRenameChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(QString errorString READ errorString NOTIFY errorStringChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit CaseClashConflictSolver(const QString &targetFilePath,
|
||||||
|
const QString &conflictFilePath,
|
||||||
|
const QString &remotePath,
|
||||||
|
const QString &localPath,
|
||||||
|
AccountPtr account,
|
||||||
|
SyncJournalDb *journal,
|
||||||
|
QObject *parent = nullptr);
|
||||||
|
|
||||||
|
[[nodiscard]] bool allowedToRename() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QString errorString() const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void allowedToRenameChanged();
|
||||||
|
|
||||||
|
void errorStringChanged();
|
||||||
|
|
||||||
|
void done();
|
||||||
|
|
||||||
|
void failed();
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void solveConflict(const QString &newFilename);
|
||||||
|
|
||||||
|
void checkIfAllowedToRename();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onRemoteDestinationFileAlreadyExists();
|
||||||
|
|
||||||
|
void onRemoteDestinationFileDoesNotExist();
|
||||||
|
|
||||||
|
void onPropfindPermissionSuccess(const QVariantMap &values);
|
||||||
|
|
||||||
|
void onPropfindPermissionError(QNetworkReply *reply);
|
||||||
|
|
||||||
|
void onRemoteSourceFileAlreadyExists();
|
||||||
|
|
||||||
|
void onRemoteSourceFileDoesNotExist();
|
||||||
|
|
||||||
|
void onMoveJobFinished();
|
||||||
|
|
||||||
|
private:
|
||||||
|
[[nodiscard]] QString remoteNewFilename() const;
|
||||||
|
|
||||||
|
[[nodiscard]] QString remoteTargetFilePath() const;
|
||||||
|
|
||||||
|
void onCheckIfAllowedToRenameComplete(const QVariantMap &values, QNetworkReply *reply = nullptr);
|
||||||
|
|
||||||
|
void processLeadingOrTrailingSpacesError(const QString &fileName);
|
||||||
|
|
||||||
|
AccountPtr _account;
|
||||||
|
|
||||||
|
QString _targetFilePath;
|
||||||
|
|
||||||
|
QString _conflictFilePath;
|
||||||
|
|
||||||
|
QString _newFilename;
|
||||||
|
|
||||||
|
QString _remotePath;
|
||||||
|
|
||||||
|
QString _localPath;
|
||||||
|
|
||||||
|
QString _errorString;
|
||||||
|
|
||||||
|
SyncJournalDb *_journal = nullptr;
|
||||||
|
|
||||||
|
bool _allowedToRename = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // CASECLASHCONFLICTSOLVER_H
|
|
@ -372,7 +372,11 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent
|
||||||
case CSYNC_FILE_EXCLUDE_CONFLICT:
|
case CSYNC_FILE_EXCLUDE_CONFLICT:
|
||||||
item->_errorString = tr("Conflict: Server version downloaded, local copy renamed and not uploaded.");
|
item->_errorString = tr("Conflict: Server version downloaded, local copy renamed and not uploaded.");
|
||||||
item->_status = SyncFileItem::Conflict;
|
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:
|
case CSYNC_FILE_EXCLUDE_CANNOT_ENCODE:
|
||||||
item->_errorString = tr("The filename cannot be encoded on your file system.");
|
item->_errorString = tr("The filename cannot be encoded on your file system.");
|
||||||
break;
|
break;
|
||||||
|
@ -689,6 +693,15 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(
|
||||||
item->_modtime = serverEntry.modtime;
|
item->_modtime = serverEntry.modtime;
|
||||||
item->_size = serverEntry.size;
|
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 {
|
auto postProcessServerNew = [=]() mutable {
|
||||||
if (item->isDirectory()) {
|
if (item->isDirectory()) {
|
||||||
_pendingAsyncJobs++;
|
_pendingAsyncJobs++;
|
||||||
|
@ -1120,6 +1133,20 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo(
|
||||||
item->_type = localEntry.isDirectory ? ItemTypeDirectory : localEntry.isVirtualFile ? ItemTypeVirtualFile : ItemTypeFile;
|
item->_type = localEntry.isDirectory ? ItemTypeDirectory : localEntry.isVirtualFile ? ItemTypeVirtualFile : ItemTypeFile;
|
||||||
_childModified = true;
|
_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]() {
|
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.
|
// 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.
|
// 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 */
|
/** FileName of the entry (this does not contains any directory or path, just the plain name */
|
||||||
QString name;
|
QString name;
|
||||||
|
QString caseClashConflictingName;
|
||||||
time_t modtime = 0;
|
time_t modtime = 0;
|
||||||
int64_t size = 0;
|
int64_t size = 0;
|
||||||
uint64_t inode = 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)
|
time_t FileSystem::getModTime(const QString &filename)
|
||||||
{
|
{
|
||||||
csync_file_stat_t stat;
|
csync_file_stat_t stat;
|
||||||
qint64 result = -1;
|
time_t result = -1;
|
||||||
if (csync_vio_local_stat(filename, &stat) != -1
|
if (csync_vio_local_stat(filename, &stat) != -1 && (stat.modtime != 0)) {
|
||||||
&& (stat.modtime != 0)) {
|
|
||||||
result = stat.modtime;
|
result = stat.modtime;
|
||||||
} else {
|
} else {
|
||||||
result = Utility::qDateTimeToTime_t(QFileInfo(filename).lastModified());
|
result = Utility::qDateTimeToTime_t(QFileInfo(filename).lastModified());
|
||||||
|
@ -93,11 +92,11 @@ bool FileSystem::fileChanged(const QString &fileName,
|
||||||
}
|
}
|
||||||
|
|
||||||
bool FileSystem::verifyFileUnchanged(const QString &fileName,
|
bool FileSystem::verifyFileUnchanged(const QString &fileName,
|
||||||
qint64 previousSize,
|
qint64 previousSize,
|
||||||
time_t previousMtime)
|
time_t previousMtime)
|
||||||
{
|
{
|
||||||
const qint64 actualSize = getSize(fileName);
|
const auto actualSize = getSize(fileName);
|
||||||
const time_t actualMtime = getModTime(fileName);
|
const auto actualMtime = getModTime(fileName);
|
||||||
if ((actualSize != previousSize && actualMtime > 0) || (actualMtime != previousMtime && previousMtime > 0 && actualMtime > 0)) {
|
if ((actualSize != previousSize && actualMtime > 0) || (actualMtime != previousMtime && previousMtime > 0 && actualMtime > 0)) {
|
||||||
qCInfo(lcFileSystem) << "File" << fileName << "has changed:"
|
qCInfo(lcFileSystem) << "File" << fileName << "has changed:"
|
||||||
<< "size: " << previousSize << "<->" << actualSize
|
<< "size: " << previousSize << "<->" << actualSize
|
||||||
|
|
|
@ -913,6 +913,63 @@ bool OwncloudPropagator::createConflict(const SyncFileItemPtr &item,
|
||||||
return true;
|
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
|
QString OwncloudPropagator::adjustRenamedPath(const QString &original) const
|
||||||
{
|
{
|
||||||
return OCC::adjustRenamedPath(_renamedDirectories, original);
|
return OCC::adjustRenamedPath(_renamedDirectories, original);
|
||||||
|
@ -1473,4 +1530,23 @@ QString OwncloudPropagator::remotePath() const
|
||||||
return _remoteFolder;
|
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)
|
: PropagateItemJob(propagator, item)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
void start() override
|
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);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class PropagateUploadFileCommon;
|
class PropagateUploadFileCommon;
|
||||||
|
@ -586,6 +574,14 @@ public:
|
||||||
bool createConflict(const SyncFileItemPtr &item,
|
bool createConflict(const SyncFileItemPtr &item,
|
||||||
PropagatorCompositeJob *composite, QString *error);
|
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
|
// Map original path (as in the DB) to target final path
|
||||||
QMap<QString, QString> _renamedDirectories;
|
QMap<QString, QString> _renamedDirectories;
|
||||||
[[nodiscard]] QString adjustRenamedPath(const QString &original) const;
|
[[nodiscard]] QString adjustRenamedPath(const QString &original) const;
|
||||||
|
|
|
@ -41,6 +41,8 @@ QString Progress::asResultString(const SyncFileItem &item)
|
||||||
}
|
}
|
||||||
case CSYNC_INSTRUCTION_CONFLICT:
|
case CSYNC_INSTRUCTION_CONFLICT:
|
||||||
return QCoreApplication::translate("progress", "Server version downloaded, copied changed local file into conflict file");
|
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:
|
case CSYNC_INSTRUCTION_REMOVE:
|
||||||
return QCoreApplication::translate("progress", "Deleted");
|
return QCoreApplication::translate("progress", "Deleted");
|
||||||
case CSYNC_INSTRUCTION_EVAL_RENAME:
|
case CSYNC_INSTRUCTION_EVAL_RENAME:
|
||||||
|
@ -65,6 +67,7 @@ QString Progress::asActionString(const SyncFileItem &item)
|
||||||
{
|
{
|
||||||
switch (item._instruction) {
|
switch (item._instruction) {
|
||||||
case CSYNC_INSTRUCTION_CONFLICT:
|
case CSYNC_INSTRUCTION_CONFLICT:
|
||||||
|
case CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT:
|
||||||
case CSYNC_INSTRUCTION_SYNC:
|
case CSYNC_INSTRUCTION_SYNC:
|
||||||
case CSYNC_INSTRUCTION_NEW:
|
case CSYNC_INSTRUCTION_NEW:
|
||||||
case CSYNC_INSTRUCTION_TYPE_CHANGE:
|
case CSYNC_INSTRUCTION_TYPE_CHANGE:
|
||||||
|
|
|
@ -526,12 +526,7 @@ void PropagateDownloadFile::startAfterIsEncryptedIsChecked()
|
||||||
qCWarning(lcPropagateDownload) << "ignored virtual file type of" << _item->_file;
|
qCWarning(lcPropagateDownload) << "ignored virtual file type of" << _item->_file;
|
||||||
_item->_type = ItemTypeFile;
|
_item->_type = ItemTypeFile;
|
||||||
}
|
}
|
||||||
if (_item->_type == ItemTypeVirtualFile) {
|
if (_item->_type == ItemTypeVirtualFile && !propagator()->localFileNameClash(_item->_file)) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
qCDebug(lcPropagateDownload) << "creating virtual file" << _item->_file;
|
qCDebug(lcPropagateDownload) << "creating virtual file" << _item->_file;
|
||||||
// do a klaas' case clash check.
|
// do a klaas' case clash check.
|
||||||
if (propagator()->localFileNameClash(_item->_file)) {
|
if (propagator()->localFileNameClash(_item->_file)) {
|
||||||
|
@ -632,9 +627,18 @@ void PropagateDownloadFile::startDownload()
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// do a klaas' case clash check.
|
// do a klaas' case clash check.
|
||||||
if (propagator()->localFileNameClash(_item->_file)) {
|
if (propagator()->localFileNameClash(_item->_file) && _item->_type != ItemTypeVirtualFile) {
|
||||||
done(SyncFileItem::FileNameClash, tr("File %1 cannot be downloaded because of a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
|
_item->_instruction = CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
|
||||||
return;
|
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);
|
propagator()->reportProgress(*_item, 0);
|
||||||
|
@ -1147,14 +1151,7 @@ void PropagateDownloadFile::finalizeDownload()
|
||||||
void PropagateDownloadFile::downloadFinished()
|
void PropagateDownloadFile::downloadFinished()
|
||||||
{
|
{
|
||||||
ASSERT(!_tmpFile.isOpen());
|
ASSERT(!_tmpFile.isOpen());
|
||||||
QString fn = propagator()->fullLocalPath(_item->_file);
|
const auto filename = 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_item->_modtime <= 0) {
|
if (_item->_modtime <= 0) {
|
||||||
FileSystem::remove(_tmpFile.fileName());
|
FileSystem::remove(_tmpFile.fileName());
|
||||||
|
@ -1179,17 +1176,22 @@ void PropagateDownloadFile::downloadFinished()
|
||||||
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
qCWarning(lcPropagateDownload()) << "invalid modified time" << _item->_file << _item->_modtime;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool previousFileExists = FileSystem::fileExists(fn);
|
if (propagator()->localFileNameClash(_item->_file)) {
|
||||||
|
_item->_instruction = CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
|
||||||
|
qCInfo(lcPropagateDownload) << "setting instruction to" << _item->_instruction << _item->_file;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool previousFileExists = FileSystem::fileExists(filename) && _item->_instruction != CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
|
||||||
if (previousFileExists) {
|
if (previousFileExists) {
|
||||||
// Preserve the existing file permissions.
|
// Preserve the existing file permissions.
|
||||||
QFileInfo existingFile(fn);
|
const auto existingFile = QFileInfo{filename};
|
||||||
if (existingFile.permissions() != _tmpFile.permissions()) {
|
if (existingFile.permissions() != _tmpFile.permissions()) {
|
||||||
_tmpFile.setPermissions(existingFile.permissions());
|
_tmpFile.setPermissions(existingFile.permissions());
|
||||||
}
|
}
|
||||||
preserveGroupOwnership(_tmpFile.fileName(), existingFile);
|
preserveGroupOwnership(_tmpFile.fileName(), existingFile);
|
||||||
|
|
||||||
// Make the file a hydrated placeholder if possible
|
// 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) {
|
if (!result) {
|
||||||
done(SyncFileItem::NormalError, result.error());
|
done(SyncFileItem::NormalError, result.error());
|
||||||
return;
|
return;
|
||||||
|
@ -1199,15 +1201,28 @@ void PropagateDownloadFile::downloadFinished()
|
||||||
// Apply the remote permissions
|
// Apply the remote permissions
|
||||||
FileSystem::setFileReadOnlyWeak(_tmpFile.fileName(), !_item->_remotePerm.isNull() && !_item->_remotePerm.hasPermission(RemotePermissions::CanWrite));
|
FileSystem::setFileReadOnlyWeak(_tmpFile.fileName(), !_item->_remotePerm.isNull() && !_item->_remotePerm.hasPermission(RemotePermissions::CanWrite));
|
||||||
|
|
||||||
bool isConflict = _item->_instruction == CSYNC_INSTRUCTION_CONFLICT
|
const auto isConflict = (_item->_instruction == CSYNC_INSTRUCTION_CONFLICT
|
||||||
&& (QFileInfo(fn).isDir() || !FileSystem::fileEquals(fn, _tmpFile.fileName()));
|
&& (QFileInfo(filename).isDir() || !FileSystem::fileEquals(filename, _tmpFile.fileName()))) ||
|
||||||
|
_item->_instruction == CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT;
|
||||||
|
|
||||||
if (isConflict) {
|
if (isConflict) {
|
||||||
QString error;
|
if (_item->_instruction == CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT) {
|
||||||
if (!propagator()->createConflict(_item, _associatedComposite, &error)) {
|
qCInfo(lcPropagateDownload) << "downloading case clashed file" << _item->_file;
|
||||||
done(SyncFileItem::SoftError, error);
|
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;
|
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;
|
const auto vfs = propagator()->syncOptions()._vfs;
|
||||||
|
@ -1223,7 +1238,7 @@ void PropagateDownloadFile::downloadFinished()
|
||||||
// the discovery phase and now.
|
// the discovery phase and now.
|
||||||
const qint64 expectedSize = _item->_previousSize;
|
const qint64 expectedSize = _item->_previousSize;
|
||||||
const time_t expectedMtime = _item->_previousModtime;
|
const time_t expectedMtime = _item->_previousModtime;
|
||||||
if (!FileSystem::verifyFileUnchanged(fn, expectedSize, expectedMtime)) {
|
if (!FileSystem::verifyFileUnchanged(filename, expectedSize, expectedMtime)) {
|
||||||
propagator()->_anotherSyncNeeded = true;
|
propagator()->_anotherSyncNeeded = true;
|
||||||
done(SyncFileItem::SoftError, tr("File has changed since discovery"));
|
done(SyncFileItem::SoftError, tr("File has changed since discovery"));
|
||||||
return;
|
return;
|
||||||
|
@ -1231,14 +1246,14 @@ void PropagateDownloadFile::downloadFinished()
|
||||||
}
|
}
|
||||||
|
|
||||||
QString error;
|
QString error;
|
||||||
emit propagator()->touchedFile(fn);
|
emit propagator()->touchedFile(filename);
|
||||||
// The fileChanged() check is done above to generate better error messages.
|
// The fileChanged() check is done above to generate better error messages.
|
||||||
if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), fn, &error)) {
|
if (!FileSystem::uncheckedRenameReplace(_tmpFile.fileName(), filename, &error)) {
|
||||||
qCWarning(lcPropagateDownload) << QString("Rename failed: %1 => %2").arg(_tmpFile.fileName()).arg(fn);
|
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
|
// If the file is locked, we want to retry this sync when it
|
||||||
// becomes available again, otherwise try again directly
|
// becomes available again, otherwise try again directly
|
||||||
if (FileSystem::isFileLocked(fn)) {
|
if (FileSystem::isFileLocked(filename)) {
|
||||||
emit propagator()->seenLockedFile(fn);
|
emit propagator()->seenLockedFile(filename);
|
||||||
} else {
|
} else {
|
||||||
propagator()->_anotherSyncNeeded = true;
|
propagator()->_anotherSyncNeeded = true;
|
||||||
}
|
}
|
||||||
|
@ -1250,14 +1265,14 @@ void PropagateDownloadFile::downloadFinished()
|
||||||
qCInfo(lcPropagateDownload()) << propagator()->account()->davUser() << propagator()->account()->davDisplayName() << propagator()->account()->displayName();
|
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())) {
|
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";
|
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...
|
// Maybe we downloaded a newer version of the file than we thought we would...
|
||||||
// Get up to date information for the journal.
|
// 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.
|
// Maybe what we downloaded was a conflict file? If so, set a conflict record.
|
||||||
// (the data was prepared in slotGetFinished above)
|
// (the data was prepared in slotGetFinished above)
|
||||||
|
|
|
@ -178,7 +178,7 @@ void PropagateLocalMkdir::startLocalMkdir()
|
||||||
|
|
||||||
if (Utility::fsCasePreserving() && propagator()->localFileNameClash(_item->_file)) {
|
if (Utility::fsCasePreserving() && propagator()->localFileNameClash(_item->_file)) {
|
||||||
qCWarning(lcPropagateLocalMkdir) << "New folder to create locally already exists with different case:" << _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;
|
return;
|
||||||
}
|
}
|
||||||
emit propagator()->touchedFile(newDirStr);
|
emit propagator()->touchedFile(newDirStr);
|
||||||
|
@ -245,14 +245,14 @@ void PropagateLocalRename::start()
|
||||||
|
|
||||||
if (QString::compare(_item->_file, _item->_renameTarget, Qt::CaseInsensitive) != 0
|
if (QString::compare(_item->_file, _item->_renameTarget, Qt::CaseInsensitive) != 0
|
||||||
&& propagator()->localFileNameClash(_item->_renameTarget)) {
|
&& 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,
|
qCInfo(lcPropagateLocalRename) << "renaming a case clashed file" << _item->_file << _item->_renameTarget;
|
||||||
// it would have to come out the localFileNameClash function
|
const auto caseClashConflictResult = propagator()->createCaseClashConflict(_item, existingFile);
|
||||||
done(SyncFileItem::FileNameClash,
|
if (caseClashConflictResult) {
|
||||||
tr("File %1 cannot be renamed to %2 because of a local file name clash")
|
done(SyncFileItem::SoftError, *caseClashConflictResult);
|
||||||
.arg(QDir::toNativeSeparators(_item->_file), QDir::toNativeSeparators(_item->_renameTarget)));
|
} else {
|
||||||
|
done(SyncFileItem::FileNameClash, tr("File %1 downloaded but it resulted in a local file name clash!").arg(QDir::toNativeSeparators(_item->_file)));
|
||||||
|
}
|
||||||
return;
|
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)
|
void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item)
|
||||||
{
|
{
|
||||||
|
@ -906,6 +920,7 @@ void SyncEngine::slotPropagationFinished(bool success)
|
||||||
}
|
}
|
||||||
|
|
||||||
conflictRecordMaintenance();
|
conflictRecordMaintenance();
|
||||||
|
caseClashConflictRecordMaintenance();
|
||||||
|
|
||||||
_journal->deleteStaleFlagsEntries();
|
_journal->deleteStaleFlagsEntries();
|
||||||
_journal->commit("All Finished.", false);
|
_journal->commit("All Finished.", false);
|
||||||
|
|
|
@ -283,6 +283,9 @@ private:
|
||||||
// Removes stale and adds missing conflict records after sync
|
// Removes stale and adds missing conflict records after sync
|
||||||
void conflictRecordMaintenance();
|
void conflictRecordMaintenance();
|
||||||
|
|
||||||
|
// Removes stale and adds missing conflict records after sync
|
||||||
|
void caseClashConflictRecordMaintenance();
|
||||||
|
|
||||||
// cleanup and emit the finished signal
|
// cleanup and emit the finished signal
|
||||||
void finalize(bool success);
|
void finalize(bool success);
|
||||||
|
|
||||||
|
|
|
@ -655,7 +655,8 @@ FakeGetReply::FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::
|
||||||
Q_ASSERT(!fileName.isEmpty());
|
Q_ASSERT(!fileName.isEmpty());
|
||||||
fileInfo = remoteRootFileInfo.find(fileName);
|
fileInfo = remoteRootFileInfo.find(fileName);
|
||||||
if (!fileInfo) {
|
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");
|
Q_ASSERT_X(fileInfo, Q_FUNC_INFO, "Could not find file on the remote");
|
||||||
QMetaObject::invokeMethod(this, &FakeGetReply::respond, Qt::QueuedConnection);
|
QMetaObject::invokeMethod(this, &FakeGetReply::respond, Qt::QueuedConnection);
|
||||||
|
@ -669,6 +670,12 @@ void FakeGetReply::respond()
|
||||||
emit finished();
|
emit finished();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!fileInfo) {
|
||||||
|
setError(ContentNotFoundError, QStringLiteral("File Not Found"));
|
||||||
|
emit metaDataChanged();
|
||||||
|
emit finished();
|
||||||
|
return;
|
||||||
|
}
|
||||||
payload = fileInfo->contentChar;
|
payload = fileInfo->contentChar;
|
||||||
size = fileInfo->size;
|
size = fileInfo->size;
|
||||||
setHeader(QNetworkRequest::ContentLengthHeader, size);
|
setHeader(QNetworkRequest::ContentLengthHeader, size);
|
||||||
|
@ -1190,7 +1197,7 @@ void FakeFolder::execUntilItemCompleted(const QString &relativePath)
|
||||||
|
|
||||||
void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
|
void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
|
||||||
{
|
{
|
||||||
foreach (const FileInfo &child, templateFi.children) {
|
for(const auto &child : templateFi.children) {
|
||||||
if (child.isDir) {
|
if (child.isDir) {
|
||||||
QDir subDir(dir);
|
QDir subDir(dir);
|
||||||
dir.mkdir(child.name);
|
dir.mkdir(child.name);
|
||||||
|
@ -1208,7 +1215,7 @@ void FakeFolder::toDisk(QDir &dir, const FileInfo &templateFi)
|
||||||
|
|
||||||
void FakeFolder::fromDisk(QDir &dir, 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()) {
|
if (diskChild.isDir()) {
|
||||||
QDir subDir = dir;
|
QDir subDir = dir;
|
||||||
subDir.cd(diskChild.fileName());
|
subDir.cd(diskChild.fileName());
|
||||||
|
|
|
@ -30,79 +30,86 @@ public:
|
||||||
E2eFileTransferTest() = default;
|
E2eFileTransferTest() = default;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EndToEndTestHelper _helper;
|
|
||||||
OCC::Folder *_testFolder;
|
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void initTestCase()
|
void initTestCase()
|
||||||
{
|
{
|
||||||
QSignalSpy accountReady(&_helper, &EndToEndTestHelper::accountReady);
|
qRegisterMetaType<OCC::SyncResult>("OCC::SyncResult");
|
||||||
_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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void testSyncFolder()
|
void testSyncFolder()
|
||||||
{
|
{
|
||||||
// Try the down-sync first
|
{
|
||||||
QSignalSpy folderSyncFinished(_testFolder, &OCC::Folder::syncFinished);
|
EndToEndTestHelper _helper;
|
||||||
OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
|
OCC::Folder *_testFolder;
|
||||||
QVERIFY(folderSyncFinished.wait(3000));
|
|
||||||
|
|
||||||
const auto testFolderPath = _testFolder->path();
|
QSignalSpy accountReady(&_helper, &EndToEndTestHelper::accountReady);
|
||||||
const QString expectedFilePath(testFolderPath + QStringLiteral("welcome.txt"));
|
_helper.startAccountConfig();
|
||||||
const QFile expectedFile(expectedFilePath);
|
QVERIFY(accountReady.wait(3000));
|
||||||
qDebug() << "Checking if expected file exists at:" << expectedFilePath;
|
|
||||||
QVERIFY(expectedFile.exists());
|
|
||||||
|
|
||||||
// Now write a file to test the upload
|
const auto accountState = _helper.accountState();
|
||||||
const auto fileName = QStringLiteral("test_file.txt");
|
QSignalSpy accountConnected(accountState.data(), &OCC::AccountState::isConnectedChanged);
|
||||||
const QString localFilePath(_testFolder->path() + fileName);
|
QVERIFY(accountConnected.wait(30000));
|
||||||
QVERIFY(OCC::Utility::writeRandomFile(localFilePath));
|
|
||||||
|
|
||||||
OCC::FolderMan::instance()->forceSyncForFolder(_testFolder);
|
_testFolder = _helper.configureSyncFolder();
|
||||||
QVERIFY(folderSyncFinished.wait(3000));
|
QVERIFY(_testFolder);
|
||||||
qDebug() << "First folder sync complete";
|
|
||||||
|
|
||||||
const auto waitForServerToProcessTime = QTime::currentTime().addSecs(3);
|
// Try the down-sync first
|
||||||
while (QTime::currentTime() < waitForServerToProcessTime) {
|
QSignalSpy folderSyncFinished(_testFolder, &OCC::Folder::syncFinished);
|
||||||
QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
|
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
|
QTest::qWait(10000);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -5,13 +5,43 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <QtTest>
|
|
||||||
#include "syncenginetestutils.h"
|
#include "syncenginetestutils.h"
|
||||||
#include <syncengine.h>
|
|
||||||
#include <propagatorjobs.h>
|
#include "syncengine.h"
|
||||||
|
#include "propagatorjobs.h"
|
||||||
|
#include "caseclashconflictsolver.h"
|
||||||
|
|
||||||
|
#include <QtTest>
|
||||||
|
|
||||||
using namespace OCC;
|
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)
|
bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
|
||||||
{
|
{
|
||||||
if (auto item = spy.findItem(path)) {
|
if (auto item = spy.findItem(path)) {
|
||||||
|
@ -20,12 +50,6 @@ bool itemDidComplete(const ItemCompletedSpy &spy, const QString &path)
|
||||||
return false;
|
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)
|
bool itemDidCompleteSuccessfully(const ItemCompletedSpy &spy, const QString &path)
|
||||||
{
|
{
|
||||||
if (auto item = spy.findItem(path)) {
|
if (auto item = spy.findItem(path)) {
|
||||||
|
@ -54,6 +78,8 @@ int itemSuccessfullyCompletedGetRank(const ItemCompletedSpy &spy, const QString
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
class TestSyncEngine : public QObject
|
class TestSyncEngine : public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
@ -1307,6 +1333,270 @@ private slots:
|
||||||
auto folderA = fakeFolder.currentLocalState().find("toDelete");
|
auto folderA = fakeFolder.currentLocalState().find("toDelete");
|
||||||
QCOMPARE(folderA, nullptr);
|
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)
|
QTEST_GUILESS_MAIN(TestSyncEngine)
|
||||||
|
|
|
@ -13,6 +13,34 @@
|
||||||
|
|
||||||
using namespace OCC;
|
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
|
#define DVSUFFIX APPLICATION_DOTVIRTUALFILE_SUFFIX
|
||||||
|
|
||||||
bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
|
bool itemInstruction(const ItemCompletedSpy &spy, const QString &path, const SyncInstructions instr)
|
||||||
|
@ -1691,6 +1719,166 @@ private slots:
|
||||||
fakeFolder.execUntilBeforePropagation();
|
fakeFolder.execUntilBeforePropagation();
|
||||||
|
|
||||||
QCOMPARE(checkStatus(), SyncFileStatus::StatusError);
|
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