mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 23:28:14 +03:00
a1dc4069c9
We are going to change the webdav path depending on the capabilities. But the SyncEngine and csync might have been created before the capabilities are retrieved. The main raison why we gave the path to the sync engine was to pass it to csync. But the thing is that csync don't need anymore this url as everything is done by the discovery classes in libsync that use the network jobs that use the account for the urls. So csync do not need the remote URI. shortenFilename in folderstatusmodel.cpp was useless because the string is the _file of a SyncFileItem which is the relative file name, that name never starts with owncloud://. All the csync test creates the folder because csync use to check if the folder exists. But we don't need to do that anymore
869 lines
32 KiB
C++
869 lines
32 KiB
C++
/*
|
|
* 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 ¤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()));
|
|
_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(", ")));
|
|
}
|