Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/desktop.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/common/filesystembase.cpp21
-rw-r--r--src/common/filesystembase.h5
-rw-r--r--src/common/syncjournaldb.cpp99
-rw-r--r--src/common/syncjournaldb.h19
-rw-r--r--src/common/syncjournalfilerecord.h37
-rw-r--r--src/common/utility.cpp60
-rw-r--r--src/common/utility.h16
-rw-r--r--src/csync/csync_exclude.cpp19
-rw-r--r--src/csync/csync_exclude.h9
-rw-r--r--src/csync/csync_private.h2
-rw-r--r--src/csync/csync_update.cpp2
-rw-r--r--src/libsync/capabilities.cpp11
-rw-r--r--src/libsync/capabilities.h5
-rw-r--r--src/libsync/owncloudpropagator.cpp27
-rw-r--r--src/libsync/owncloudpropagator.h23
-rw-r--r--src/libsync/propagatedownload.cpp66
-rw-r--r--src/libsync/propagatedownload.h1
-rw-r--r--src/libsync/propagateupload.cpp13
-rw-r--r--src/libsync/syncengine.cpp58
-rw-r--r--src/libsync/syncengine.h12
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/testsyncconflict.cpp351
-rw-r--r--test/testsyncjournaldb.cpp22
23 files changed, 792 insertions, 87 deletions
diff --git a/src/common/filesystembase.cpp b/src/common/filesystembase.cpp
index 1d95e3918..8de47312e 100644
--- a/src/common/filesystembase.cpp
+++ b/src/common/filesystembase.cpp
@@ -403,27 +403,6 @@ QByteArray FileSystem::calcAdler32(const QString &filename)
}
#endif
-QString FileSystem::makeConflictFileName(const QString &fn, const QDateTime &dt)
-{
- QString conflictFileName(fn);
- // Add _conflict-XXXX before the extension.
- int dotLocation = conflictFileName.lastIndexOf('.');
- // If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
- if (dotLocation <= conflictFileName.lastIndexOf('/') + 1) {
- dotLocation = conflictFileName.size();
- }
- QString timeString = dt.toString("yyyyMMdd-hhmmss");
-
- // Additional marker
- QByteArray conflictFileUserName = qgetenv("CSYNC_CONFLICT_FILE_USERNAME");
- if (conflictFileUserName.isEmpty())
- conflictFileName.insert(dotLocation, "_conflict-" + timeString);
- else
- conflictFileName.insert(dotLocation, "_conflict_" + QString::fromUtf8(conflictFileUserName) + "-" + timeString);
-
- return conflictFileName;
-}
-
bool FileSystem::remove(const QString &fileName, QString *errorString)
{
#ifdef Q_OS_WIN
diff --git a/src/common/filesystembase.h b/src/common/filesystembase.h
index 9568dac12..e7c7672b1 100644
--- a/src/common/filesystembase.h
+++ b/src/common/filesystembase.h
@@ -132,11 +132,6 @@ namespace FileSystem {
#endif
/**
- * Returns a file name based on \a fn that's suitable for a conflict.
- */
- QString OCSYNC_EXPORT makeConflictFileName(const QString &fn, const QDateTime &dt);
-
- /**
* Returns true when a file is locked. (Windows only)
*/
bool OCSYNC_EXPORT isFileLocked(const QString &fileName);
diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp
index b2d348af8..a40149d3a 100644
--- a/src/common/syncjournaldb.cpp
+++ b/src/common/syncjournaldb.cpp
@@ -438,7 +438,7 @@ bool SyncJournalDb::checkConnect()
return sqlFail("Create table version", createQuery);
}
- // create the checksumtype table.
+ // create the datafingerprint table.
createQuery.prepare("CREATE TABLE IF NOT EXISTS datafingerprint("
"fingerprint TEXT UNIQUE"
");");
@@ -446,6 +446,17 @@ bool SyncJournalDb::checkConnect()
return sqlFail("Create table datafingerprint", createQuery);
}
+ // create the conflicts table.
+ createQuery.prepare("CREATE TABLE IF NOT EXISTS conflicts("
+ "path TEXT PRIMARY KEY,"
+ "baseFileId TEXT,"
+ "baseEtag TEXT,"
+ "baseModtime INTEGER"
+ ");");
+ if (!createQuery.exec()) {
+ return sqlFail("Create table conflicts", createQuery);
+ }
+
createQuery.prepare("CREATE TABLE IF NOT EXISTS version("
"major INTEGER(8),"
"minor INTEGER(8),"
@@ -693,6 +704,23 @@ bool SyncJournalDb::checkConnect()
return sqlFail("prepare _setDataFingerprintQuery2", *_setDataFingerprintQuery2);
}
+ _getConflictRecordQuery.reset(new SqlQuery(_db));
+ if (_getConflictRecordQuery->prepare("SELECT baseFileId, baseModtime, baseEtag FROM conflicts WHERE path=?1;")) {
+ return sqlFail("prepare _getConflictRecordQuery", *_getConflictRecordQuery);
+ }
+
+ _setConflictRecordQuery.reset(new SqlQuery(_db));
+ if (_setConflictRecordQuery->prepare("INSERT OR REPLACE INTO conflicts "
+ "(path, baseFileId, baseModtime, baseEtag) "
+ "VALUES (?1, ?2, ?3, ?4);")) {
+ return sqlFail("prepare _setConflictRecordQuery", *_setConflictRecordQuery);
+ }
+
+ _deleteConflictRecordQuery.reset(new SqlQuery(_db));
+ if (_deleteConflictRecordQuery->prepare("DELETE FROM conflicts WHERE path=?1;")) {
+ return sqlFail("prepare _deleteConflictRecordQuery", *_deleteConflictRecordQuery);
+ }
+
// don't start a new transaction now
commitInternal(QString("checkConnect End"), false);
@@ -741,6 +769,9 @@ void SyncJournalDb::close()
_getDataFingerprintQuery.reset(0);
_setDataFingerprintQuery1.reset(0);
_setDataFingerprintQuery2.reset(0);
+ _getConflictRecordQuery.reset(0);
+ _setConflictRecordQuery.reset(0);
+ _deleteConflictRecordQuery.reset(0);
_db.close();
_avoidReadFromDbOnNextSyncFilter.clear();
@@ -1951,6 +1982,72 @@ void SyncJournalDb::setDataFingerprint(const QByteArray &dataFingerprint)
_setDataFingerprintQuery2->exec();
}
+void SyncJournalDb::setConflictRecord(const ConflictRecord &record)
+{
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect())
+ return;
+
+ auto &query = *_setConflictRecordQuery;
+ query.reset_and_clear_bindings();
+ query.bindValue(1, record.path);
+ query.bindValue(2, record.baseFileId);
+ query.bindValue(3, record.baseModtime);
+ query.bindValue(4, record.baseEtag);
+ ASSERT(query.exec());
+}
+
+ConflictRecord SyncJournalDb::conflictRecord(const QByteArray &path)
+{
+ ConflictRecord entry;
+
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect())
+ return entry;
+
+ auto &query = *_getConflictRecordQuery;
+ query.reset_and_clear_bindings();
+ query.bindValue(1, path);
+ ASSERT(query.exec());
+ if (!query.next())
+ return entry;
+
+ entry.path = path;
+ entry.baseFileId = query.baValue(0);
+ entry.baseModtime = query.int64Value(1);
+ entry.baseEtag = query.baValue(2);
+ return entry;
+}
+
+void SyncJournalDb::deleteConflictRecord(const QByteArray &path)
+{
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect())
+ return;
+
+ auto &query = *_deleteConflictRecordQuery;
+ query.reset_and_clear_bindings();
+ query.bindValue(1, path);
+ ASSERT(query.exec());
+}
+
+QByteArrayList SyncJournalDb::conflictRecordPaths()
+{
+ QMutexLocker locker(&_mutex);
+ if (!checkConnect())
+ return {};
+
+ SqlQuery query(_db);
+ query.prepare("SELECT path FROM conflicts");
+ ASSERT(query.exec());
+
+ QByteArrayList paths;
+ while (query.next())
+ paths.append(query.baValue(0));
+
+ return paths;
+}
+
void SyncJournalDb::clearFileTable()
{
QMutexLocker lock(&_mutex);
diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h
index dc650ffa3..aa30d349c 100644
--- a/src/common/syncjournaldb.h
+++ b/src/common/syncjournaldb.h
@@ -207,6 +207,22 @@ public:
void setDataFingerprint(const QByteArray &dataFingerprint);
QByteArray dataFingerprint();
+
+ // Conflict record functions
+
+ /// Store a new or updated record in the database
+ void setConflictRecord(const ConflictRecord &record);
+
+ /// Retrieve a conflict record by path of the _conflict- file
+ ConflictRecord conflictRecord(const QByteArray &path);
+
+ /// Delete a conflict record by path of the _conflict- file
+ void deleteConflictRecord(const QByteArray &path);
+
+ /// Return all paths of _conflict- files with records in the db
+ QByteArrayList conflictRecordPaths();
+
+
/**
* Delete any file entry. This will force the next sync to re-sync everything as if it was new,
* restoring everyfile on every remote. If a file is there both on the client and server side,
@@ -266,6 +282,9 @@ private:
QScopedPointer<SqlQuery> _getDataFingerprintQuery;
QScopedPointer<SqlQuery> _setDataFingerprintQuery1;
QScopedPointer<SqlQuery> _setDataFingerprintQuery2;
+ QScopedPointer<SqlQuery> _getConflictRecordQuery;
+ QScopedPointer<SqlQuery> _setConflictRecordQuery;
+ QScopedPointer<SqlQuery> _deleteConflictRecordQuery;
/* This is the list of paths we called avoidReadFromDbOnNextSync on.
* It means that they should not be written to the DB in any case since doing
diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h
index 76fd4cdbe..b09006365 100644
--- a/src/common/syncjournalfilerecord.h
+++ b/src/common/syncjournalfilerecord.h
@@ -111,6 +111,43 @@ public:
bool isValid() const;
};
+
+/** Represents a conflict in the conflicts table.
+ *
+ * In the following the "conflict file" is the file with the "_conflict-"
+ * tag and the base file is the file that its a conflict for. So if
+ * a/foo.txt is the base file, its conflict file could be
+ * a/foo_conflict-1234.txt.
+ */
+class OCSYNC_EXPORT ConflictRecord
+{
+public:
+ /** Path to the _conflict- file
+ *
+ * So if a/foo.txt has a conflict, this path would point to
+ * a/foo_conflict-1234.txt.
+ *
+ * The path is sync-folder relative.
+ */
+ QByteArray path;
+
+ /// File id of the base file
+ QByteArray baseFileId;
+
+ /** Modtime of the base file
+ *
+ * may not be available and be -1
+ */
+ qint64 baseModtime = -1;
+
+ /** Etag of the base file
+ *
+ * may not be available and empty
+ */
+ QByteArray baseEtag;
+
+ bool isValid() const { return !path.isEmpty(); }
+};
}
#endif // SYNCJOURNALFILERECORD_H
diff --git a/src/common/utility.cpp b/src/common/utility.cpp
index bdf603515..29308ea84 100644
--- a/src/common/utility.cpp
+++ b/src/common/utility.cpp
@@ -542,6 +542,21 @@ QUrl Utility::concatUrlPath(const QUrl &url, const QString &concatPath,
return tmpUrl;
}
+QString Utility::makeConflictFileName(const QString &fn, const QDateTime &dt)
+{
+ QString conflictFileName(fn);
+ // Add _conflict-XXXX before the extension.
+ int dotLocation = conflictFileName.lastIndexOf('.');
+ // If no extension, add it at the end (take care of cases like foo/.hidden or foo.bar/file)
+ if (dotLocation <= conflictFileName.lastIndexOf('/') + 1) {
+ dotLocation = conflictFileName.size();
+ }
+ QString timeString = dt.toString("yyyyMMdd-hhmmss");
+
+ conflictFileName.insert(dotLocation, "_conflict-" + timeString);
+ return conflictFileName;
+}
+
bool Utility::isConflictFile(const char *name)
{
const char *bname = std::strrchr(name, '/');
@@ -551,32 +566,33 @@ bool Utility::isConflictFile(const char *name)
bname = name;
}
- if (std::strstr(bname, "_conflict-"))
- return true;
-
- if (shouldUploadConflictFiles()) {
- // For uploads, we want to consider files with any kind of username tag
- // as conflict files. (pattern *_conflict_*-)
- const char *startOfMarker = std::strstr(bname, "_conflict_");
- if (startOfMarker && std::strchr(startOfMarker, '-'))
- return true;
- } else {
- // Old behavior: optionally, files with the specific string in the env variable
- // appended are also considered conflict files.
- static auto conflictFileUsername = qgetenv("CSYNC_CONFLICT_FILE_USERNAME");
- static auto usernameConflictId = QByteArray("_conflict_" + conflictFileUsername + "-");
- if (!conflictFileUsername.isEmpty() && std::strstr(bname, usernameConflictId.constData())) {
- return true;
- }
- }
+ return std::strstr(bname, "_conflict-");
+}
- return false;
+bool Utility::isConflictFile(const QString &name)
+{
+ auto bname = name.midRef(name.lastIndexOf('/') + 1);
+ return bname.contains("_conflict-", Utility::fsCasePreserving() ? Qt::CaseInsensitive : Qt::CaseSensitive);
}
-bool Utility::shouldUploadConflictFiles()
+QByteArray Utility::conflictFileBaseName(const QByteArray &conflictName)
{
- static bool uploadConflictFiles = qEnvironmentVariableIntValue("OWNCLOUD_UPLOAD_CONFLICT_FILES") != 0;
- return uploadConflictFiles;
+ // This function must be able to deal with conflict files for conflict files.
+ // To do this, we scan backwards, for the outermost conflict marker and
+ // strip only that to generate the conflict file base name.
+ int from = conflictName.size();
+ while (from != -1) {
+ auto start = conflictName.lastIndexOf("_conflict-", from);
+ if (start == -1)
+ return "";
+ from = start - 1;
+
+ auto end = conflictName.indexOf('.', start);
+ if (end == -1)
+ end = conflictName.size();
+ return conflictName.left(start) + conflictName.mid(end);
+ }
+ return "";
}
} // namespace OCC
diff --git a/src/common/utility.h b/src/common/utility.h
index 666e44cfd..7247673ce 100644
--- a/src/common/utility.h
+++ b/src/common/utility.h
@@ -182,17 +182,23 @@ namespace Utility {
with the given parent. If no parent is specified, the caller must destroy the settings */
OCSYNC_EXPORT std::unique_ptr<QSettings> settingsWithGroup(const QString &group, QObject *parent = 0);
+ /** Returns a file name based on \a fn that's suitable for a conflict.
+ */
+ OCSYNC_EXPORT QString makeConflictFileName(const QString &fn, const QDateTime &dt);
+
/** Returns whether a file name indicates a conflict file
- *
- * See FileSystem::makeConflictFileName.
*/
OCSYNC_EXPORT bool isConflictFile(const char *name);
+ OCSYNC_EXPORT bool isConflictFile(const QString &name);
- /** Returns whether conflict files should be uploaded.
+ /** Find the base name for a conflict file name
+ *
+ * Will return an empty string if it's not a conflict file.
*
- * Experimental! Real feature planned for 2.5.
+ * Prefer to use the data from the conflicts table in the journal to determine
+ * a conflict's base file.
*/
- OCSYNC_EXPORT bool shouldUploadConflictFiles();
+ OCSYNC_EXPORT QByteArray conflictFileBaseName(const QByteArray &conflictName);
#ifdef Q_OS_WIN
OCSYNC_EXPORT QVariant registryGetKeyValue(HKEY hRootKey, const QString &subKey, const QString &valueName);
diff --git a/src/csync/csync_exclude.cpp b/src/csync/csync_exclude.cpp
index 762a8eae4..f113a354e 100644
--- a/src/csync/csync_exclude.cpp
+++ b/src/csync/csync_exclude.cpp
@@ -218,7 +218,7 @@ bool csync_is_windows_reserved_word(const char *filename)
return false;
}
-static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const char *path)
+static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const char *path, bool excludeConflictFiles)
{
const char *bname = NULL;
size_t blen = 0;
@@ -313,11 +313,9 @@ static CSYNC_EXCLUDE_TYPE _csync_excluded_common(const char *path)
}
}
- if (!OCC::Utility::shouldUploadConflictFiles()) {
- if (OCC::Utility::isConflictFile(bname)) {
- match = CSYNC_FILE_EXCLUDE_CONFLICT;
- goto out;
- }
+ if (excludeConflictFiles && OCC::Utility::isConflictFile(bname)) {
+ match = CSYNC_FILE_EXCLUDE_CONFLICT;
+ goto out;
}
out:
@@ -341,6 +339,11 @@ void ExcludedFiles::addExcludeFilePath(const QString &path)
_excludeFiles.insert(path);
}
+void ExcludedFiles::setExcludeConflictFiles(bool onoff)
+{
+ _excludeConflictFiles = onoff;
+}
+
void ExcludedFiles::addManualExclude(const QByteArray &expr)
{
_manualExcludes.append(expr);
@@ -408,7 +411,7 @@ bool ExcludedFiles::isExcluded(
CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const char *path, ItemType filetype) const
{
- auto match = _csync_excluded_common(path);
+ auto match = _csync_excluded_common(path, _excludeConflictFiles);
if (match != CSYNC_NOT_EXCLUDED)
return match;
if (_allExcludes.isEmpty())
@@ -454,7 +457,7 @@ CSYNC_EXCLUDE_TYPE ExcludedFiles::traversalPatternMatch(const char *path, ItemTy
CSYNC_EXCLUDE_TYPE ExcludedFiles::fullPatternMatch(const char *path, ItemType filetype) const
{
- auto match = _csync_excluded_common(path);
+ auto match = _csync_excluded_common(path, _excludeConflictFiles);
if (match != CSYNC_NOT_EXCLUDED)
return match;
if (_allExcludes.isEmpty())
diff --git a/src/csync/csync_exclude.h b/src/csync/csync_exclude.h
index 9f1c544d7..6f53e64a0 100644
--- a/src/csync/csync_exclude.h
+++ b/src/csync/csync_exclude.h
@@ -77,6 +77,13 @@ public:
void addExcludeFilePath(const QString &path);
/**
+ * Whether conflict files shall be excluded.
+ *
+ * Defaults to true.
+ */
+ void setExcludeConflictFiles(bool onoff);
+
+ /**
* Checks whether a file or directory should be excluded.
*
* @param filePath the absolute path to the file
@@ -191,6 +198,8 @@ private:
QRegularExpression _fullRegexFile;
QRegularExpression _fullRegexDir;
+ bool _excludeConflictFiles = true;
+
friend class ExcludedFilesTest;
};
diff --git a/src/csync/csync_private.h b/src/csync/csync_private.h
index 9a0276507..5463b7e40 100644
--- a/src/csync/csync_private.h
+++ b/src/csync/csync_private.h
@@ -203,6 +203,8 @@ struct OCSYNC_EXPORT csync_s {
bool ignore_hidden_files = true;
+ bool upload_conflict_files = false;
+
csync_s(const char *localUri, OCC::SyncJournalDb *statedb);
~csync_s();
int reinitialize();
diff --git a/src/csync/csync_update.cpp b/src/csync/csync_update.cpp
index ad58ef5b6..2a96faeff 100644
--- a/src/csync/csync_update.cpp
+++ b/src/csync/csync_update.cpp
@@ -400,7 +400,7 @@ out:
// If conflict files are uploaded, they won't be marked as IGNORE / CSYNC_FILE_EXCLUDE_CONFLICT
// but we still want them marked!
- if (OCC::Utility::shouldUploadConflictFiles()) {
+ if (ctx->upload_conflict_files) {
if (OCC::Utility::isConflictFile(fs->path.constData())) {
fs->error_status = CSYNC_STATUS_INDIVIDUAL_IS_CONFLICT_FILE;
}
diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp
index 333410373..08c1e45ff 100644
--- a/src/libsync/capabilities.cpp
+++ b/src/libsync/capabilities.cpp
@@ -15,6 +15,7 @@
#include "capabilities.h"
#include <QVariantMap>
+#include <QDebug>
namespace OCC {
@@ -148,4 +149,14 @@ QString Capabilities::invalidFilenameRegex() const
{
return _capabilities["dav"].toMap()["invalidFilenameRegex"].toString();
}
+
+bool Capabilities::uploadConflictFiles() const
+{
+ static auto envIsSet = !qEnvironmentVariableIsEmpty("OWNCLOUD_UPLOAD_CONFLICT_FILES");
+ static int envValue = qEnvironmentVariableIntValue("OWNCLOUD_UPLOAD_CONFLICT_FILES");
+ if (envIsSet)
+ return envValue != 0;
+
+ return _capabilities["uploadConflictFiles"].toBool();
+}
}
diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h
index f9f6614a4..63a59d6c0 100644
--- a/src/libsync/capabilities.h
+++ b/src/libsync/capabilities.h
@@ -116,6 +116,11 @@ public:
*/
QString invalidFilenameRegex() const;
+ /**
+ * Whether conflict files should remain local (default) or should be uploaded.
+ */
+ bool uploadConflictFiles() const;
+
private:
QVariantMap _capabilities;
};
diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp
index e1d081141..d960b4a2d 100644
--- a/src/libsync/owncloudpropagator.cpp
+++ b/src/libsync/owncloudpropagator.cpp
@@ -739,6 +739,12 @@ void PropagatorCompositeJob::slotSubJobAbortFinished()
}
}
+void PropagatorCompositeJob::appendJob(PropagatorJob *job)
+{
+ job->setCompositeParent(this);
+ _jobsToDo.append(job);
+}
+
bool PropagatorCompositeJob::scheduleSelfOrChild()
{
if (_state == Finished) {
@@ -767,13 +773,8 @@ bool PropagatorCompositeJob::scheduleSelfOrChild()
}
// Now it's our turn, check if we have something left to do.
- if (!_jobsToDo.isEmpty()) {
- PropagatorJob *nextJob = _jobsToDo.first();
- _jobsToDo.remove(0);
- _runningJobs.append(nextJob);
- return possiblyRunNextJob(nextJob);
- }
- while (!_tasksToDo.isEmpty()) {
+ // First, convert a task to a job if necessary
+ while (_jobsToDo.isEmpty() && !_tasksToDo.isEmpty()) {
SyncFileItemPtr nextTask = _tasksToDo.first();
_tasksToDo.remove(0);
PropagatorJob *job = propagator()->createJob(nextTask);
@@ -781,9 +782,15 @@ bool PropagatorCompositeJob::scheduleSelfOrChild()
qCWarning(lcDirectory) << "Useless task found for file" << nextTask->destination() << "instruction" << nextTask->_instruction;
continue;
}
-
- _runningJobs.append(job);
- return possiblyRunNextJob(job);
+ appendJob(job);
+ break;
+ }
+ // Then run the next job
+ if (!_jobsToDo.isEmpty()) {
+ PropagatorJob *nextJob = _jobsToDo.first();
+ _jobsToDo.remove(0);
+ _runningJobs.append(nextJob);
+ return possiblyRunNextJob(nextJob);
}
// If neither us or our children had stuff left to do we could hang. Make sure
diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h
index 062b3825e..4abe8eb03 100644
--- a/src/libsync/owncloudpropagator.h
+++ b/src/libsync/owncloudpropagator.h
@@ -49,6 +49,7 @@ qint64 freeSpaceLimit();
class SyncJournalDb;
class OwncloudPropagator;
+class PropagatorCompositeJob;
/**
* @brief the base class of propagator jobs
@@ -102,6 +103,12 @@ public:
*/
virtual qint64 committedDiskSpace() const { return 0; }
+ /** Set the composite parent job
+ *
+ * Used only from PropagatorCompositeJob itself, when a job is added.
+ */
+ void setCompositeParent(PropagatorCompositeJob *job) { _compositeParent = job; }
+
public slots:
/*
* Asynchronous abort requires emit of abortFinished() signal,
@@ -128,6 +135,13 @@ signals:
void abortFinished(SyncFileItem::Status status = SyncFileItem::NormalError);
protected:
OwncloudPropagator *propagator() const;
+
+ /** If this job gets added to a composite job, this will point to the parent.
+ *
+ * That can be useful for jobs that want to spawn follow-up jobs without
+ * becoming composite jobs themselves.
+ */
+ PropagatorCompositeJob *_compositeParent = nullptr;
};
/*
@@ -214,10 +228,7 @@ public:
qDeleteAll(_runningJobs);
}
- void appendJob(PropagatorJob *job)
- {
- _jobsToDo.append(job);
- }
+ void appendJob(PropagatorJob *job);
void appendTask(const SyncFileItemPtr &item)
{
_tasksToDo.append(item);
@@ -439,7 +450,10 @@ public:
QString getFilePath(const QString &tmp_file_name) const;
+ /** Creates the job for an item.
+ */
PropagateItemJob *createJob(const SyncFileItemPtr &item);
+
void scheduleNextJob();
void reportProgress(const SyncFileItem &, quint64 bytes);
@@ -497,6 +511,7 @@ private slots:
void scheduleNextJobImpl();
signals:
+ void newItem(const SyncFileItemPtr &);
void itemCompleted(const SyncFileItemPtr &);
void progress(const SyncFileItem &, quint64 bytes);
void finished(bool success);
diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp
index 2efc537ea..2f37ca304 100644
--- a/src/libsync/propagatedownload.cpp
+++ b/src/libsync/propagatedownload.cpp
@@ -525,7 +525,7 @@ void PropagateDownloadFile::slotGetFinished()
{
propagator()->_activeJobList.removeOne(this);
- GETFileJob *job = qobject_cast<GETFileJob *>(sender());
+ GETFileJob *job = _job;
ASSERT(job);
QNetworkReply::NetworkError err = job->reply()->error();
@@ -639,6 +639,26 @@ void PropagateDownloadFile::slotGetFinished()
return;
}
+ // Did the file come with conflict headers? If so, store them now!
+ // If we download conflict files but the server doesn't send conflict
+ // headers, the record will be established by SyncEngine::conflictRecordMaintenance.
+ // (we can't reliably determine the file id of the base file here,
+ // it might still be downloaded in a parallel job and not exist in
+ // the database yet!)
+ if (job->reply()->rawHeader("OC-Conflict") == "1") {
+ _conflictRecord.path = _item->_file.toUtf8();
+ _conflictRecord.baseFileId = job->reply()->rawHeader("OC-ConflictBaseFileId");
+ _conflictRecord.baseEtag = _job->reply()->rawHeader("OC-ConflictBaseEtag");
+
+ auto mtimeHeader = _job->reply()->rawHeader("OC-ConflictBaseMtime");
+ if (!mtimeHeader.isEmpty())
+ _conflictRecord.baseModtime = mtimeHeader.toLongLong();
+
+ // We don't set it yet. That will only be done when the download finished
+ // successfully, much further down. Here we just grab the headers because the
+ // job will be deleted later.
+ }
+
// Do checksum validation for the download. If there is no checksum header, the validator
// will also emit the validated() signal to continue the flow in slot transmissionChecksumValidated()
// as this is (still) also correct.
@@ -677,7 +697,7 @@ void PropagateDownloadFile::deleteExistingFolder()
// on error, just try to move it away...
}
- QString conflictDir = FileSystem::makeConflictFileName(
+ QString conflictDir = Utility::makeConflictFileName(
existingDir, Utility::qDateTimeFromTime_t(FileSystem::getModTime(existingDir)));
emit propagator()->touchedFile(existingDir);
@@ -805,9 +825,11 @@ void PropagateDownloadFile::downloadFinished()
&& !FileSystem::fileEquals(fn, _tmpFile.fileName());
if (isConflict) {
QString renameError;
- QString conflictFileName = FileSystem::makeConflictFileName(
- fn, Utility::qDateTimeFromTime_t(FileSystem::getModTime(fn)));
- if (!FileSystem::rename(fn, conflictFileName, &renameError)) {
+ auto conflictModTime = FileSystem::getModTime(fn);
+ QString conflictFileName = Utility::makeConflictFileName(
+ _item->_file, Utility::qDateTimeFromTime_t(conflictModTime));
+ QString conflictFilePath = propagator()->getFilePath(conflictFileName);
+ if (!FileSystem::rename(fn, conflictFilePath, &renameError)) {
// If the rename fails, don't replace it.
// If the file is locked, we want to retry this sync when it
@@ -820,6 +842,35 @@ void PropagateDownloadFile::downloadFinished()
return;
}
qCInfo(lcPropagateDownload) << "Created conflict file" << fn << "->" << conflictFileName;
+
+ // Create a new conflict record. To get the base etag, we need to read it from the db.
+ ConflictRecord conflictRecord;
+ conflictRecord.path = conflictFileName.toUtf8();
+ conflictRecord.baseModtime = _item->_previousModtime;
+
+ SyncJournalFileRecord baseRecord;
+ if (propagator()->_journal->getFileRecord(_item->_originalFile, &baseRecord) && baseRecord.isValid()) {
+ conflictRecord.baseEtag = baseRecord._etag;
+ conflictRecord.baseFileId = baseRecord._fileId;
+ } else {
+ // We might very well end up with no fileid/etag for new/new conflicts
+ }
+
+ propagator()->_journal->setConflictRecord(conflictRecord);
+
+ // Create a new upload job if the new conflict file should be uploaded
+ if (propagator()->account()->capabilities().uploadConflictFiles()) {
+ SyncFileItemPtr conflictItem = SyncFileItemPtr(new SyncFileItem);
+ conflictItem->_file = conflictFileName;
+ conflictItem->_type = SyncFileItem::File;
+ conflictItem->_direction = SyncFileItem::Up;
+ conflictItem->_instruction = CSYNC_INSTRUCTION_NEW;
+ conflictItem->_modtime = conflictModTime;
+ conflictItem->_size = _item->_previousSize;
+ ASSERT(_compositeParent);
+ emit propagator()->newItem(conflictItem);
+ _compositeParent->appendTask(conflictItem);
+ }
}
FileSystem::setModTime(_tmpFile.fileName(), _item->_modtime);
@@ -886,6 +937,11 @@ void PropagateDownloadFile::downloadFinished()
// Get up to date information for the journal.
_item->_size = FileSystem::getSize(fn);
+ // Maybe what we downloaded was a conflict file? If so, set a conflict record.
+ // (the data was prepared in slotGetFinished above)
+ if (_conflictRecord.isValid())
+ propagator()->_journal->setConflictRecord(_conflictRecord);
+
updateMetadata(isConflict);
}
diff --git a/src/libsync/propagatedownload.h b/src/libsync/propagatedownload.h
index 4b8988b65..632cb0c79 100644
--- a/src/libsync/propagatedownload.h
+++ b/src/libsync/propagatedownload.h
@@ -202,6 +202,7 @@ private:
QPointer<GETFileJob> _job;
QFile _tmpFile;
bool _deleteExisting;
+ ConflictRecord _conflictRecord;
QElapsedTimer _stopwatch;
};
diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp
index 6de6316af..1b5bcc0c8 100644
--- a/src/libsync/propagateupload.cpp
+++ b/src/libsync/propagateupload.cpp
@@ -605,6 +605,19 @@ QMap<QByteArray, QByteArray> PropagateUploadFileCommon::headers()
// csync_owncloud.c's owncloud_file_id always strips the quotes.
headers["If-Match"] = '"' + _item->_etag + '"';
}
+
+ // Set up a conflict file header pointing to the original file
+ auto conflictRecord = propagator()->_journal->conflictRecord(_item->_file.toUtf8());
+ if (conflictRecord.isValid()) {
+ headers["OC-Conflict"] = "1";
+ if (!conflictRecord.baseFileId.isEmpty())
+ headers["OC-ConflictBaseFileId"] = conflictRecord.baseFileId;
+ if (conflictRecord.baseModtime != -1)
+ headers["OC-ConflictBaseMtime"] = QByteArray::number(conflictRecord.baseModtime);
+ if (!conflictRecord.baseEtag.isEmpty())
+ headers["OC-ConflictBaseEtag"] = conflictRecord.baseEtag;
+ }
+
return headers;
}
diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp
index 5e387921c..31338f4f6 100644
--- a/src/libsync/syncengine.cpp
+++ b/src/libsync/syncengine.cpp
@@ -329,6 +329,45 @@ void SyncEngine::deleteStaleErrorBlacklistEntries(const SyncFileItemVector &sync
_journal->deleteStaleErrorBlacklistEntries(blacklist_file_paths);
}
+void SyncEngine::conflictRecordMaintenance()
+{
+ // Remove stale conflict entries from the database
+ // by checking which files still exist and removing the
+ // missing ones.
+ auto conflictRecordPaths = _journal->conflictRecordPaths();
+ for (const auto &path : conflictRecordPaths) {
+ auto fsPath = _propagator->getFilePath(QString::fromUtf8(path));
+ if (!QFileInfo(fsPath).exists()) {
+ _journal->deleteConflictRecord(path);
+ }
+ }
+
+ // Did the sync see any conflict files that don't yet have records?
+ // If so, add them now.
+ //
+ // This happens when the conflicts table is new or when conflict files
+ // are downlaoded but the server doesn't send conflict headers.
+ for (const auto &path : _seenFiles) {
+ if (!Utility::isConflictFile(path))
+ continue;
+
+ auto bapath = path.toUtf8();
+ if (!conflictRecordPaths.contains(bapath)) {
+ ConflictRecord record;
+ record.path = bapath;
+
+ // Determine fileid of target file
+ auto basePath = Utility::conflictFileBaseName(bapath);
+ SyncJournalFileRecord baseRecord;
+ if (_journal->getFileRecord(basePath, &baseRecord) && baseRecord.isValid()) {
+ record.baseFileId = baseRecord._fileId;
+ }
+
+ _journal->setConflictRecord(record);
+ }
+ }
+}
+
int SyncEngine::treewalkLocal(csync_file_stat_t *file, csync_file_stat_t *other, void *data)
{
return static_cast<SyncEngine *>(data)->treewalkFile(file, other, false);
@@ -483,7 +522,7 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
break;
case CSYNC_STATUS_INDIVIDUAL_IS_CONFLICT_FILE:
item->_status = SyncFileItem::Conflict;
- if (Utility::shouldUploadConflictFiles()) {
+ if (account()->capabilities().uploadConflictFiles()) {
// For uploaded conflict files, files with no action performed on them should
// be displayed: but we mustn't overwrite the instruction if something happens
// to the file!
@@ -552,7 +591,7 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
_hasNoneFiles = true;
}
// Put none-instruction conflict files into the syncfileitem list
- if (Utility::shouldUploadConflictFiles()
+ if (account()->capabilities().uploadConflictFiles()
&& file->error_status == CSYNC_STATUS_INDIVIDUAL_IS_CONFLICT_FILE
&& item->_instruction == CSYNC_INSTRUCTION_IGNORE) {
break;
@@ -668,8 +707,6 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
checkErrorBlacklisting(*item);
}
- _progressInfo->adjustTotalsForFile(*item);
-
_needsUpdate = true;
if (other) {
@@ -677,6 +714,7 @@ int SyncEngine::treewalkFile(csync_file_stat_t *file, csync_file_stat_t *other,
item->_previousSize = other->size;
}
+ slotNewItem(item);
_syncItemMap.insert(key, item);
return re;
}
@@ -805,6 +843,9 @@ void SyncEngine::startSync()
// database creation error!
}
+ _csync_ctx->upload_conflict_files = _account->capabilities().uploadConflictFiles();
+ _excludedFiles->setExcludeConflictFiles(!_account->capabilities().uploadConflictFiles());
+
_csync_ctx->read_remote_from_db = true;
_lastLocalDiscoveryStyle = _csync_ctx->local_discovery_style;
@@ -894,6 +935,11 @@ void SyncEngine::slotRootEtagReceived(const QString &e)
}
}
+void SyncEngine::slotNewItem(const SyncFileItemPtr &item)
+{
+ _progressInfo->adjustTotalsForFile(*item);
+}
+
void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
{
if (discoveryResult < 0) {
@@ -1051,6 +1097,7 @@ void SyncEngine::slotDiscoveryJobFinished(int discoveryResult)
connect(_propagator.data(), &OwncloudPropagator::touchedFile, this, &SyncEngine::slotAddTouchedFile);
connect(_propagator.data(), &OwncloudPropagator::insufficientLocalStorage, this, &SyncEngine::slotInsufficientLocalStorage);
connect(_propagator.data(), &OwncloudPropagator::insufficientRemoteStorage, this, &SyncEngine::slotInsufficientRemoteStorage);
+ connect(_propagator.data(), &OwncloudPropagator::newItem, this, &SyncEngine::slotNewItem);
// apply the network limits to the propagator
setNetworkLimits(_uploadLimit, _downloadLimit);
@@ -1116,11 +1163,12 @@ void SyncEngine::slotFinished(bool success)
_journal->setDataFingerprint(_discoveryMainThread->_dataFingerprint);
}
- // emit the treewalk results.
if (!_journal->postSyncCleanup(_seenFiles, _temporarilyUnavailablePaths)) {
qCDebug(lcEngine) << "Cleaning of synced ";
}
+ conflictRecordMaintenance();
+
_journal->commit("All Finished.", false);
// Send final progress information even if no
diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h
index 8be936e38..5ae335b42 100644
--- a/src/libsync/syncengine.h
+++ b/src/libsync/syncengine.h
@@ -161,6 +161,15 @@ signals:
private slots:
void slotFolderDiscovered(bool local, const QString &folder);
void slotRootEtagReceived(const QString &);
+
+ /** Called when a SyncFileItem gets accepted for a sync.
+ *
+ * Mostly done in initial creation inside treewalkFile but
+ * can also be called via the propagator for items that are
+ * created during propagation.
+ */
+ void slotNewItem(const SyncFileItemPtr &item);
+
void slotItemCompleted(const SyncFileItemPtr &item);
void slotFinished(bool success);
void slotProgress(const SyncFileItem &item, quint64 curent);
@@ -200,6 +209,9 @@ private:
// Removes stale error blacklist entries from the journal.
void deleteStaleErrorBlacklistEntries(const SyncFileItemVector &syncItems);
+ // Removes stale and adds missing conflict records after sync
+ void conflictRecordMaintenance();
+
// cleanup and emit the finished signal
void finalize(bool success);
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index b62a6ee01..b23678543 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -47,6 +47,7 @@ owncloud_add_test(FileSystem "")
owncloud_add_test(Utility "")
owncloud_add_test(SyncEngine "syncenginetestutils.h")
owncloud_add_test(SyncMove "syncenginetestutils.h")
+owncloud_add_test(SyncConflict "syncenginetestutils.h")
owncloud_add_test(SyncFileStatusTracker "syncenginetestutils.h")
owncloud_add_test(ChunkingNg "syncenginetestutils.h")
owncloud_add_test(UploadReset "syncenginetestutils.h")
diff --git a/test/testsyncconflict.cpp b/test/testsyncconflict.cpp
new file mode 100644
index 000000000..0a15bb9e9
--- /dev/null
+++ b/test/testsyncconflict.cpp
@@ -0,0 +1,351 @@
+/*
+ * This software is in the public domain, furnished "as is", without technical
+ * support, and with no warranty, express or implied, as to its usefulness for
+ * any purpose.
+ *
+ */
+
+#include <QtTest>
+#include "syncenginetestutils.h"
+#include <syncengine.h>
+
+using namespace OCC;
+
+SyncFileItemPtr findItem(const QSignalSpy &spy, const QString &path)
+{
+ for (const QList<QVariant> &args : spy) {
+ auto item = args[0].value<SyncFileItemPtr>();
+ if (item->destination() == path)
+ return item;
+ }
+ return SyncFileItemPtr(new SyncFileItem);
+}
+
+bool itemSuccessful(const QSignalSpy &spy, const QString &path, const csync_instructions_e instr)
+{
+ auto item = findItem(spy, path);
+ return item->_status == SyncFileItem::Success && item->_instruction == instr;
+}
+
+bool itemConflict(const QSignalSpy &spy, const QString &path)
+{
+ auto item = findItem(spy, path);
+ return item->_status == SyncFileItem::Conflict && item->_instruction == CSYNC_INSTRUCTION_CONFLICT;
+}
+
+bool itemSuccessfulMove(const QSignalSpy &spy, const QString &path)
+{
+ return itemSuccessful(spy, path, CSYNC_INSTRUCTION_RENAME);
+}
+
+QStringList findConflicts(const FileInfo &dir)
+{
+ QStringList conflicts;
+ for (const auto &item : dir.children) {
+ if (item.name.contains("conflict")) {
+ conflicts.append(item.path());
+ }
+ }
+ return conflicts;
+}
+
+bool expectAndWipeConflict(FileModifier &local, FileInfo state, const QString path)
+{
+ PathComponents pathComponents(path);
+ auto base = state.find(pathComponents.parentDirComponents());
+ if (!base)
+ return false;
+ for (const auto &item : base->children) {
+ if (item.name.startsWith(pathComponents.fileName()) && item.name.contains("_conflict")) {
+ local.remove(item.path());
+ return true;
+ }
+ }
+ return false;
+}
+
+class TestSyncConflict : public QObject
+{
+ Q_OBJECT
+
+private slots:
+ void testNoUpload()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ fakeFolder.localModifier().setContents("A/a1", 'L');
+ fakeFolder.remoteModifier().setContents("A/a1", 'R');
+ fakeFolder.localModifier().appendByte("A/a2");
+ fakeFolder.remoteModifier().appendByte("A/a2");
+ fakeFolder.remoteModifier().appendByte("A/a2");
+ QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(expectAndWipeConflict(fakeFolder.localModifier(), fakeFolder.currentLocalState(), "A/a1"));
+ QVERIFY(expectAndWipeConflict(fakeFolder.localModifier(), fakeFolder.currentLocalState(), "A/a2"));
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ }
+
+ void testUploadAfterDownload()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ fakeFolder.syncEngine().account()->setCapabilities({ { "uploadConflictFiles", true } });
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ QMap<QByteArray, QString> conflictMap;
+ fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
+ if (op == QNetworkAccessManager::PutOperation) {
+ auto baseFileId = request.rawHeader("OC-ConflictBaseFileId");
+ if (!baseFileId.isEmpty()) {
+ auto components = request.url().toString().split('/');
+ QString conflictFile = components.mid(components.size() - 2).join('/');
+ conflictMap[baseFileId] = conflictFile;
+ }
+ }
+ return nullptr;
+ });
+
+ fakeFolder.localModifier().setContents("A/a1", 'L');
+ fakeFolder.remoteModifier().setContents("A/a1", 'R');
+ fakeFolder.localModifier().appendByte("A/a2");
+ fakeFolder.remoteModifier().appendByte("A/a2");
+ fakeFolder.remoteModifier().appendByte("A/a2");
+ QVERIFY(fakeFolder.syncOnce());
+ auto local = fakeFolder.currentLocalState();
+ auto remote = fakeFolder.currentRemoteState();
+ QCOMPARE(local, remote);
+
+ auto a1FileId = fakeFolder.remoteModifier().find("A/a1")->fileId;
+ auto a2FileId = fakeFolder.remoteModifier().find("A/a2")->fileId;
+ QVERIFY(conflictMap.contains(a1FileId));
+ QVERIFY(conflictMap.contains(a2FileId));
+ QCOMPARE(conflictMap.size(), 2);
+ QCOMPARE(Utility::conflictFileBaseName(conflictMap[a1FileId].toUtf8()), QByteArray("A/a1"));
+
+ QCOMPARE(remote.find(conflictMap[a1FileId])->contentChar, 'L');
+ QCOMPARE(remote.find("A/a1")->contentChar, 'R');
+
+ QCOMPARE(remote.find(conflictMap[a2FileId])->size, 5);
+ QCOMPARE(remote.find("A/a2")->size, 6);
+ }
+
+ void testSeparateUpload()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ fakeFolder.syncEngine().account()->setCapabilities({ { "uploadConflictFiles", true } });
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ QMap<QByteArray, QString> conflictMap;
+ fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
+ if (op == QNetworkAccessManager::PutOperation) {
+ auto baseFileId = request.rawHeader("OC-ConflictBaseFileId");
+ if (!baseFileId.isEmpty()) {
+ auto components = request.url().toString().split('/');
+ QString conflictFile = components.mid(components.size() - 2).join('/');
+ conflictMap[baseFileId] = conflictFile;
+ }
+ }
+ return nullptr;
+ });
+
+ // Explicitly add a conflict file to simulate the case where the upload of the
+ // file didn't finish in the same sync run that the conflict was created.
+ // To do that we need to create a mock conflict record.
+ auto a1FileId = fakeFolder.remoteModifier().find("A/a1")->fileId;
+ QString conflictName = QLatin1String("A/a1_conflict-me-1234");
+ fakeFolder.localModifier().insert(conflictName, 64, 'L');
+ ConflictRecord conflictRecord;
+ conflictRecord.path = conflictName.toUtf8();
+ conflictRecord.baseFileId = a1FileId;
+ fakeFolder.syncJournal().setConflictRecord(conflictRecord);
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(conflictMap.size(), 1);
+ QCOMPARE(conflictMap[a1FileId], conflictName);
+ QCOMPARE(fakeFolder.currentRemoteState().find(conflictMap[a1FileId])->contentChar, 'L');
+ conflictMap.clear();
+
+ // Now the user can locally alter the conflict file and it will be uploaded
+ // as usual.
+ fakeFolder.localModifier().setContents(conflictName, 'P');
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(conflictMap.size(), 1);
+ QCOMPARE(conflictMap[a1FileId], conflictName);
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ conflictMap.clear();
+
+ // Similarly, remote modifications of conflict files get propagated downwards
+ fakeFolder.remoteModifier().setContents(conflictName, 'Q');
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QVERIFY(conflictMap.isEmpty());
+
+ // Conflict files for conflict files!
+ auto a1ConflictFileId = fakeFolder.remoteModifier().find(conflictName)->fileId;
+ fakeFolder.remoteModifier().appendByte(conflictName);
+ fakeFolder.remoteModifier().appendByte(conflictName);
+ fakeFolder.localModifier().appendByte(conflictName);
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QCOMPARE(conflictMap.size(), 1);
+ QVERIFY(conflictMap.contains(a1ConflictFileId));
+ QCOMPARE(fakeFolder.currentRemoteState().find(conflictName)->size, 66);
+ QCOMPARE(fakeFolder.currentRemoteState().find(conflictMap[a1ConflictFileId])->size, 65);
+ conflictMap.clear();
+ }
+
+ // What happens if we download a conflict file? Is the metadata set up correctly?
+ void testDownloadingConflictFile()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ fakeFolder.syncEngine().account()->setCapabilities({ { "uploadConflictFiles", true } });
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ // With no headers from the server
+ fakeFolder.remoteModifier().insert("A/a1_conflict-1234");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ auto conflictRecord = fakeFolder.syncJournal().conflictRecord("A/a1_conflict-1234");
+ QVERIFY(conflictRecord.isValid());
+ QCOMPARE(conflictRecord.baseFileId, fakeFolder.remoteModifier().find("A/a1")->fileId);
+
+ // Now with server headers
+ QObject parent;
+ auto a2FileId = fakeFolder.remoteModifier().find("A/a2")->fileId;
+ fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
+ if (op == QNetworkAccessManager::GetOperation) {
+ auto reply = new FakeGetReply(fakeFolder.remoteModifier(), op, request, &parent);
+ reply->setRawHeader("OC-Conflict", "1");
+ reply->setRawHeader("OC-ConflictBaseFileId", a2FileId);
+ reply->setRawHeader("OC-ConflictBaseMtime", "1234");
+ reply->setRawHeader("OC-ConflictBaseEtag", "etag");
+ return reply;
+ }
+ return nullptr;
+ });
+ fakeFolder.remoteModifier().insert("A/really-a-conflict"); // doesn't look like a conflict, but headers say it is
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ conflictRecord = fakeFolder.syncJournal().conflictRecord("A/really-a-conflict");
+ QVERIFY(conflictRecord.isValid());
+ QCOMPARE(conflictRecord.baseFileId, a2FileId);
+ QCOMPARE(conflictRecord.baseModtime, 1234);
+ QCOMPARE(conflictRecord.baseEtag, QByteArray("etag"));
+ }
+
+ // Check that conflict records are removed when the file is gone
+ void testConflictRecordRemoval1()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ fakeFolder.syncEngine().account()->setCapabilities({ { "uploadConflictFiles", true } });
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ // Make conflict records
+ ConflictRecord conflictRecord;
+ conflictRecord.path = "A/a1";
+ fakeFolder.syncJournal().setConflictRecord(conflictRecord);
+ conflictRecord.path = "A/a2";
+ fakeFolder.syncJournal().setConflictRecord(conflictRecord);
+
+ // A nothing-to-sync keeps them alive
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QVERIFY(fakeFolder.syncJournal().conflictRecord("A/a1").isValid());
+ QVERIFY(fakeFolder.syncJournal().conflictRecord("A/a2").isValid());
+
+ // When the file is removed, the record is removed too
+ fakeFolder.localModifier().remove("A/a2");
+ QVERIFY(fakeFolder.syncOnce());
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+ QVERIFY(fakeFolder.syncJournal().conflictRecord("A/a1").isValid());
+ QVERIFY(!fakeFolder.syncJournal().conflictRecord("A/a2").isValid());
+ }
+
+ // Same test, but with uploadConflictFiles == false
+ void testConflictRecordRemoval2()
+ {
+ FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
+ fakeFolder.syncEngine().account()->setCapabilities({ { "uploadConflictFiles", false } });
+ QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
+
+ // Create two conflicts
+ fakeFolder.localModifier().appendByte("A/a1");
+ fakeFolder.localModifier().appendByte("A/a1");
+ fakeFolder.remoteModifier().appendByte("A/a1");
+ fakeFolder.localModifier().appendByte("A/a2");
+ fakeFolder.localModifier().appendByte("A/a2");
+ fakeFolder.remoteModifier().appendByte("A/a2");
+ QVERIFY(fakeFolder.syncOnce());
+
+ auto conflicts = findConflicts(fakeFolder.currentLocalState().children["A"]);
+ QByteArray a1conflict;
+ QByteArray a2conflict;
+ for (const auto & conflict : conflicts) {
+ if (conflict.contains("a1"))
+ a1conflict = conflict.toUtf8();
+ if (conflict.contains("a2"))
+ a2conflict = conflict.toUtf8();
+ }
+
+ // A nothing-to-sync keeps them alive
+ QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.syncJournal().conflictRecord(a1conflict).isValid());
+ QVERIFY(fakeFolder.syncJournal().conflictRecord(a2conflict).isValid());
+
+ // When the file is removed, the record is removed too
+ fakeFolder.localModifier().remove(a2conflict);
+ QVERIFY(fakeFolder.syncOnce());
+ QVERIFY(fakeFolder.syncJournal().conflictRecord(a1conflict).isValid());
+ QVERIFY(!fakeFolder.syncJournal().conflictRecord(a2conflict).isValid());
+ }
+
+ void testConflictFileBaseName_data()
+ {
+ QTest::addColumn<QString>("input");
+ QTest::addColumn<QString>("output");
+
+ QTest::newRow("")
+ << "a/b/foo"
+ << "";
+ QTest::newRow("")
+ << "a/b/foo.txt"
+ << "";
+ QTest::newRow("")
+ << "a/b/foo_conflict"
+ << "";
+ QTest::newRow("")
+ << "a/b/foo_conflict.txt"
+ << "";
+
+ QTest::newRow("")
+ << "a/b/foo_conflict-123.txt"
+ << "a/b/foo.txt";
+ QTest::newRow("")
+ << "a/b/foo_conflict-foo-123.txt"
+ << "a/b/foo.txt";
+
+ QTest::newRow("")
+ << "a/b/foo_conflict-123"
+ << "a/b/foo";
+ QTest::newRow("")
+ << "a/b/foo_conflict-foo-123"
+ << "a/b/foo";
+
+ // double conflict files
+ QTest::newRow("")
+ << "a/b/foo_conflict-123_conflict-456.txt"
+ << "a/b/foo_conflict-123.txt";
+ QTest::newRow("")
+ << "a/b/foo_conflict-foo-123_conflict-bar-456.txt"
+ << "a/b/foo_conflict-foo-123.txt";
+ }
+
+ void testConflictFileBaseName()
+ {
+ QFETCH(QString, input);
+ QFETCH(QString, output);
+ QCOMPARE(Utility::conflictFileBaseName(input.toUtf8()), output.toUtf8());
+ }
+};
+
+QTEST_GUILESS_MAIN(TestSyncConflict)
+#include "testsyncconflict.moc"
diff --git a/test/testsyncjournaldb.cpp b/test/testsyncjournaldb.cpp
index 389dcec5d..389330ffa 100644
--- a/test/testsyncjournaldb.cpp
+++ b/test/testsyncjournaldb.cpp
@@ -183,6 +183,28 @@ private slots:
QCOMPARE(record.numericFileId(), QByteArray("123456789"));
}
+ void testConflictRecord()
+ {
+ ConflictRecord record;
+ record.path = "abc";
+ record.baseFileId = "def";
+ record.baseModtime = 1234;
+ record.baseEtag = "ghi";
+
+ QVERIFY(!_db.conflictRecord(record.path).isValid());
+
+ _db.setConflictRecord(record);
+ auto newRecord = _db.conflictRecord(record.path);
+ QVERIFY(newRecord.isValid());
+ QCOMPARE(newRecord.path, record.path);
+ QCOMPARE(newRecord.baseFileId, record.baseFileId);
+ QCOMPARE(newRecord.baseModtime, record.baseModtime);
+ QCOMPARE(newRecord.baseEtag, record.baseEtag);
+
+ _db.deleteConflictRecord(record.path);
+ QVERIFY(!_db.conflictRecord(record.path).isValid());
+ }
+
private:
SyncJournalDb _db;
};