| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880 |
- /*
- * 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;
- virtual void setModTime(const QString &relativePath, const QDateTime &modTime) = 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));
- }
- void setModTime(const QString &relativePath, const QDateTime &modTime) override {
- OCC::FileSystem::setModTime(_rootDir.filePath(relativePath), OCC::Utility::qDateTimeToTime_t(modTime));
- }
- };
- 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));
- }
- void setModTime(const QString &relativePath, const QDateTime &modTime) override {
- FileInfo *file = findInvalidatingEtags(relativePath);
- Q_ASSERT(file);
- file->lastModified = modTime;
- }
- 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);
- setError(InternalServerError, "Internal Server Fake Error");
- 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 ¤tRemoteState() { 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(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(", ")));
- }
|