Skip to content

Commit

Permalink
Merge pull request #5232 from nextcloud/feature/syncWithCaseClashNames
Browse files Browse the repository at this point in the history
Feature/sync with case clash names
  • Loading branch information
mgallien authored Jan 26, 2023
2 parents 5c42da4 + fbb0a33 commit aa74448
Show file tree
Hide file tree
Showing 37 changed files with 2,096 additions and 218 deletions.
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,9 @@ option(BUILD_LIBRARIES_ONLY "BUILD_LIBRARIES_ONLY" OFF)
# build the GUI component, when disabled only nextcloudcmd is built
option(BUILD_GUI "BUILD_GUI" ON)

# build the tests
option(BUILD_TESTING "BUILD_TESTING" ON)

# When this option is enabled, 5xx errors are not added to the blacklist
# Normally you don't want to enable this option because if a particular file
# triggers a bug on the server, you want the file to be blacklisted.
Expand Down
5 changes: 5 additions & 0 deletions src/common/preparedsqlquerymanager.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,11 @@ class OCSYNC_EXPORT PreparedSqlQueryManager
DeleteKeyValueStoreQuery,
GetConflictRecordQuery,
SetConflictRecordQuery,
GetCaseClashConflictRecordQuery,
GetCaseClashConflictRecordByPathQuery,
SetCaseClashConflictRecordQuery,
DeleteCaseClashConflictRecordQuery,
GetAllCaseClashConflictPathQuery,
DeleteConflictRecordQuery,
GetRawPinStateQuery,
GetEffectivePinStateQuery,
Expand Down
107 changes: 107 additions & 0 deletions src/common/syncjournaldb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,18 @@ bool SyncJournalDb::checkConnect()
return sqlFail(QStringLiteral("Create table conflicts"), createQuery);
}

// create the caseconflicts table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS caseconflicts("
"path TEXT PRIMARY KEY,"
"baseFileId TEXT,"
"baseEtag TEXT,"
"baseModtime INTEGER,"
"basePath TEXT UNIQUE"
");");
if (!createQuery.exec()) {
return sqlFail(QStringLiteral("Create table caseconflicts"), createQuery);
}

createQuery.prepare("CREATE TABLE IF NOT EXISTS version("
"major INTEGER(8),"
"minor INTEGER(8),"
Expand Down Expand Up @@ -2201,6 +2213,101 @@ ConflictRecord SyncJournalDb::conflictRecord(const QByteArray &path)
return entry;
}

void SyncJournalDb::setCaseConflictRecord(const ConflictRecord &record)
{
QMutexLocker locker(&_mutex);
if (!checkConnect())
return;

const auto query = _queryManager.get(PreparedSqlQueryManager::SetCaseClashConflictRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO caseconflicts "
"(path, baseFileId, baseModtime, baseEtag, basePath) "
"VALUES (?1, ?2, ?3, ?4, ?5);"),
_db);
ASSERT(query)
query->bindValue(1, record.path);
query->bindValue(2, record.baseFileId);
query->bindValue(3, record.baseModtime);
query->bindValue(4, record.baseEtag);
query->bindValue(5, record.initialBasePath);
ASSERT(query->exec())
}

ConflictRecord SyncJournalDb::caseConflictRecordByBasePath(const QString &baseNamePath)
{
ConflictRecord entry;

QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return entry;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetCaseClashConflictRecordQuery, QByteArrayLiteral("SELECT path, baseFileId, baseModtime, baseEtag, basePath FROM caseconflicts WHERE basePath=?1;"), _db);
ASSERT(query)
query->bindValue(1, baseNamePath);
ASSERT(query->exec())
if (!query->next().hasData)
return entry;

entry.path = query->baValue(0);
entry.baseFileId = query->baValue(1);
entry.baseModtime = query->int64Value(2);
entry.baseEtag = query->baValue(3);
entry.initialBasePath = query->baValue(4);
return entry;
}

ConflictRecord SyncJournalDb::caseConflictRecordByPath(const QString &path)
{
ConflictRecord entry;

QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return entry;
}
const auto query = _queryManager.get(PreparedSqlQueryManager::GetCaseClashConflictRecordByPathQuery, QByteArrayLiteral("SELECT path, baseFileId, baseModtime, baseEtag, basePath FROM caseconflicts WHERE path=?1;"), _db);
ASSERT(query)
query->bindValue(1, path);
ASSERT(query->exec())
if (!query->next().hasData)
return entry;

entry.path = query->baValue(0);
entry.baseFileId = query->baValue(1);
entry.baseModtime = query->int64Value(2);
entry.baseEtag = query->baValue(3);
entry.initialBasePath = query->baValue(4);
return entry;
}

void SyncJournalDb::deleteCaseClashConflictByPathRecord(const QString &path)
{
QMutexLocker locker(&_mutex);
if (!checkConnect())
return;

const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteCaseClashConflictRecordQuery, QByteArrayLiteral("DELETE FROM caseconflicts WHERE path=?1;"), _db);
ASSERT(query)
query->bindValue(1, path);
ASSERT(query->exec())
}

QByteArrayList SyncJournalDb::caseClashConflictRecordPaths()
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return {};
}

const auto query = _queryManager.get(PreparedSqlQueryManager::GetAllCaseClashConflictPathQuery, QByteArrayLiteral("SELECT path FROM caseconflicts;"), _db);
ASSERT(query)
ASSERT(query->exec())

QByteArrayList paths;
while (query->next().hasData)
paths.append(query->baValue(0));

return paths;
}

void SyncJournalDb::deleteConflictRecord(const QByteArray &path)
{
QMutexLocker locker(&_mutex);
Expand Down
16 changes: 16 additions & 0 deletions src/common/syncjournaldb.h
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,15 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject
/// Retrieve a conflict record by path of the file with the conflict tag
ConflictRecord conflictRecord(const QByteArray &path);

/// Retrieve a conflict record by path of the file with the conflict tag
ConflictRecord caseConflictRecordByBasePath(const QString &baseNamePath);

/// Retrieve a conflict record by path of the file with the conflict tag
ConflictRecord caseConflictRecordByPath(const QString &path);

/// Return all paths of files with a conflict tag in the name and records in the db
QByteArrayList caseClashConflictRecordPaths();

/// Delete a conflict record by path of the file with the conflict tag
void deleteConflictRecord(const QByteArray &path);

Expand Down Expand Up @@ -373,6 +382,13 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject
*/
int autotestFailCounter = -1;

public slots:
/// Store a new or updated record in the database
void setCaseConflictRecord(const ConflictRecord &record);

/// Delete a case clash conflict record by path of the file with the conflict tag
void deleteCaseClashConflictByPathRecord(const QString &path);

private:
int getFileRecordCount();
[[nodiscard]] bool updateDatabaseStructure();
Expand Down
54 changes: 32 additions & 22 deletions src/common/utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -624,35 +624,21 @@ QString Utility::makeConflictFileName(
return conflictFileName;
}

bool Utility::isConflictFile(const char *name)
{
const char *bname = std::strrchr(name, '/');
if (bname) {
bname += 1;
} else {
bname = name;
}

// Old pattern
if (std::strstr(bname, "_conflict-"))
return true;

// New pattern
if (std::strstr(bname, "(conflicted copy"))
return true;

return false;
}

bool Utility::isConflictFile(const QString &name)
{
auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);

if (bname.contains(QStringLiteral("_conflict-")))
if (bname.contains(QStringLiteral("_conflict-"))) {
return true;
}

if (bname.contains(QStringLiteral("(conflicted copy"))) {
return true;
}

if (bname.contains(QStringLiteral("(conflicted copy")))
if (isCaseClashConflictFile(name)) {
return true;
}

return false;
}
Expand Down Expand Up @@ -722,4 +708,28 @@ QString Utility::sanitizeForFileName(const QString &name)
return result;
}

QString Utility::makeCaseClashConflictFileName(const QString &filename, const QDateTime &datetime)
{
auto conflictFileName(filename);
// Add conflict tag before the extension.
auto dotLocation = conflictFileName.lastIndexOf(QLatin1Char('.'));
// If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
if (dotLocation <= conflictFileName.lastIndexOf(QLatin1Char('/')) + 1) {
dotLocation = conflictFileName.size();
}

auto conflictMarker = QStringLiteral(" (case clash from ");
conflictMarker += datetime.toString(QStringLiteral("yyyy-MM-dd hhmmss")) + QLatin1Char(')');

conflictFileName.insert(dotLocation, conflictMarker);
return conflictFileName;
}

bool Utility::isCaseClashConflictFile(const QString &name)
{
const auto bname = name.midRef(name.lastIndexOf(QLatin1Char('/')) + 1);

return bname.contains(QStringLiteral("(case clash from"));
}

} // namespace OCC
5 changes: 4 additions & 1 deletion src/common/utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,13 @@ namespace Utility {
OCSYNC_EXPORT QString makeConflictFileName(
const QString &fn, const QDateTime &dt, const QString &user);

OCSYNC_EXPORT QString makeCaseClashConflictFileName(const QString &filename, const QDateTime &datetime);

/** Returns whether a file name indicates a conflict file
*/
OCSYNC_EXPORT bool isConflictFile(const char *name);
bool isConflictFile(const char *name) = delete;
OCSYNC_EXPORT bool isConflictFile(const QString &name);
OCSYNC_EXPORT bool isCaseClashConflictFile(const QString &name);

/** Find the base name for a conflict file name, using name pattern only
*
Expand Down
33 changes: 17 additions & 16 deletions src/csync/csync.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,22 +104,23 @@ Q_ENUM_NS(csync_status_codes_e)
* the csync state of a file.
*/
enum SyncInstructions {
CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
CSYNC_INSTRUCTION_ERROR = 1 << 8,
CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
Used when the type of something changes from directory to file
or back. */
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
but without any propagation (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
CSYNC_INSTRUCTION_ERROR = 1 << 8,
CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
Used when the type of something changes from directory to file
or back. */
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
but without any propagation (UPDATE|RECONCILE) */
CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */
};

Q_ENUM_NS(SyncInstructions)
Expand Down
10 changes: 7 additions & 3 deletions src/csync/csync_exclude.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -205,10 +205,14 @@ static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const QString &path, bool exclu
return CSYNC_FILE_SILENTLY_EXCLUDED;
}


if (excludeConflictFiles && OCC::Utility::isConflictFile(path)) {
return CSYNC_FILE_EXCLUDE_CONFLICT;
if (excludeConflictFiles) {
if (OCC::Utility::isCaseClashConflictFile(path)) {
return CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT;
} else if (OCC::Utility::isConflictFile(path)) {
return CSYNC_FILE_EXCLUDE_CONFLICT;
}
}

return CSYNC_NOT_EXCLUDED;
}

Expand Down
1 change: 1 addition & 0 deletions src/csync/csync_exclude.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ enum CSYNC_EXCLUDE_TYPE {
CSYNC_FILE_EXCLUDE_HIDDEN,
CSYNC_FILE_EXCLUDE_STAT_FAILED,
CSYNC_FILE_EXCLUDE_CONFLICT,
CSYNC_FILE_EXCLUDE_CASE_CLASH_CONFLICT,
CSYNC_FILE_EXCLUDE_CANNOT_ENCODE,
CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED,
CSYNC_FILE_EXCLUDE_LEADING_SPACE,
Expand Down
3 changes: 3 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ set(client_UI_SRCS
accountsettings.ui
conflictdialog.ui
invalidfilenamedialog.ui
caseclashfilenamedialog.ui
foldercreationdialog.ui
folderwizardsourcepage.ui
folderwizardtargetpage.ui
Expand Down Expand Up @@ -73,6 +74,8 @@ set(client_SRCS
application.cpp
invalidfilenamedialog.h
invalidfilenamedialog.cpp
caseclashfilenamedialog.h
caseclashfilenamedialog.cpp
callstatechecker.h
callstatechecker.cpp
conflictdialog.h
Expand Down
Loading

0 comments on commit aa74448

Please sign in to comment.