mirror of
https://github.com/nextcloud/desktop.git
synced 2025-10-26 11:17:43 +00:00
This commit adds client-side support for delta-sync, this adds a new 3rdparty submodule `gh:ahmedammar/zsync`. This zsync tree is a modified version of upstream, adding some needed support for the upload path and other requirements. If the server does not announce the required zsync capability then a full upload/download is fallen back to. Delta synchronization can be enabled/disabled using command line, config, or gui options. On both upload and download paths, a check is made for the existance of a zsync metadata file on the server for a given path. This is provided by a dav property called `zsync`, found during discovery phase. If it doesn't exist the code reverts back to a complete upload or download, i.e. previous implementations. In the case of upload, a new zsync metadata file will be uploaded as part of the chunked upload and future synchronizations will be delta-sync capable. Chunked uploads no longer use sequential file names for each chunk id, instead, they are named as the byte offset into the remote file, this is a minimally intrusive modification to allow fo delta-sync and legacy code paths to run seamlessly. A new http header OC-Total-File-Length is sent, which informs the server of the final expected size of the file not just the total transmitted bytes as reported by OC-Total-Length. The seeding and generation of the zsync metadata file is done in a separate thread since this is a cpu intensive task, ensuring main thread is not blocked. This commit closes owncloud/client#179.
157 lines
6.1 KiB
C++
157 lines
6.1 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.
|
|
*
|
|
*/
|
|
|
|
#include <QtTest>
|
|
#include "syncenginetestutils.h"
|
|
#include <syncengine.h>
|
|
#include <propagatecommonzsync.h>
|
|
|
|
using namespace OCC;
|
|
|
|
QStringList findConflicts(const FileInfo &dir)
|
|
{
|
|
QStringList conflicts;
|
|
for (const auto &item : dir.children) {
|
|
if (item.name.contains("conflict")) {
|
|
conflicts.append(item.path());
|
|
}
|
|
}
|
|
return conflicts;
|
|
}
|
|
|
|
static quint64 blockstart_from_offset(quint64 offset)
|
|
{
|
|
return offset & ~quint64(ZSYNC_BLOCKSIZE - 1);
|
|
}
|
|
|
|
class TestZsync : public QObject
|
|
{
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
|
|
void testFileDownloadSimple()
|
|
{
|
|
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
|
|
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" }, { "zsync", "1.0" } } } });
|
|
|
|
SyncOptions opt;
|
|
opt._deltaSyncEnabled = true;
|
|
opt._deltaSyncMinFileSize = 0;
|
|
fakeFolder.syncEngine().setSyncOptions(opt);
|
|
|
|
const int size = 100 * 1000 * 1000;
|
|
QByteArray metadata;
|
|
|
|
// Test 1: NEW file upload with zsync metadata
|
|
fakeFolder.localModifier().insert("A/a0", size);
|
|
fakeFolder.localModifier().appendByte("A/a0", 'X');
|
|
qsrand(QDateTime::currentDateTime().toTime_t());
|
|
for (int i = 0; i < 10; i++) {
|
|
quint64 offset = qrand() % size;
|
|
fakeFolder.localModifier().modifyByte("A/a0", offset, 'Y');
|
|
}
|
|
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *data) -> QNetworkReply * {
|
|
if (op == QNetworkAccessManager::PutOperation && request.url().toString().endsWith(".zsync")) {
|
|
metadata = data->readAll();
|
|
return new FakePutReply{ fakeFolder.uploadState(), op, request, metadata, this };
|
|
}
|
|
|
|
return nullptr;
|
|
});
|
|
QVERIFY(fakeFolder.syncOnce());
|
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
|
|
|
// Keep hold of original file contents
|
|
QFile f(fakeFolder.localPath() + "/A/a0");
|
|
f.open(QIODevice::ReadOnly);
|
|
QByteArray data = f.readAll();
|
|
f.close();
|
|
|
|
// Test 2: update local file to unchanged version and download changes
|
|
fakeFolder.localModifier().remove("A/a0");
|
|
fakeFolder.localModifier().insert("A/a0", size);
|
|
auto currentMtime = QDateTime::currentDateTimeUtc();
|
|
fakeFolder.remoteModifier().setModTime("A/a0", currentMtime);
|
|
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
|
|
QUrlQuery query(request.url());
|
|
if (op == QNetworkAccessManager::GetOperation) {
|
|
if (query.hasQueryItem("zsync")) {
|
|
return new FakeGetWithDataReply{ fakeFolder.remoteModifier(), metadata, op, request, this };
|
|
}
|
|
|
|
return new FakeGetWithDataReply{ fakeFolder.remoteModifier(), data, op, request, this };
|
|
}
|
|
|
|
return nullptr;
|
|
});
|
|
QVERIFY(fakeFolder.syncOnce());
|
|
auto conflicts = findConflicts(fakeFolder.currentLocalState().children["A"]);
|
|
QCOMPARE(conflicts.size(), 1);
|
|
for (auto c : conflicts) {
|
|
fakeFolder.localModifier().remove(c);
|
|
}
|
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
|
}
|
|
|
|
void testFileUploadSimple()
|
|
{
|
|
FakeFolder fakeFolder{ FileInfo::A12_B12_C12_S12() };
|
|
fakeFolder.syncEngine().account()->setCapabilities({ { "dav", QVariantMap{ { "chunking", "1.0" }, { "zsync", "1.0" } } } });
|
|
|
|
SyncOptions opt;
|
|
opt._deltaSyncEnabled = true;
|
|
opt._deltaSyncMinFileSize = 0;
|
|
fakeFolder.syncEngine().setSyncOptions(opt);
|
|
|
|
const int size = 100 * 1000 * 1000;
|
|
QByteArray metadata;
|
|
|
|
// Test 1: NEW file upload with zsync metadata
|
|
fakeFolder.localModifier().insert("A/a0", size);
|
|
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *data) -> QNetworkReply * {
|
|
if (op == QNetworkAccessManager::PutOperation && request.url().toString().endsWith(".zsync")) {
|
|
metadata = data->readAll();
|
|
return new FakePutReply{ fakeFolder.uploadState(), op, request, metadata, this };
|
|
}
|
|
|
|
return nullptr;
|
|
});
|
|
QVERIFY(fakeFolder.syncOnce());
|
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
|
|
|
// Test 2: Modify local contents and ensure that modified chunks are sent
|
|
QVector<quint64> mods;
|
|
qsrand(QDateTime::currentDateTime().toTime_t());
|
|
fakeFolder.localModifier().appendByte("A/a0", 'X');
|
|
mods.append(blockstart_from_offset(size + 1));
|
|
for (int i = 0; i < 10; i++) {
|
|
quint64 offset = qrand() % size;
|
|
fakeFolder.localModifier().modifyByte("A/a0", offset, 'Y');
|
|
mods.append(blockstart_from_offset(offset));
|
|
}
|
|
fakeFolder.setServerOverride([&](QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *) -> QNetworkReply * {
|
|
QUrlQuery query(request.url());
|
|
if (op == QNetworkAccessManager::GetOperation && query.hasQueryItem("zsync")) {
|
|
return new FakeGetWithDataReply{ fakeFolder.remoteModifier(), metadata, op, request, this };
|
|
}
|
|
|
|
if (request.attribute(QNetworkRequest::CustomVerbAttribute) == QLatin1String("MOVE")) {
|
|
return new FakeChunkZsyncMoveReply{ fakeFolder.uploadState(), fakeFolder.remoteModifier(), op, request, 0, mods, this };
|
|
}
|
|
|
|
return nullptr;
|
|
});
|
|
QVERIFY(fakeFolder.syncOnce());
|
|
fakeFolder.remoteModifier().appendByte("A/a0", 'X');
|
|
QCOMPARE(fakeFolder.currentLocalState(), fakeFolder.currentRemoteState());
|
|
}
|
|
};
|
|
|
|
QTEST_GUILESS_MAIN(TestZsync)
|
|
#include "testzsync.moc"
|