]> dgit.raspbian.org Git - nextcloud-desktop.git/commitdiff
Merge branch 'master' into dbjournal_per_account
authorChristian Kamm <mail@ckamm.de>
Tue, 22 Nov 2016 12:09:56 +0000 (13:09 +0100)
committerChristian Kamm <mail@ckamm.de>
Wed, 23 Nov 2016 15:47:56 +0000 (16:47 +0100)
28 files changed:
1  2 
csync/src/csync.c
csync/src/csync.h
csync/src/csync_exclude.c
csync/src/csync_update.c
csync/tests/csync_tests/check_csync_commit.c
csync/tests/csync_tests/check_csync_create.c
csync/tests/csync_tests/check_csync_exclude.c
csync/tests/csync_tests/check_csync_init.c
csync/tests/csync_tests/check_csync_log.c
csync/tests/csync_tests/check_csync_statedb_load.c
csync/tests/csync_tests/check_csync_statedb_query.c
csync/tests/csync_tests/check_csync_update.c
doc/architecture.rst
src/cmd/cmd.cpp
src/gui/folder.cpp
src/gui/folderman.cpp
src/gui/folderman.h
src/gui/folderwatcher_linux.cpp
src/libsync/propagateremotemove.cpp
src/libsync/syncengine.cpp
src/libsync/syncengine.h
src/libsync/syncjournaldb.cpp
src/libsync/syncjournaldb.h
src/libsync/utility.cpp
src/libsync/utility.h
test/syncenginetestutils.h
test/testfolderman.cpp
test/testutility.cpp

index 2f936608ecedaf5091a5ad011e1f61208446c585,9d850d4fb14b17b5040aa6c2b17c90834f7ff38b..3aa2bc48c8b0006b08a3dc00ced38ec95cbc88f6
@@@ -574,9 -572,7 +566,8 @@@ int csync_destroy(CSYNC *ctx) 
  
    _csync_clean_ctx(ctx);
  
 +  SAFE_FREE(ctx->statedb.file);
    SAFE_FREE(ctx->local.uri);
-   SAFE_FREE(ctx->remote.uri);
    SAFE_FREE(ctx->error_string);
  
  #ifdef WITH_ICONV
index 608d0e3a51713a48b400670ef835fcb690d0d529,ea4f3d5647a94b95e0e147387c3ea6bb2c5fe945..9bf21b7db0a2c874a2da9d8cbb0d9006ae0f028b
@@@ -327,7 -326,7 +326,7 @@@ void OCSYNC_EXPORT csync_create(CSYNC *
   *
   * @param ctx  The context to initialize.
   */
- void csync_init(CSYNC *ctx, const char *db_file);
 -void OCSYNC_EXPORT csync_init(CSYNC *ctx);
++void OCSYNC_EXPORT csync_init(CSYNC *ctx, const char *db_file);
  
  /**
   * @brief Update detection
Simple merge
index 585a266724a886ca2bf9fc3f48ae38d1eb0c7bb3,b5b34797195fbdeb05d3c29912e4fe2e5fcc90f2..2ac248d1556e3865e2d766fcc9479bae25760656
@@@ -740,7 -704,8 +704,6 @@@ int csync_ftw(CSYNC *ctx, const char *u
    }
  
    while ((dirent = csync_vio_readdir(ctx, dh))) {
 -    const char *path = NULL;
--    size_t ulen = 0;
      int flen;
      int flag;
  
index 7461232b3aaf6e09aa5f58ccc90207ddba857a18,679816af63935dd23dc6adb7dbf8f09d6f469360..16c89bf9e7cc686426e295e65be5786fbb058238
@@@ -30,35 -30,25 +30,29 @@@ static int setup(void **state) 
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
-     csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
+     csync_create(&csync, "/tmp/check_csync1");
  
      *state = csync;
 +    
 +    return 0;
  }
  
 -static void setup_module(void **state) {
 +static int setup_module(void **state) {
      CSYNC *csync;
      int rc;
  
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
-     csync_create(&csync, "/tmp/check_csync1", "dummy://foo/bar");
+     csync_create(&csync, "/tmp/check_csync1");
  
 -    csync_init(csync);
 +    csync_init(csync, "foo");
      *state = csync;
 +    
 +    return 0;
  }
  
 -static void teardown(void **state) {
 +static int teardown(void **state) {
      CSYNC *csync = *state;
      int rc;
  
      rc = system("rm -rf /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("rm -rf /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
      *state = NULL;
 +    
 +    return 0;
  }
  
  static void check_csync_commit(void **state)
index 2c24a3aff5388a69f77c594312ff112a3922668c,b0f52a59f853fd8e7e7d5cadb98e9ab63417f47b..5d5d0a82a6ab5814948dd03f1804e4f65ded7f20
  
  #define EXCLUDE_LIST_FILE SOURCEDIR"/../sync-exclude.lst"
  
 -static void setup(void **state) {
 +static int setup(void **state) {
      CSYNC *csync;
  
-     csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
+     csync_create(&csync, "/tmp/check_csync1");
  
      *state = csync;
 +    return 0;
  }
  
 -static void setup_init(void **state) {
 +static int setup_init(void **state) {
      CSYNC *csync;
      int rc;
  
index aef472abf64ad3b6c44517851216568868e47770,44dedbefd58738251cdc6c09f28aa7a5bb17ef97..41876d6381a8e98780ea92f68a024df25c8462b0
@@@ -30,32 -30,24 +30,26 @@@ static int setup(void **state) 
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
-     csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
+     csync_create(&csync, "/tmp/check_csync1");
  
      *state = csync;
 +    return 0;
  }
  
 -static void setup_module(void **state) {
 +static int setup_module(void **state) {
      CSYNC *csync;
      int rc;
  
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
-     csync_create(&csync, "/tmp/check_csync1", "dummy://foo/bar");
+     csync_create(&csync, "/tmp/check_csync1");
  
      *state = csync;
 +    return 0;
  }
  
 -static void teardown(void **state) {
 +static int teardown(void **state) {
      CSYNC *csync = *state;
      int rc;
  
      rc = system("rm -rf /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("rm -rf /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
      *state = NULL;
 +    
 +    return 0;
  }
  
  static void check_csync_init(void **state)
index 0047b6120614b60f7005d2eaeacc5347860d87e2,7ad7e1bc881de6179d7833e55e40477019cd01c4..a4abfa9b40a0c4cbddf94b43ac526b3cb5c9885b
@@@ -34,17 -33,12 +33,14 @@@ static int setup(void **state) 
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
-     csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
+     csync_create(&csync, "/tmp/check_csync1");
  
      *state = csync;
 +    
 +    return 0;
  }
  
 -static void teardown(void **state) {
 +static int teardown(void **state) {
      CSYNC *csync = *state;
      int rc;
  
      rc = system("rm -rf /tmp/check_csync1");
      assert_int_equal(rc, 0);
  
-     rc = system("rm -rf /tmp/check_csync2");
-     assert_int_equal(rc, 0);
- 
      *state = NULL;
 +    
 +    return 0;
  }
  
  static void check_log_callback(int verbosity,
index 9493b7e086da85c87ba83bcc59fd761c22ee89c6,63459e9e2e8f3bf9fec219577d555bcd1ae3d3b7..603195e8c25bd17efaf915f3f2af1788a778a41d
@@@ -34,16 -34,12 +34,12 @@@ static int setup(void **state
  
      rc = system("rm -rf /tmp/check_csync1");
      assert_int_equal(rc, 0);
-     rc = system("rm -rf /tmp/check_csync2");
-     assert_int_equal(rc, 0);
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
      rc = system("mkdir -p /tmp/check_csync");
      assert_int_equal(rc, 0);
-     csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
+     csync_create(&csync, "/tmp/check_csync1");
 -    csync_init(csync);
 +    csync_init(csync, TESTDB);
  
      sqlite3 *db = NULL;
      rc = sqlite3_open_v2(TESTDB, &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, NULL);
@@@ -110,12 -102,8 +106,10 @@@ static int teardown(void **state) 
      assert_int_equal(rc, 0);
      rc = system("rm -rf /tmp/check_csync1");
      assert_int_equal(rc, 0);
-     rc = system("rm -rf /tmp/check_csync2");
-     assert_int_equal(rc, 0);
  
      *state = NULL;
 +    
 +    return 0;
  }
  
  
index 07b16369e64611c131abe754dc2d393d77a39190,9f8c8abe58260abce7dd3db5df8735cb41d9e578..6bd731947c0ee303acfb78f924e5304bff2cf1af
@@@ -91,10 -91,8 +91,8 @@@ static int setup(void **state
      assert_int_equal(rc, 0);
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
-     csync_create(&csync, "/tmp/check_csync1", "/tmp/check_csync2");
+     csync_create(&csync, "/tmp/check_csync1");
 -    csync_init(csync);
 +    csync_init(csync, TESTDB);
  
      /* Create a new db with metadata */
      sqlite3 *db;
@@@ -124,10 -120,8 +122,8 @@@ static int setup_ftw(void **state
      assert_int_equal(rc, 0);
      rc = system("mkdir -p /tmp/check_csync1");
      assert_int_equal(rc, 0);
-     rc = system("mkdir -p /tmp/check_csync2");
-     assert_int_equal(rc, 0);
-     csync_create(&csync, "/tmp", "/tmp");
+     csync_create(&csync, "/tmp");
 -    csync_init(csync);
 +    csync_init(csync, TESTDB);
  
      sqlite3 *db = NULL;
      rc = sqlite3_open_v2(TESTDB, &db, SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE, NULL);
@@@ -168,10 -158,6 +164,8 @@@ static int teardown_rm(void **state) 
      assert_int_equal(rc, 0);
      rc = system("rm -rf /tmp/check_csync1");
      assert_int_equal(rc, 0);
-     rc = system("rm -rf /tmp/check_csync2");
-     assert_int_equal(rc, 0);
 +    
 +    return 0;
  }
  
  /* create a file stat, caller must free memory */
Simple merge
diff --cc src/cmd/cmd.cpp
Simple merge
index 5ab6fbbe4ca45148b8e5e2b972b19d3f942ccab2,8b44c1b32c9806bc21b54d6fbe400b68537b94a5..435ba16db6453c35c2a65812d574183a47d101e4
@@@ -58,9 -58,9 +58,8 @@@ Folder::Folder(const FolderDefinition& 
        , _wipeDb(false)
        , _proxyDirty(true)
        , _lastSyncDuration(0)
-       , _forceSyncOnPollTimeout(false)
        , _consecutiveFailingSyncs(0)
        , _consecutiveFollowUpSyncs(0)
 -      , _journal(definition.localPath)
        , _fileLog(new SyncRunFileLog)
  {
      qRegisterMetaType<SyncFileItemVector>("SyncFileItemVector");
@@@ -80,9 -80,8 +79,9 @@@
      checkLocalPath();
  
      _syncResult.setFolder(_definition.alias);
 +    _journal.setAccountParameterForFilePath(path(), remoteUrl(), remotePath());
  
-     _engine.reset(new SyncEngine(_accountState->account(), path(), remoteUrl(), remotePath(), &_journal));
+     _engine.reset(new SyncEngine(_accountState->account(), path(), remotePath(), &_journal));
      // pass the setting if hidden files are to be ignored, will be read in csync_update
      _engine->setIgnoreHiddenFiles(_definition.ignoreHiddenFiles);
  
Simple merge
Simple merge
Simple merge
Simple merge
index ace7dea5de9f8d3e21438dfc0a118a4b52623b22,5f10e7035b5a98f8f29ddd34a90d07f1b2018320..ddf9652ca268941892455f257271ce023fb5db14
@@@ -78,23 -77,13 +77,16 @@@ SyncEngine::SyncEngine(AccountPtr accou
  {
      qRegisterMetaType<SyncFileItem>("SyncFileItem");
      qRegisterMetaType<SyncFileItem::Status>("SyncFileItem::Status");
+     qRegisterMetaType<SyncFileStatus>("SyncFileStatus");
  
-     // We need to reconstruct the url because the path needs to be fully decoded, as csync will re-encode the path:
-     //  Remember that csync will just append the filename to the path and pass it to the vio plugin.
-     //  csync_owncloud will then re-encode everything.
- #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
-     QString url_string = _remoteUrl.scheme() + QLatin1String("://") + _remoteUrl.authority(QUrl::EncodeDelimiters) + _remoteUrl.path(QUrl::FullyDecoded);
- #else
-     // Qt4 was broken anyway as it did not encode the '#' as it should have done  (it was actually a problem when parsing the path from QUrl::setPath
-     QString url_string = _remoteUrl.toString();
- #endif
-     url_string = Utility::toCSyncScheme(url_string);
+     // Everything in the SyncEngine expects a trailing slash for the localPath.
+     Q_ASSERT(localPath.endsWith(QLatin1Char('/')));
  
-     csync_create(&_csync_ctx, localPath.toUtf8().data(), url_string.toUtf8().data());
+     csync_create(&_csync_ctx, localPath.toUtf8().data());
 -    csync_init(_csync_ctx);
 +
 +    const QString dbFile = _journal->databaseFilePath();
 +    csync_init(_csync_ctx, dbFile.toUtf8().data());
 +
      _excludedFiles.reset(new ExcludedFiles(&_csync_ctx->excludes));
      _syncFileStatusTracker.reset(new SyncFileStatusTracker(this));
  
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
index 0000000000000000000000000000000000000000,fefe05ab04d9d496cc70343adb14f6ceeb98df97..651c34f2523316791255bc4dfb0fb8d024fccd8a
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,869 +1,870 @@@
 -        _journalDb.reset(new OCC::SyncJournalDb(localPath()));
+ /*
+  *    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.
+  *
+  */
+ #pragma once
+ 
+ #include "account.h"
+ #include "creds/abstractcredentials.h"
+ #include "filesystem.h"
+ #include "syncengine.h"
+ #include "syncjournaldb.h"
+ 
+ #include <QDir>
+ #include <QNetworkReply>
+ #include <QMap>
+ #include <QtTest>
+ 
+ static const QUrl sRootUrl("owncloud://somehost/owncloud/remote.php/webdav/");
+ static const QUrl sRootUrl2("owncloud://somehost/owncloud/remote.php/dav/files/admin/");
+ static const QUrl sUploadUrl("owncloud://somehost/owncloud/remote.php/dav/uploads/admin/");
+ 
+ inline QString getFilePathFromUrl(const QUrl &url) {
+     QString path = url.path();
+     if (path.startsWith(sRootUrl.path()))
+         return path.mid(sRootUrl.path().length());
+     if (path.startsWith(sRootUrl2.path()))
+         return path.mid(sRootUrl2.path().length());
+     if (path.startsWith(sUploadUrl.path()))
+         return path.mid(sUploadUrl.path().length());
+     return {};
+ }
+ 
+ 
+ inline QString generateEtag() {
+     return QString::number(QDateTime::currentDateTime().toMSecsSinceEpoch(), 16);
+ }
+ inline QByteArray generateFileId() {
+     return QByteArray::number(qrand(), 16);
+ }
+ 
+ class PathComponents : public QStringList {
+ public:
+     PathComponents(const char *path) : PathComponents{QString::fromUtf8(path)} {}
+     PathComponents(const QString &path) : QStringList{path.split('/', QString::SkipEmptyParts)} { }
+     PathComponents(const QStringList &pathComponents) : QStringList{pathComponents} { }
+ 
+     PathComponents parentDirComponents() const {
+         return PathComponents{mid(0, size() - 1)};
+     }
+     PathComponents subComponents() const { return PathComponents{mid(1)}; }
+     QString pathRoot() const { return first(); }
+     QString fileName() const { return last(); }
+ };
+ 
+ class FileModifier
+ {
+ public:
+     virtual ~FileModifier() { }
+     virtual void remove(const QString &relativePath) = 0;
+     virtual void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') = 0;
+     virtual void setContents(const QString &relativePath, char contentChar) = 0;
+     virtual void appendByte(const QString &relativePath) = 0;
+     virtual void mkdir(const QString &relativePath) = 0;
+     virtual void rename(const QString &relativePath, const QString &relativeDestinationDirectory) = 0;
+ };
+ 
+ class DiskFileModifier : public FileModifier
+ {
+     QDir _rootDir;
+ public:
+     DiskFileModifier(const QString &rootDirPath) : _rootDir(rootDirPath) { }
+     void remove(const QString &relativePath) override {
+         QFileInfo fi{_rootDir.filePath(relativePath)};
+         if (fi.isFile())
+             QVERIFY(_rootDir.remove(relativePath));
+         else
+             QVERIFY(QDir{fi.filePath()}.removeRecursively());
+     }
+     void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
+         QFile file{_rootDir.filePath(relativePath)};
+         QVERIFY(!file.exists());
+         file.open(QFile::WriteOnly);
+         QByteArray buf(1024, contentChar);
+         for (int x = 0; x < size/buf.size(); ++x) {
+             file.write(buf);
+         }
+         file.write(buf.data(), size % buf.size());
+         file.close();
+         // Set the mtime 30 seconds in the past, for some tests that need to make sure that the mtime differs.
+         OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(QDateTime::currentDateTime().addSecs(-30)));
+         QCOMPARE(file.size(), size);
+     }
+     void setContents(const QString &relativePath, char contentChar) override {
+         QFile file{_rootDir.filePath(relativePath)};
+         QVERIFY(file.exists());
+         qint64 size = file.size();
+         file.open(QFile::WriteOnly);
+         file.write(QByteArray{}.fill(contentChar, size));
+     }
+     void appendByte(const QString &relativePath) override {
+         QFile file{_rootDir.filePath(relativePath)};
+         QVERIFY(file.exists());
+         file.open(QFile::ReadWrite);
+         QByteArray contents = file.read(1);
+         file.seek(file.size());
+         file.write(contents);
+     }
+     void mkdir(const QString &relativePath) override {
+         _rootDir.mkpath(relativePath);
+     }
+     void rename(const QString &from, const QString &to) override {
+         QVERIFY(_rootDir.exists(from));
+         QVERIFY(_rootDir.rename(from, to));
+     }
+ };
+ 
+ class FileInfo : public FileModifier
+ {
+ public:
+     static FileInfo A12_B12_C12_S12() {
+         FileInfo fi{QString{}, {
+             {QStringLiteral("A"), {
+                 {QStringLiteral("a1"), 4},
+                 {QStringLiteral("a2"), 4}
+             }},
+             {QStringLiteral("B"), {
+                 {QStringLiteral("b1"), 16},
+                 {QStringLiteral("b2"), 16}
+             }},
+             {QStringLiteral("C"), {
+                 {QStringLiteral("c1"), 24},
+                 {QStringLiteral("c2"), 24}
+             }},
+         }};
+         FileInfo sharedFolder{QStringLiteral("S"), {
+             {QStringLiteral("s1"), 32},
+             {QStringLiteral("s2"), 32}
+         }};
+         sharedFolder.isShared = true;
+         sharedFolder.children[QStringLiteral("s1")].isShared = true;
+         sharedFolder.children[QStringLiteral("s2")].isShared = true;
+         fi.children.insert(sharedFolder.name, std::move(sharedFolder));
+         return fi;
+     }
+ 
+     FileInfo() = default;
+     FileInfo(const QString &name) : name{name} { }
+     FileInfo(const QString &name, qint64 size) : name{name}, isDir{false}, size{size} { }
+     FileInfo(const QString &name, qint64 size, char contentChar) : name{name}, isDir{false}, size{size}, contentChar{contentChar} { }
+     FileInfo(const QString &name, const std::initializer_list<FileInfo> &children) : name{name} {
+         QString p = path();
+         for (const auto &source : children) {
+             auto &dest = this->children[source.name] = source;
+             dest.parentPath = p;
+             dest.fixupParentPathRecursively();
+         }
+     }
+ 
+     void remove(const QString &relativePath) override {
+         const PathComponents pathComponents{relativePath};
+         FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+         Q_ASSERT(parent);
+         parent->children.erase(std::find_if(parent->children.begin(), parent->children.end(),
+                 [&pathComponents](const FileInfo &fi){ return fi.name == pathComponents.fileName(); }));
+     }
+ 
+     void insert(const QString &relativePath, qint64 size = 64, char contentChar = 'W') override {
+         create(relativePath, size, contentChar);
+     }
+ 
+     void setContents(const QString &relativePath, char contentChar) override {
+         FileInfo *file = findInvalidatingEtags(relativePath);
+         Q_ASSERT(file);
+         file->contentChar = contentChar;
+     }
+ 
+     void appendByte(const QString &relativePath) override {
+         FileInfo *file = findInvalidatingEtags(relativePath);
+         Q_ASSERT(file);
+         file->size += 1;
+     }
+ 
+     void mkdir(const QString &relativePath) override {
+         createDir(relativePath);
+     }
+ 
+     void rename(const QString &oldPath, const QString &newPath) override {
+         const PathComponents newPathComponents{newPath};
+         FileInfo *dir = findInvalidatingEtags(newPathComponents.parentDirComponents());
+         Q_ASSERT(dir);
+         Q_ASSERT(dir->isDir);
+         const PathComponents pathComponents{oldPath};
+         FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+         Q_ASSERT(parent);
+         FileInfo fi = parent->children.take(pathComponents.fileName());
+         fi.parentPath = dir->path();
+         fi.name = newPathComponents.fileName();
+         fi.fixupParentPathRecursively();
+         dir->children.insert(newPathComponents.fileName(), std::move(fi));
+     }
+ 
+     FileInfo *find(const PathComponents &pathComponents, const bool invalidateEtags = false) {
+         if (pathComponents.isEmpty()) {
+             if (invalidateEtags)
+                 etag = generateEtag();
+             return this;
+         }
+         QString childName = pathComponents.pathRoot();
+         auto it = children.find(childName);
+         if (it != children.end()) {
+             auto file = it->find(pathComponents.subComponents(), invalidateEtags);
+             if (file && invalidateEtags)
+                 // Update parents on the way back
+                 etag = file->etag;
+             return file;
+         }
+         return nullptr;
+     }
+ 
+     FileInfo *createDir(const QString &relativePath) {
+         const PathComponents pathComponents{relativePath};
+         FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+         Q_ASSERT(parent);
+         FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName()};
+         child.parentPath = parent->path();
+         child.etag = generateEtag();
+         return &child;
+     }
+ 
+     FileInfo *create(const QString &relativePath, qint64 size, char contentChar) {
+         const PathComponents pathComponents{relativePath};
+         FileInfo *parent = findInvalidatingEtags(pathComponents.parentDirComponents());
+         Q_ASSERT(parent);
+         FileInfo &child = parent->children[pathComponents.fileName()] = FileInfo{pathComponents.fileName(), size};
+         child.parentPath = parent->path();
+         child.contentChar = contentChar;
+         child.etag = generateEtag();
+         return &child;
+     }
+ 
+     bool operator<(const FileInfo &other) const {
+         return name < other.name;
+     }
+ 
+     bool operator==(const FileInfo &other) const {
+         // Consider files to be equal between local<->remote as a user would.
+         return name == other.name
+             && isDir == other.isDir
+             && size == other.size
+             && contentChar == other.contentChar
+             && children == other.children;
+     }
+ 
+     QString path() const {
+         return (parentPath.isEmpty() ? QString() : (parentPath + '/')) + name;
+     }
+ 
+     QString name;
+     bool isDir = true;
+     bool isShared = false;
+     QDateTime lastModified = QDateTime::currentDateTime().addDays(-7);
+     QString etag = generateEtag();
+     QByteArray fileId = generateFileId();
+     qint64 size = 0;
+     char contentChar = 'W';
+ 
+     // Sorted by name to be able to compare trees
+     QMap<QString, FileInfo> children;
+     QString parentPath;
+ 
+ private:
+     FileInfo *findInvalidatingEtags(const PathComponents &pathComponents) {
+         return find(pathComponents, true);
+     }
+ 
+     void fixupParentPathRecursively() {
+         auto p = path();
+         for (auto it = children.begin(); it != children.end(); ++it) {
+             Q_ASSERT(it.key() == it->name);
+             it->parentPath = p;
+             it->fixupParentPathRecursively();
+         }
+     }
+ 
+     friend inline QDebug operator<<(QDebug dbg, const FileInfo& fi) {
+         return dbg << "{ " << fi.path() << ": " << fi.children;
+     }
+ };
+ 
+ class FakePropfindReply : public QNetworkReply
+ {
+     Q_OBJECT
+ public:
+     QByteArray payload;
+ 
+     FakePropfindReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+     : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+ 
+         QString fileName = getFilePathFromUrl(request.url());
+         Q_ASSERT(!fileName.isNull()); // for root, it should be empty
+         const FileInfo *fileInfo = remoteRootFileInfo.find(fileName);
+         Q_ASSERT(fileInfo);
+         QString prefix = request.url().path().left(request.url().path().size() - fileName.size());
+ 
+         // Don't care about the request and just return a full propfind
+         const QString davUri{QStringLiteral("DAV:")};
+         const QString ocUri{QStringLiteral("http://owncloud.org/ns")};
+         QBuffer buffer{&payload};
+         buffer.open(QIODevice::WriteOnly);
+         QXmlStreamWriter xml( &buffer );
+         xml.writeNamespace(davUri, "d");
+         xml.writeNamespace(ocUri, "oc");
+         xml.writeStartDocument();
+         xml.writeStartElement(davUri, QStringLiteral("multistatus"));
+         auto writeFileResponse = [&](const FileInfo &fileInfo) {
+             xml.writeStartElement(davUri, QStringLiteral("response"));
+ 
+             xml.writeTextElement(davUri, QStringLiteral("href"), prefix + fileInfo.path());
+             xml.writeStartElement(davUri, QStringLiteral("propstat"));
+             xml.writeStartElement(davUri, QStringLiteral("prop"));
+ 
+             if (fileInfo.isDir) {
+                 xml.writeStartElement(davUri, QStringLiteral("resourcetype"));
+                 xml.writeEmptyElement(davUri, QStringLiteral("collection"));
+                 xml.writeEndElement(); // resourcetype
+             } else
+                 xml.writeEmptyElement(davUri, QStringLiteral("resourcetype"));
+ 
+             auto gmtDate = fileInfo.lastModified.toTimeZone(QTimeZone("GMT"));
+             auto stringDate = gmtDate.toString("ddd, dd MMM yyyy HH:mm:ss 'GMT'");
+             xml.writeTextElement(davUri, QStringLiteral("getlastmodified"), stringDate);
+             xml.writeTextElement(davUri, QStringLiteral("getcontentlength"), QString::number(fileInfo.size));
+             xml.writeTextElement(davUri, QStringLiteral("getetag"), fileInfo.etag);
+             xml.writeTextElement(ocUri, QStringLiteral("permissions"), fileInfo.isShared ? QStringLiteral("SRDNVCKW") : QStringLiteral("RDNVCKW"));
+             xml.writeTextElement(ocUri, QStringLiteral("id"), fileInfo.fileId);
+             xml.writeEndElement(); // prop
+             xml.writeTextElement(davUri, QStringLiteral("status"), "HTTP/1.1 200 OK");
+             xml.writeEndElement(); // propstat
+             xml.writeEndElement(); // response
+         };
+ 
+         writeFileResponse(*fileInfo);
+         foreach(const FileInfo &childFileInfo, fileInfo->children)
+            writeFileResponse(childFileInfo);
+         xml.writeEndElement(); // multistatus
+         xml.writeEndDocument();
+ 
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
+         setHeader(QNetworkRequest::ContentTypeHeader, "application/xml; charset=utf-8");
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 207);
+         setFinished(true);
+         emit metaDataChanged();
+         if (bytesAvailable())
+             emit readyRead();
+         emit finished();
+     }
+ 
+     void abort() override { }
+ 
+     qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
+     qint64 readData(char *data, qint64 maxlen) override {
+         qint64 len = std::min(qint64{payload.size()}, maxlen);
+         strncpy(data, payload.constData(), len);
+         payload.remove(0, len);
+         return len;
+     }
+ };
+ 
+ class FakePutReply : public QNetworkReply
+ {
+     Q_OBJECT
+     FileInfo *fileInfo;
+ public:
+     FakePutReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, const QByteArray &putPayload, QObject *parent)
+     : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+ 
+         QString fileName = getFilePathFromUrl(request.url());
+         Q_ASSERT(!fileName.isEmpty());
+         if ((fileInfo = remoteRootFileInfo.find(fileName))) {
+             fileInfo->size = putPayload.size();
+             fileInfo->contentChar = putPayload.at(0);
+         } else {
+             // Assume that the file is filled with the same character
+             fileInfo = remoteRootFileInfo.create(fileName, putPayload.size(), putPayload.at(0));
+         }
+ 
+         if (!fileInfo) {
+             abort();
+             return;
+         }
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         emit uploadProgress(fileInfo->size, fileInfo->size);
+         setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
+         setRawHeader("ETag", fileInfo->etag.toLatin1());
+         setRawHeader("X-OC-MTime", "accepted"); // Prevents Q_ASSERT(!_runningNow) since we'll call PropagateItemJob::done twice in that case.
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
+         emit metaDataChanged();
+         emit finished();
+     }
+ 
+     void abort() override { }
+     qint64 readData(char *, qint64) override { return 0; }
+ };
+ 
+ class FakeMkcolReply : public QNetworkReply
+ {
+     Q_OBJECT
+     FileInfo *fileInfo;
+ public:
+     FakeMkcolReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+     : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+ 
+         QString fileName = getFilePathFromUrl(request.url());
+         Q_ASSERT(!fileName.isEmpty());
+         fileInfo = remoteRootFileInfo.createDir(fileName);
+ 
+         if (!fileInfo) {
+             abort();
+             return;
+         }
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         setRawHeader("OC-FileId", fileInfo->fileId);
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
+         emit metaDataChanged();
+         emit finished();
+     }
+ 
+     void abort() override { }
+     qint64 readData(char *, qint64) override { return 0; }
+ };
+ 
+ class FakeDeleteReply : public QNetworkReply
+ {
+     Q_OBJECT
+ public:
+     FakeDeleteReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+     : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+ 
+         QString fileName = getFilePathFromUrl(request.url());
+         Q_ASSERT(!fileName.isEmpty());
+         remoteRootFileInfo.remove(fileName);
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 204);
+         emit metaDataChanged();
+         emit finished();
+     }
+ 
+     void abort() override { }
+     qint64 readData(char *, qint64) override { return 0; }
+ };
+ 
+ class FakeMoveReply : public QNetworkReply
+ {
+     Q_OBJECT
+ public:
+     FakeMoveReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+     : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+ 
+         QString fileName = getFilePathFromUrl(request.url());
+         Q_ASSERT(!fileName.isEmpty());
+         QString dest = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
+         Q_ASSERT(!dest.isEmpty());
+         remoteRootFileInfo.rename(fileName, dest);
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
+         emit metaDataChanged();
+         emit finished();
+     }
+ 
+     void abort() override { }
+     qint64 readData(char *, qint64) override { return 0; }
+ };
+ 
+ class FakeGetReply : public QNetworkReply
+ {
+     Q_OBJECT
+ public:
+     const FileInfo *fileInfo;
+     QByteArray payload;
+ 
+     FakeGetReply(FileInfo &remoteRootFileInfo, QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+     : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+ 
+         QString fileName = getFilePathFromUrl(request.url());
+         Q_ASSERT(!fileName.isEmpty());
+         fileInfo = remoteRootFileInfo.find(fileName);
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         payload.fill(fileInfo->contentChar, fileInfo->size);
+         setHeader(QNetworkRequest::ContentLengthHeader, payload.size());
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 200);
+         setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
+         setRawHeader("ETag", fileInfo->etag.toLatin1());
+         setRawHeader("OC-FileId", fileInfo->fileId);
+         emit metaDataChanged();
+         if (bytesAvailable())
+             emit readyRead();
+         emit finished();
+     }
+ 
+     void abort() override { }
+     qint64 bytesAvailable() const override { return payload.size() + QIODevice::bytesAvailable(); }
+ 
+     qint64 readData(char *data, qint64 maxlen) override {
+         qint64 len = std::min(qint64{payload.size()}, maxlen);
+         strncpy(data, payload.constData(), len);
+         payload.remove(0, len);
+         return len;
+     }
+ };
+ 
+ 
+ class FakeChunkMoveReply : public QNetworkReply
+ {
+     Q_OBJECT
+     FileInfo *fileInfo;
+ public:
+     FakeChunkMoveReply(FileInfo &uploadsFileInfo, FileInfo &remoteRootFileInfo,
+                        QNetworkAccessManager::Operation op, const QNetworkRequest &request,
+                        QObject *parent) : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+ 
+         QString source = getFilePathFromUrl(request.url());
+         Q_ASSERT(!source.isEmpty());
+         Q_ASSERT(source.endsWith("/.file"));
+         source = source.left(source.length() - qstrlen("/.file"));
+         auto sourceFolder = uploadsFileInfo.find(source);
+         Q_ASSERT(sourceFolder);
+         Q_ASSERT(sourceFolder->isDir);
+         int count = 0;
+         int size = 0;
+         char payload = '*';
+ 
+         do {
+             if (!sourceFolder->children.contains(QString::number(count)))
+                 break;
+             auto &x = sourceFolder->children[QString::number(count)];
+             Q_ASSERT(!x.isDir);
+             Q_ASSERT(x.size > 0); // There should not be empty chunks
+             size += x.size;
+             payload = x.contentChar;
+             ++count;
+         } while(true);
+ 
+         Q_ASSERT(count > 1); // There should be at least two chunks, otherwise why would we use chunking?
+         QCOMPARE(sourceFolder->children.count(), count); // There should not be holes or extra files
+ 
+         QString fileName = getFilePathFromUrl(QUrl::fromEncoded(request.rawHeader("Destination")));
+         Q_ASSERT(!fileName.isEmpty());
+ 
+         if ((fileInfo = remoteRootFileInfo.find(fileName))) {
+             QCOMPARE(request.rawHeader("If"), QByteArray("<" + request.rawHeader("Destination") + "> ([\"" + fileInfo->etag.toLatin1() + "\"])"));
+             fileInfo->size = size;
+             fileInfo->contentChar = payload;
+         } else {
+             Q_ASSERT(!request.hasRawHeader("If"));
+             // Assume that the file is filled with the same character
+             fileInfo = remoteRootFileInfo.create(fileName, size, payload);
+         }
+ 
+         if (!fileInfo) {
+             abort();
+             return;
+         }
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 201);
+         setRawHeader("OC-ETag", fileInfo->etag.toLatin1());
+         setRawHeader("ETag", fileInfo->etag.toLatin1());
+         setRawHeader("OC-FileId", fileInfo->fileId);
+         emit metaDataChanged();
+         emit finished();
+     }
+ 
+     void abort() override { }
+     qint64 readData(char *, qint64) override { return 0; }
+ };
+ 
+ 
+ class FakeErrorReply : public QNetworkReply
+ {
+     Q_OBJECT
+ public:
+     FakeErrorReply(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QObject *parent)
+     : QNetworkReply{parent} {
+         setRequest(request);
+         setUrl(request.url());
+         setOperation(op);
+         open(QIODevice::ReadOnly);
+         QMetaObject::invokeMethod(this, "respond", Qt::QueuedConnection);
+     }
+ 
+     Q_INVOKABLE void respond() {
+         setAttribute(QNetworkRequest::HttpStatusCodeAttribute, 500);
+         emit metaDataChanged();
+         emit finished();
+     }
+ 
+     void abort() override { }
+     qint64 readData(char *, qint64) override { return 0; }
+ };
+ 
+ class FakeQNAM : public QNetworkAccessManager
+ {
+     FileInfo _remoteRootFileInfo;
+     FileInfo _uploadFileInfo;
+     QStringList _errorPaths;
+ public:
+     FakeQNAM(FileInfo initialRoot) : _remoteRootFileInfo{std::move(initialRoot)} { }
+     FileInfo &currentRemoteState() { return _remoteRootFileInfo; }
+     FileInfo &uploadState() { return _uploadFileInfo; }
+     QStringList &errorPaths() { return _errorPaths; }
+ 
+ protected:
+     QNetworkReply *createRequest(Operation op, const QNetworkRequest &request,
+                                          QIODevice *outgoingData = 0) {
+         const QString fileName = getFilePathFromUrl(request.url());
+         Q_ASSERT(!fileName.isNull());
+         if (_errorPaths.contains(fileName))
+             return new FakeErrorReply{op, request, this};
+ 
+         bool isUpload = request.url().path().startsWith(sUploadUrl.path());
+         FileInfo &info = isUpload ? _uploadFileInfo : _remoteRootFileInfo;
+ 
+         auto verb = request.attribute(QNetworkRequest::CustomVerbAttribute);
+         if (verb == QLatin1String("PROPFIND"))
+             // Ignore outgoingData always returning somethign good enough, works for now.
+             return new FakePropfindReply{info, op, request, this};
+         else if (verb == QLatin1String("GET"))
+             return new FakeGetReply{info, op, request, this};
+         else if (verb == QLatin1String("PUT"))
+             return new FakePutReply{info, op, request, outgoingData->readAll(), this};
+         else if (verb == QLatin1String("MKCOL"))
+             return new FakeMkcolReply{info, op, request, this};
+         else if (verb == QLatin1String("DELETE"))
+             return new FakeDeleteReply{info, op, request, this};
+         else if (verb == QLatin1String("MOVE") && !isUpload)
+             return new FakeMoveReply{info, op, request, this};
+         else if (verb == QLatin1String("MOVE") && isUpload)
+             return new FakeChunkMoveReply{info, _remoteRootFileInfo, op, request, this};
+         else {
+             qDebug() << verb << outgoingData;
+             Q_UNREACHABLE();
+         }
+     }
+ };
+ 
+ class FakeCredentials : public OCC::AbstractCredentials
+ {
+     QNetworkAccessManager *_qnam;
+ public:
+     FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { }
+     virtual QString authType() const { return "test"; }
+     virtual QString user() const { return "admin"; }
+     virtual QNetworkAccessManager* getQNAM() const { return _qnam; }
+     virtual bool ready() const { return true; }
+     virtual void fetchFromKeychain() { }
+     virtual void askFromUser() { }
+     virtual bool stillValid(QNetworkReply *) { return true; }
+     virtual void persist() { }
+     virtual void invalidateToken() { }
+     virtual void forgetSensitiveData() { }
+ };
+ 
+ class FakeFolder
+ {
+     QTemporaryDir _tempDir;
+     DiskFileModifier _localModifier;
+     // FIXME: Clarify ownership, double delete
+     FakeQNAM *_fakeQnam;
+     OCC::AccountPtr _account;
+     std::unique_ptr<OCC::SyncJournalDb> _journalDb;
+     std::unique_ptr<OCC::SyncEngine> _syncEngine;
+ 
+ public:
+     FakeFolder(const FileInfo &fileTemplate)
+         : _localModifier(_tempDir.path())
+     {
+         // Needs to be done once
+         OCC::SyncEngine::minimumFileAgeForUpload = 0;
+         csync_set_log_level(11);
+ 
+         QDir rootDir{_tempDir.path()};
+         toDisk(rootDir, fileTemplate);
+ 
+         _fakeQnam = new FakeQNAM(fileTemplate);
+         _account = OCC::Account::create();
+         _account->setUrl(QUrl(QStringLiteral("http://admin:admin@localhost/owncloud")));
+         _account->setCredentials(new FakeCredentials{_fakeQnam});
+ 
++        _journalDb.reset(new OCC::SyncJournalDb());
++        _journalDb->setDatabaseFilePath(localPath() + ".sync_test.db");
+         _syncEngine.reset(new OCC::SyncEngine(_account, localPath(), "", _journalDb.get()));
+ 
+         // A new folder will update the local file state database on first sync.
+         // To have a state matching what users will encounter, we have to a sync
+         // using an identical local/remote file tree first.
+         syncOnce();
+     }
+ 
+     OCC::SyncEngine &syncEngine() const { return *_syncEngine; }
+ 
+     FileModifier &localModifier() { return _localModifier; }
+     FileModifier &remoteModifier() { return _fakeQnam->currentRemoteState(); }
+     FileInfo currentLocalState() {
+         QDir rootDir{_tempDir.path()};
+         FileInfo rootTemplate;
+         fromDisk(rootDir, rootTemplate);
+         return rootTemplate;
+     }
+ 
+     FileInfo currentRemoteState() { return _fakeQnam->currentRemoteState(); }
+     FileInfo &uploadState() { return _fakeQnam->uploadState(); }
+ 
+     QStringList &serverErrorPaths() { return _fakeQnam->errorPaths(); }
+ 
+     QString localPath() const {
+         // SyncEngine wants a trailing slash
+         if (_tempDir.path().endsWith('/'))
+             return _tempDir.path();
+         return _tempDir.path() + '/';
+     }
+ 
+     void scheduleSync() {
+         // Have to be done async, else, an error before exec() does not terminate the event loop.
+         QMetaObject::invokeMethod(_syncEngine.get(), "startSync", Qt::QueuedConnection);
+     }
+ 
+     void execUntilBeforePropagation() {
+         QSignalSpy spy(_syncEngine.get(), SIGNAL(aboutToPropagate(SyncFileItemVector&)));
+         QVERIFY(spy.wait());
+     }
+ 
+     void execUntilItemCompleted(const QString &relativePath) {
+         QSignalSpy spy(_syncEngine.get(), SIGNAL(itemCompleted(const SyncFileItem &, const PropagatorJob &)));
+         QElapsedTimer t;
+         t.start();
+         while (t.elapsed() < 5000) {
+             spy.clear();
+             QVERIFY(spy.wait());
+             for(const QList<QVariant> &args : spy) {
+                 auto item = args[0].value<OCC::SyncFileItem>();
+                 if (item.destination() == relativePath)
+                     return;
+             }
+         }
+         QVERIFY(false);
+     }
+ 
+     bool execUntilFinished() {
+         QSignalSpy spy(_syncEngine.get(), SIGNAL(finished(bool)));
+         bool ok = spy.wait(60000);
+         Q_ASSERT(ok && "Sync timed out");
+         return spy[0][0].toBool();
+     }
+ 
+     bool syncOnce() {
+         scheduleSync();
+         return execUntilFinished();
+     }
+ 
+ private:
+     static void toDisk(QDir &dir, const FileInfo &templateFi) {
+         foreach (const FileInfo &child, templateFi.children) {
+             if (child.isDir) {
+                 QDir subDir(dir);
+                 dir.mkdir(child.name);
+                 subDir.cd(child.name);
+                 toDisk(subDir, child);
+             } else {
+                 QFile file{dir.filePath(child.name)};
+                 file.open(QFile::WriteOnly);
+                 file.write(QByteArray{}.fill(child.contentChar, child.size));
+                 file.close();
+                 OCC::FileSystem::setModTime(file.fileName(), OCC::Utility::qDateTimeToTime_t(child.lastModified));
+             }
+         }
+     }
+ 
+     static void fromDisk(QDir &dir, FileInfo &templateFi) {
+         foreach (const QFileInfo &diskChild, dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot)) {
+             if (diskChild.isDir()) {
+                 QDir subDir = dir;
+                 subDir.cd(diskChild.fileName());
+                 templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName()});
+                 fromDisk(subDir, templateFi.children.last());
+             } else {
+                 QFile f{diskChild.filePath()};
+                 f.open(QFile::ReadOnly);
+                 char contentChar = f.read(1).at(0);
+                 templateFi.children.insert(diskChild.fileName(), FileInfo{diskChild.fileName(), diskChild.size(), contentChar});
+             }
+         }
+     }
+ };
+ 
+ // QTest::toString overloads
+ namespace OCC {
+     inline char *toString(const SyncFileStatus &s) {
+         return QTest::toString(QString("SyncFileStatus(" + s.toSocketAPIString() + ")"));
+     }
+ }
+ 
+ inline void addFiles(QStringList &dest, const FileInfo &fi)
+ {
+     if (fi.isDir) {
+         dest += QString("%1 - dir").arg(fi.name);
+         foreach (const FileInfo &fi, fi.children)
+             addFiles(dest, fi);
+     } else {
+         dest += QString("%1 - %2 %3-bytes").arg(fi.name).arg(fi.size).arg(fi.contentChar);
+     }
+ }
+ 
+ inline char *toString(const FileInfo &fi)
+ {
+     QStringList files;
+     foreach (const FileInfo &fi, fi.children)
+         addFiles(files, fi);
+     return QTest::toString(QString("FileInfo with %1 files(%2)").arg(files.size()).arg(files.join(", ")));
+ }
index 1d837d87330952d02edaec3e6265e30946135241,d753d59773ade790c9963af1c502f1e4e43ef051..16dc81446fd35d8a9e4b3ff25f9c65af457564d5
  #include "folderman.h"
  #include "account.h"
  #include "accountstate.h"
+ #include "configfile.h"
 +#include "creds/httpcredentials.h"
  
- 
  using namespace OCC;
  
 +class HttpCredentialsTest : public HttpCredentials {
 +public:
 +    HttpCredentialsTest(const QString& user, const QString& password)
 +        : HttpCredentials(user, password, "", "")
 +    {}
 +
 +    void askFromUser() Q_DECL_OVERRIDE {
 +
 +    }
 +};
  
  static FolderDefinition folderDefinition(const QString &path) {
      FolderDefinition d;
Simple merge