Initial QSfera import
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "checkserverjobfactory.h"
|
||||
|
||||
#include "common/utility.h"
|
||||
#include "libsync/accessmanager.h"
|
||||
#include "libsync/account.h"
|
||||
#include "libsync/cookiejar.h"
|
||||
#include "libsync/creds/abstractcredentials.h"
|
||||
#include "libsync/theme.h"
|
||||
|
||||
#include <QJsonParseError>
|
||||
|
||||
namespace {
|
||||
|
||||
// FIXME: this is not a permanent solution, eventually we want to replace the job factories with job classes so we can store such information there
|
||||
class CheckServerCoreJob : OCC::CoreJob
|
||||
{
|
||||
Q_OBJECT
|
||||
friend OCC::CheckServerJobFactory;
|
||||
|
||||
public:
|
||||
using OCC::CoreJob::CoreJob;
|
||||
|
||||
private:
|
||||
// doesn't concern users of the job factory
|
||||
// we just need a place to maintain these variables, but the factory is likely deleted before the job has finished
|
||||
bool _redirectDistinct = true;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcCheckServerJob, "sync.checkserverjob", QtInfoMsg)
|
||||
|
||||
CheckServerJobResult::CheckServerJobResult() = default;
|
||||
|
||||
CheckServerJobResult::CheckServerJobResult(const QJsonObject &statusObject, const QUrl &serverUrl)
|
||||
: _statusObject(statusObject)
|
||||
, _serverUrl(serverUrl)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonObject CheckServerJobResult::statusObject() const
|
||||
{
|
||||
return _statusObject;
|
||||
}
|
||||
|
||||
QUrl CheckServerJobResult::serverUrl() const
|
||||
{
|
||||
return _serverUrl;
|
||||
}
|
||||
|
||||
CheckServerJobFactory CheckServerJobFactory::createFromAccount(const AccountPtr &account, bool clearCookies, QObject *parent)
|
||||
{
|
||||
// in order to receive all ssl erorrs we need a fresh QNam
|
||||
auto nam = account->credentials()->createAM();
|
||||
nam->setCustomTrustedCaCertificates(account->approvedCerts());
|
||||
nam->setParent(parent);
|
||||
// do we start with the old cookies or new
|
||||
if (!(clearCookies && Theme::instance()->connectionValidatorClearCookies())) {
|
||||
const auto accountCookies = account->accessManager()->openCloudCookieJar()->allCookies();
|
||||
nam->openCloudCookieJar()->setAllCookies(accountCookies);
|
||||
}
|
||||
return CheckServerJobFactory(nam);
|
||||
}
|
||||
|
||||
CoreJob *CheckServerJobFactory::startJob(const QUrl &url, QObject *parent)
|
||||
{
|
||||
// the custom job class is used to store some state we need to maintain until the job has finished
|
||||
|
||||
auto req = makeRequest(Utility::concatUrlPath(url, QStringLiteral("status.php")));
|
||||
|
||||
req.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
req.setRawHeader(QByteArrayLiteral("OC-Connection-Validator"), QByteArrayLiteral("desktop"));
|
||||
req.setMaximumRedirectsAllowed(_maxRedirectsAllowed);
|
||||
|
||||
auto job = new CheckServerCoreJob(nam()->get(req), parent);
|
||||
|
||||
QObject::connect(job->reply(), &QNetworkReply::redirected, job, [job] {
|
||||
const auto code = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (code == 302 || code == 307) {
|
||||
job->_redirectDistinct = false;
|
||||
}
|
||||
});
|
||||
|
||||
QObject::connect(job->reply(), &QNetworkReply::finished, job, [serverUrl = url, job]() mutable {
|
||||
const QUrl targetUrl = job->reply()->url().adjusted(QUrl::RemoveFilename);
|
||||
|
||||
// TODO: still needed?
|
||||
if (targetUrl.scheme() == QLatin1String("https")
|
||||
&& job->reply()->sslConfiguration().sessionTicket().isEmpty()
|
||||
&& job->reply()->error() == QNetworkReply::NoError) {
|
||||
qCWarning(lcCheckServerJob) << u"No SSL session identifier / session ticket is used, this might impact sync performance negatively.";
|
||||
}
|
||||
|
||||
if (!Utility::urlEqual(serverUrl, targetUrl)) {
|
||||
if (job->_redirectDistinct) {
|
||||
serverUrl = targetUrl;
|
||||
} else {
|
||||
qCWarning(lcCheckServerJob) << u"We got a temporary moved server aborting";
|
||||
setJobError(job, QStringLiteral("Illegal redirect by server"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const int httpStatus = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
if (job->reply()->error() == QNetworkReply::TooManyRedirectsError) {
|
||||
qCWarning(lcCheckServerJob) << u"error:" << job->reply()->errorString();
|
||||
setJobError(job, job->reply()->errorString());
|
||||
} else if (httpStatus != 200 || job->reply()->bytesAvailable() == 0) {
|
||||
qCWarning(lcCheckServerJob) << u"error: status.php replied" << httpStatus;
|
||||
setJobError(job, QStringLiteral("Invalid HTTP status code received for status.php: %1").arg(httpStatus));
|
||||
} else {
|
||||
const QByteArray body = job->reply()->peek(4 * 1024);
|
||||
QJsonParseError error;
|
||||
auto status = QJsonDocument::fromJson(body, &error);
|
||||
// empty or invalid response
|
||||
if (error.error != QJsonParseError::NoError || status.isNull()) {
|
||||
qCWarning(lcCheckServerJob) << u"status.php from server is not valid JSON!" << body << job->reply()->request().url() << error.errorString();
|
||||
}
|
||||
|
||||
qCInfo(lcCheckServerJob) << u"status.php returns: " << status << u" " << job->reply()->error() << u" Reply: " << job->reply();
|
||||
|
||||
if (status.object().contains(QStringLiteral("installed"))) {
|
||||
CheckServerJobResult result(status.object(), serverUrl);
|
||||
setJobResult(job, QVariant::fromValue(result));
|
||||
} else {
|
||||
qCWarning(lcCheckServerJob) << u"No proper answer on " << job->reply()->url();
|
||||
setJobError(job, QStringLiteral("Did not receive expected reply from server"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
} // OCC
|
||||
|
||||
#include "checkserverjobfactory.moc"
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "abstractcorejob.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class QSFERA_SYNC_EXPORT CheckServerJobResult
|
||||
{
|
||||
|
||||
public:
|
||||
CheckServerJobResult();
|
||||
CheckServerJobResult(const QJsonObject &statusObject, const QUrl &serverUrl);
|
||||
|
||||
QJsonObject statusObject() const;
|
||||
QUrl serverUrl() const;
|
||||
|
||||
private:
|
||||
const QJsonObject _statusObject;
|
||||
const QUrl _serverUrl;
|
||||
};
|
||||
|
||||
|
||||
class QSFERA_SYNC_EXPORT CheckServerJobFactory : public AbstractCoreJobFactory
|
||||
{
|
||||
public:
|
||||
using AbstractCoreJobFactory::AbstractCoreJobFactory;
|
||||
|
||||
/**
|
||||
* clearCookies: Whether to clear the cookies before we start the CheckServerJob job
|
||||
* This option also depends on Theme::instance()->connectionValidatorClearCookies()
|
||||
*/
|
||||
static CheckServerJobFactory createFromAccount(const AccountPtr &account, bool clearCookies, QObject *parent);
|
||||
|
||||
CoreJob *startJob(const QUrl &url, QObject *parent) override;
|
||||
|
||||
private:
|
||||
int _maxRedirectsAllowed = 5;
|
||||
};
|
||||
|
||||
} // OCC
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::CheckServerJobResult)
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#include "fetchuserinfojobfactory.h"
|
||||
#include "common/utility.h"
|
||||
#include "creds/httpcredentials.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QJsonParseError>
|
||||
#include <QNetworkReply>
|
||||
#include <QStringLiteral>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcFetchUserInfoJob, "sync.networkjob.fetchuserinfojob", QtInfoMsg);
|
||||
|
||||
namespace OCC {
|
||||
|
||||
FetchUserInfoJobFactory FetchUserInfoJobFactory::fromBasicAuthCredentials(QNetworkAccessManager *nam, const QString &username, const QString &password)
|
||||
{
|
||||
QString authorizationHeader = QStringLiteral("Basic %1").arg(QString::fromUtf8(QStringLiteral("%1:%2").arg(username, password).toUtf8().toBase64()));
|
||||
return { nam, authorizationHeader };
|
||||
}
|
||||
|
||||
FetchUserInfoJobFactory FetchUserInfoJobFactory::fromOAuth2Credentials(QNetworkAccessManager *nam, const QString &bearerToken)
|
||||
{
|
||||
QString authorizationHeader = QStringLiteral("Bearer %1").arg(bearerToken);
|
||||
return { nam, authorizationHeader };
|
||||
}
|
||||
|
||||
FetchUserInfoJobFactory::FetchUserInfoJobFactory(QNetworkAccessManager *nam, const QString &authHeaderValue)
|
||||
: AbstractCoreJobFactory(nam)
|
||||
, _authorizationHeader(authHeaderValue)
|
||||
{
|
||||
}
|
||||
|
||||
CoreJob *FetchUserInfoJobFactory::startJob(const QUrl &url, QObject *parent)
|
||||
{
|
||||
QUrlQuery urlQuery({ { QStringLiteral("format"), QStringLiteral("json") } });
|
||||
|
||||
auto req = makeRequest(Utility::concatUrlPath(url, QStringLiteral("ocs/v2.php/cloud/user"), urlQuery));
|
||||
|
||||
// We are not connected yet so we need to handle the authentication manually
|
||||
req.setRawHeader("Authorization", _authorizationHeader.toUtf8());
|
||||
req.setRawHeader(QByteArrayLiteral("OCS-APIREQUEST"), QByteArrayLiteral("true"));
|
||||
|
||||
// We just added the Authorization header, don't let HttpCredentialsAccessManager tamper with it
|
||||
req.setAttribute(HttpCredentials::DontAddCredentialsAttribute, true);
|
||||
req.setAttribute(QNetworkRequest::AuthenticationReuseAttribute, QNetworkRequest::Manual);
|
||||
|
||||
auto *job = new CoreJob(nam()->get(req), parent);
|
||||
|
||||
QObject::connect(job->reply(), &QNetworkReply::finished, job, [job] {
|
||||
const auto data = job->reply()->readAll();
|
||||
const auto statusCode = job->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (job->reply()->error() != QNetworkReply::NoError || statusCode != 200) {
|
||||
setJobError(job, QApplication::translate("FetchUserInfoJobFactory", "Failed to retrieve user info"));
|
||||
} else {
|
||||
qCDebug(lcFetchUserInfoJob) << data;
|
||||
|
||||
QJsonParseError error = {};
|
||||
const auto json = QJsonDocument::fromJson(data, &error);
|
||||
|
||||
if (error.error == QJsonParseError::NoError) {
|
||||
const auto jsonData = json.object().value(QStringLiteral("ocs")).toObject().value(QStringLiteral("data")).toObject();
|
||||
|
||||
FetchUserInfoResult result(jsonData.value(QStringLiteral("id")).toString(), jsonData.value(QStringLiteral("display-name")).toString());
|
||||
|
||||
setJobResult(job, QVariant::fromValue(result));
|
||||
} else {
|
||||
setJobError(job, error.errorString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return job;
|
||||
}
|
||||
|
||||
} // OCC
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Copyright (C) Fabian Müller <fmueller@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "abstractcorejob.h"
|
||||
namespace OCC {
|
||||
|
||||
class QSFERA_SYNC_EXPORT FetchUserInfoResult
|
||||
{
|
||||
public:
|
||||
FetchUserInfoResult() = default;
|
||||
|
||||
FetchUserInfoResult(const QString &userName, const QString &displayName)
|
||||
{
|
||||
_userName = userName;
|
||||
_displayName = displayName;
|
||||
}
|
||||
|
||||
QString userName() const
|
||||
{
|
||||
return _userName;
|
||||
}
|
||||
|
||||
QString displayName() const
|
||||
{
|
||||
return _displayName;
|
||||
};
|
||||
|
||||
private:
|
||||
QString _userName;
|
||||
QString _displayName;
|
||||
};
|
||||
|
||||
class QSFERA_SYNC_EXPORT FetchUserInfoJobFactory : public AbstractCoreJobFactory
|
||||
{
|
||||
public:
|
||||
static FetchUserInfoJobFactory fromBasicAuthCredentials(QNetworkAccessManager *nam, const QString &username, const QString &token);
|
||||
static FetchUserInfoJobFactory fromOAuth2Credentials(QNetworkAccessManager *nam, const QString &bearerToken);
|
||||
|
||||
CoreJob *startJob(const QUrl &url, QObject *parent) override;
|
||||
|
||||
private:
|
||||
FetchUserInfoJobFactory(QNetworkAccessManager *nam, const QString &authHeaderValue);
|
||||
|
||||
QString _authorizationHeader;
|
||||
};
|
||||
|
||||
} // OCC
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::FetchUserInfoResult)
|
||||
@@ -0,0 +1,276 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#include "libsync/networkjobs/getfilejob.h"
|
||||
|
||||
#include "libsync/owncloudpropagator_p.h"
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
|
||||
Q_LOGGING_CATEGORY(lcGetJob, "sync.networkjob.get", QtInfoMsg)
|
||||
|
||||
// DOES NOT take ownership of the device.
|
||||
GETFileJob::GETFileJob(AccountPtr account, const QUrl &url, const QString &path, QIODevice *device, const QMap<QByteArray, QByteArray> &headers,
|
||||
const QString &expectedEtagForResume, qint64 resumeStart, QObject *parent)
|
||||
: AbstractNetworkJob(account, url, path, parent)
|
||||
, _device(device)
|
||||
, _headers(headers)
|
||||
, _expectedEtagForResume(expectedEtagForResume)
|
||||
, _expectedContentLength(-1)
|
||||
, _contentLength(-1)
|
||||
, _resumeStart(resumeStart)
|
||||
{
|
||||
connect(this, &GETFileJob::networkError, this, [this] {
|
||||
if (timedOut()) {
|
||||
qCWarning(lcGetJob) << this << u"timeout";
|
||||
_errorString = tr("Connection Timeout");
|
||||
_errorStatus = SyncFileItem::FatalError;
|
||||
}
|
||||
});
|
||||
|
||||
// Long downloads must not block non-propagation jobs.
|
||||
setPriority(QNetworkRequest::LowPriority);
|
||||
}
|
||||
|
||||
void GETFileJob::start()
|
||||
{
|
||||
if (_resumeStart > 0) {
|
||||
_headers["Range"] = "bytes=" + QByteArray::number(_resumeStart) + '-';
|
||||
_headers["Accept-Ranges"] = "bytes";
|
||||
qCDebug(lcGetJob) << u"Retry with range " << _headers["Range"];
|
||||
}
|
||||
|
||||
QNetworkRequest req;
|
||||
for (auto it = _headers.cbegin(); it != _headers.cend(); ++it) {
|
||||
req.setRawHeader(it.key(), it.value());
|
||||
}
|
||||
|
||||
if (_bandwidthManager) {
|
||||
// probably a qt bug, with http2 we might handle the input too slow causing the whole file to be buffered by qt in ram
|
||||
req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false);
|
||||
}
|
||||
sendRequest("GET", req);
|
||||
|
||||
qCDebug(lcGetJob) << _bandwidthManager << _bandwidthChoked << _bandwidthLimited;
|
||||
if (_bandwidthManager) {
|
||||
_bandwidthManager->registerDownloadJob(this);
|
||||
}
|
||||
AbstractNetworkJob::start();
|
||||
}
|
||||
|
||||
void GETFileJob::finished()
|
||||
{
|
||||
if (_bandwidthManager) {
|
||||
_bandwidthManager->unregisterDownloadJob(this);
|
||||
}
|
||||
if (reply()->bytesAvailable() && _httpOk) {
|
||||
// we were throttled, write out the remaining data
|
||||
slotReadyRead();
|
||||
Q_ASSERT(!reply()->bytesAvailable());
|
||||
}
|
||||
// ensure the device is closed in case the underlying file is modified in a signal connected to finished()
|
||||
_device->close();
|
||||
}
|
||||
|
||||
void GETFileJob::newReplyHook(QNetworkReply *reply)
|
||||
{
|
||||
if (_bandwidthManager) {
|
||||
reply->setReadBufferSize(16 * 1024); // keep low so we can easier limit the bandwidth
|
||||
}
|
||||
connect(reply, &QNetworkReply::metaDataChanged, this, &GETFileJob::slotMetaDataChanged);
|
||||
connect(reply, &QNetworkReply::finished, this, &GETFileJob::slotReadyRead);
|
||||
connect(reply, &QNetworkReply::downloadProgress, this, &GETFileJob::downloadProgress);
|
||||
}
|
||||
|
||||
void GETFileJob::slotMetaDataChanged()
|
||||
{
|
||||
// For some reason setting the read buffer in GETFileJob::start doesn't seem to go
|
||||
// through the HTTP layer thread(?)
|
||||
if (_bandwidthManager) {
|
||||
reply()->setReadBufferSize(16 * 1024);
|
||||
}
|
||||
|
||||
int httpStatus = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (httpStatus == 301 || httpStatus == 302 || httpStatus == 303 || httpStatus == 307 || httpStatus == 308 || httpStatus == 401) {
|
||||
// Redirects and auth failures (OAuth token renew) are handled by AbstractNetworkJob and
|
||||
// will end up restarting the job. We do not want to process further data from the initial
|
||||
// request. newReplyHook() will reestablish signal connections for the follow-up request.
|
||||
disconnect(reply(), &QNetworkReply::finished, this, &GETFileJob::slotReadyRead);
|
||||
disconnect(reply(), &QNetworkReply::readyRead, this, &GETFileJob::slotReadyRead);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the status code isn't 2xx, don't write the reply body to the file.
|
||||
// For any error: handle it when the job is finished, not here.
|
||||
if (httpStatus / 100 != 2) {
|
||||
// Disable the buffer limit, as we don't limit the bandwidth for error messages.
|
||||
// (We are only going to do a readAll() at the end.)
|
||||
if (_bandwidthManager) {
|
||||
reply()->setReadBufferSize(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (reply()->error() != QNetworkReply::NoError) {
|
||||
return;
|
||||
}
|
||||
_etag = getEtagFromReply(reply());
|
||||
|
||||
if (_etag.isEmpty()) {
|
||||
qCWarning(lcGetJob) << u"No E-Tag reply by server, considering it invalid";
|
||||
_errorString = tr("No E-Tag received from server, check Proxy/Gateway");
|
||||
_errorStatus = SyncFileItem::NormalError;
|
||||
abort();
|
||||
return;
|
||||
} else if (!_expectedEtagForResume.isEmpty() && _expectedEtagForResume != _etag) {
|
||||
qCWarning(lcGetJob) << u"We received a different E-Tag for resuming!" << _expectedEtagForResume << u"vs" << _etag;
|
||||
_errorString = tr("We received a different E-Tag for resuming. Retrying next time.");
|
||||
_errorStatus = SyncFileItem::NormalError;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
bool ok;
|
||||
_contentLength = reply()->header(QNetworkRequest::ContentLengthHeader).toLongLong(&ok);
|
||||
if (ok && _expectedContentLength != -1 && _contentLength != _expectedContentLength) {
|
||||
qCWarning(lcGetJob) << u"We received a different content length than expected!" << _expectedContentLength << u"vs" << _contentLength;
|
||||
_errorString = tr("We received an unexpected download Content-Length.");
|
||||
_errorStatus = SyncFileItem::NormalError;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 start = 0;
|
||||
const QString ranges = QString::fromUtf8(reply()->rawHeader("Content-Range"));
|
||||
if (!ranges.isEmpty()) {
|
||||
static QRegularExpression rx(QStringLiteral("bytes (\\d+)-"));
|
||||
const auto match = rx.match(ranges);
|
||||
if (match.hasMatch()) {
|
||||
start = match.captured(1).toLongLong();
|
||||
}
|
||||
}
|
||||
if (start != _resumeStart) {
|
||||
qCWarning(lcGetJob) << u"Wrong content-range: " << ranges << u" while expecting start was" << _resumeStart;
|
||||
if (ranges.isEmpty()) {
|
||||
// device doesn't support range, just try again from scratch
|
||||
_device->close();
|
||||
if (!_device->open(QIODevice::WriteOnly)) {
|
||||
_errorString = _device->errorString();
|
||||
_errorStatus = SyncFileItem::NormalError;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
_resumeStart = 0;
|
||||
} else {
|
||||
_errorString = tr("Server returned wrong content-range");
|
||||
_errorStatus = SyncFileItem::NormalError;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto lastModified = reply()->header(QNetworkRequest::LastModifiedHeader);
|
||||
if (!lastModified.isNull()) {
|
||||
_lastModified = Utility::qDateTimeToTime_t(lastModified.toDateTime());
|
||||
}
|
||||
_httpOk = true;
|
||||
connect(reply(), &QIODevice::readyRead, this, &GETFileJob::slotReadyRead);
|
||||
}
|
||||
|
||||
void GETFileJob::setBandwidthManager(BandwidthManager *bwm)
|
||||
{
|
||||
_bandwidthManager = bwm;
|
||||
}
|
||||
|
||||
void GETFileJob::setChoked(bool c)
|
||||
{
|
||||
if (c != _bandwidthChoked) {
|
||||
_bandwidthChoked = c;
|
||||
QMetaObject::invokeMethod(this, &GETFileJob::slotReadyRead, Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void GETFileJob::setBandwidthLimited(bool b)
|
||||
{
|
||||
if (_bandwidthLimited != b) {
|
||||
_bandwidthLimited = b;
|
||||
QMetaObject::invokeMethod(this, &GETFileJob::slotReadyRead, Qt::QueuedConnection);
|
||||
}
|
||||
}
|
||||
|
||||
void GETFileJob::giveBandwidthQuota(qint64 q)
|
||||
{
|
||||
_bandwidthQuota = q;
|
||||
qCDebug(lcGetJob) << u"Got" << q << u"bytes";
|
||||
QMetaObject::invokeMethod(this, &GETFileJob::slotReadyRead, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
qint64 GETFileJob::currentDownloadPosition()
|
||||
{
|
||||
if (_device && _device->pos() > 0 && _device->pos() > qint64(_resumeStart)) {
|
||||
return _device->pos();
|
||||
}
|
||||
return _resumeStart;
|
||||
}
|
||||
|
||||
void GETFileJob::slotReadyRead()
|
||||
{
|
||||
Q_ASSERT(reply());
|
||||
if (!_httpOk) {
|
||||
return;
|
||||
}
|
||||
|
||||
const qint64 bufferSize = std::min<qint64>(1024 * 8, reply()->bytesAvailable());
|
||||
QByteArray buffer(bufferSize, Qt::Uninitialized);
|
||||
|
||||
while (reply()->bytesAvailable() > 0) {
|
||||
if (_bandwidthChoked) {
|
||||
qCWarning(lcGetJob) << u"Download choked";
|
||||
break;
|
||||
}
|
||||
qint64 toRead = bufferSize;
|
||||
if (_bandwidthLimited) {
|
||||
toRead = std::min<qint64>(bufferSize, _bandwidthQuota);
|
||||
if (toRead == 0) {
|
||||
qCWarning(lcGetJob) << u"Out of bandwidth quota";
|
||||
break;
|
||||
}
|
||||
_bandwidthQuota -= toRead;
|
||||
}
|
||||
|
||||
const qint64 read = reply()->read(buffer.data(), toRead);
|
||||
if (read < 0) {
|
||||
_errorString = networkReplyErrorString(*reply());
|
||||
_errorStatus = SyncFileItem::NormalError;
|
||||
qCWarning(lcGetJob) << u"Error while reading from device: " << _errorString;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
|
||||
const qint64 written = _device->write(buffer.constData(), read);
|
||||
if (written != read) {
|
||||
_errorString = _device->errorString();
|
||||
_errorStatus = SyncFileItem::NormalError;
|
||||
qCWarning(lcGetJob) << u"Error while writing to file" << written << read << _errorString;
|
||||
abort();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
GETFileJob::~GETFileJob()
|
||||
{
|
||||
if (_bandwidthManager) {
|
||||
_bandwidthManager->unregisterDownloadJob(this);
|
||||
}
|
||||
}
|
||||
|
||||
QString GETFileJob::errorString() const
|
||||
{
|
||||
if (!_errorString.isEmpty()) {
|
||||
return _errorString;
|
||||
}
|
||||
return AbstractNetworkJob::errorString();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/abstractnetworkjob.h"
|
||||
#include "libsync/accountfwd.h"
|
||||
#include "libsync/bandwidthmanager.h"
|
||||
#include "libsync/qsferasynclib.h"
|
||||
#include "libsync/syncfileitem.h"
|
||||
|
||||
|
||||
namespace OCC {
|
||||
/**
|
||||
* @brief Downloads the remote file via GET
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT GETFileJob : public AbstractNetworkJob
|
||||
{
|
||||
Q_OBJECT
|
||||
QIODevice *_device;
|
||||
QMap<QByteArray, QByteArray> _headers;
|
||||
QString _expectedEtagForResume;
|
||||
qint64 _expectedContentLength;
|
||||
qint64 _contentLength;
|
||||
qint64 _resumeStart;
|
||||
|
||||
public:
|
||||
// DOES NOT take ownership of the device.
|
||||
// For directDownloadUrl:
|
||||
explicit GETFileJob(AccountPtr account, const QUrl &url, const QString &path, QIODevice *device, const QMap<QByteArray, QByteArray> &headers,
|
||||
const QString &expectedEtagForResume, qint64 resumeStart, QObject *parent = nullptr);
|
||||
virtual ~GETFileJob();
|
||||
|
||||
qint64 currentDownloadPosition();
|
||||
|
||||
void start() override;
|
||||
void finished() override;
|
||||
|
||||
void newReplyHook(QNetworkReply *reply) override;
|
||||
|
||||
qint64 resumeStart() { return _resumeStart; }
|
||||
|
||||
qint64 contentLength() const { return _contentLength; }
|
||||
qint64 expectedContentLength() const { return _expectedContentLength; }
|
||||
void setExpectedContentLength(qint64 size) { _expectedContentLength = size; }
|
||||
|
||||
void setChoked(bool c);
|
||||
void setBandwidthLimited(bool b);
|
||||
void giveBandwidthQuota(qint64 q);
|
||||
void setBandwidthManager(BandwidthManager *bwm);
|
||||
|
||||
QString &etag() { return _etag; }
|
||||
time_t lastModified() { return _lastModified; }
|
||||
|
||||
void setErrorString(const QString &s) { _errorString = s; }
|
||||
QString errorString() const;
|
||||
SyncFileItem::Status errorStatus() { return _errorStatus; }
|
||||
void setErrorStatus(const SyncFileItem::Status &s) { _errorStatus = s; }
|
||||
|
||||
private Q_SLOTS:
|
||||
void slotReadyRead();
|
||||
void slotMetaDataChanged();
|
||||
|
||||
Q_SIGNALS:
|
||||
void downloadProgress(qint64, qint64);
|
||||
|
||||
protected:
|
||||
bool restartDevice();
|
||||
|
||||
QString _etag;
|
||||
time_t _lastModified = 0;
|
||||
QString _errorString;
|
||||
SyncFileItem::Status _errorStatus = SyncFileItem::NoStatus;
|
||||
bool _bandwidthLimited = false; // if _bandwidthQuota will be used
|
||||
bool _bandwidthChoked = false; // if download is paused (won't read on readyRead())
|
||||
qint64 _bandwidthQuota = 0;
|
||||
bool _httpOk = false;
|
||||
QPointer<BandwidthManager> _bandwidthManager = nullptr;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#include "jsonjob.h"
|
||||
|
||||
#include "account.h"
|
||||
#include "common/utility.h"
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QLoggingCategory>
|
||||
#include <QNetworkReply>
|
||||
|
||||
Q_LOGGING_CATEGORY(lcJsonApiJob, "sync.networkjob.jsonapi", QtInfoMsg)
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
JsonJob::~JsonJob()
|
||||
{
|
||||
}
|
||||
|
||||
void JsonJob::finished()
|
||||
{
|
||||
if (reply()->error() != QNetworkReply::NoError) {
|
||||
qCWarning(lcJsonApiJob) << u"Network error: " << this << errorString();
|
||||
} else {
|
||||
parse(reply()->readAll());
|
||||
}
|
||||
SimpleNetworkJob::finished();
|
||||
}
|
||||
|
||||
void JsonJob::parse(const QByteArray &data)
|
||||
{
|
||||
const auto doc = QJsonDocument::fromJson(data, &_parseError);
|
||||
// empty or invalid response
|
||||
if (_parseError.error != QJsonParseError::NoError || doc.isNull()) {
|
||||
qCWarning(lcJsonApiJob) << u"invalid JSON!" << data << _parseError.errorString();
|
||||
} else {
|
||||
_data = doc.object();
|
||||
}
|
||||
}
|
||||
|
||||
const QJsonParseError &JsonJob::parseError() const
|
||||
{
|
||||
return _parseError;
|
||||
}
|
||||
|
||||
const QJsonObject &JsonJob::data() const
|
||||
{
|
||||
return _data;
|
||||
}
|
||||
|
||||
|
||||
JsonApiJob::JsonApiJob(AccountPtr account, const QString &path, const QByteArray &verb, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent)
|
||||
: JsonJob(account, account->url(), path, verb, arguments, req, parent)
|
||||
{
|
||||
_request.setRawHeader(QByteArrayLiteral("OCS-APIREQUEST"), QByteArrayLiteral("true"));
|
||||
|
||||
auto q = query();
|
||||
// HACK: q will be empty for POST, PUT, PATCH request as we utilise the body (see SimpleNetworkJob)
|
||||
// however the oc api requires the format header to be part of the url
|
||||
q.addQueryItem(QStringLiteral("format"), QStringLiteral("json"));
|
||||
setQuery(q);
|
||||
}
|
||||
|
||||
JsonApiJob::JsonApiJob(AccountPtr account, const QString &path, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent)
|
||||
: JsonApiJob(account, path, QByteArrayLiteral("GET"), arguments, req, parent)
|
||||
{
|
||||
}
|
||||
|
||||
int JsonApiJob::ocsStatus() const
|
||||
{
|
||||
return _ocsStatus;
|
||||
}
|
||||
|
||||
void JsonApiJob::parse(const QByteArray &rawData)
|
||||
{
|
||||
static const QRegularExpression rex(QStringLiteral("<statuscode>(\\d+)</statuscode>"));
|
||||
const auto match = rex.match(QString::fromUtf8(rawData));
|
||||
if (match.hasMatch()) {
|
||||
// this is a error message coming back from ocs.
|
||||
_ocsStatus = match.captured(1).toInt();
|
||||
} else {
|
||||
JsonJob::parse(rawData);
|
||||
// example: "{"ocs":{"meta":{"status":"ok","statuscode":100,"message":null},"data":{"version":{"major":8,"minor":"... (504)
|
||||
if (parseError().error == QJsonParseError::NoError) {
|
||||
if (data().contains(QLatin1String("ocs"))) {
|
||||
const auto meta = data().value(QLatin1String("ocs")).toObject().value(QLatin1String("meta")).toObject();
|
||||
_ocsStatus = meta.value(QLatin1String("statuscode")).toInt();
|
||||
_ocsMessage = meta.value(QLatin1String("message")).toString();
|
||||
} else {
|
||||
_ocsMessage = parseError().errorString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const QString &JsonApiJob::ocsMessage() const
|
||||
{
|
||||
return _ocsMessage;
|
||||
}
|
||||
|
||||
bool JsonApiJob::ocsSuccess() const
|
||||
{
|
||||
// v1 api: 100
|
||||
// v2 api: 200 as in http
|
||||
return ocsStatus() == 100 || ocsStatus() == 200;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (C) by Hannah von Reth <hannah.vonreth@owncloud.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
||||
* for more details.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "libsync/networkjobs/simplenetworkjob.h"
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
||||
class QSFERA_SYNC_EXPORT JsonJob : public SimpleNetworkJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using SimpleNetworkJob::SimpleNetworkJob;
|
||||
virtual ~JsonJob();
|
||||
|
||||
const QJsonObject &data() const;
|
||||
const QJsonParseError &parseError() const;
|
||||
|
||||
protected:
|
||||
void finished() override;
|
||||
|
||||
virtual void parse(const QByteArray &data);
|
||||
|
||||
|
||||
private:
|
||||
QJsonParseError _parseError;
|
||||
QJsonObject _data;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief Job to check an API that return JSON
|
||||
*
|
||||
* Note! you need to be in the connected state before calling this because of a server bug:
|
||||
* https://github.com/owncloud/core/issues/12930
|
||||
*
|
||||
* To be used like this:
|
||||
* \code
|
||||
* _job = new JsonApiJob(account, QLatin1String("ocs/v1.php/foo/bar"), this);
|
||||
* connect(job, SIGNAL(jsonReceived(QJsonDocument)), ...)
|
||||
* The received QVariantMap is null in case of error
|
||||
* \encode
|
||||
*
|
||||
* @ingroup libsync
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT JsonApiJob : public JsonJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit JsonApiJob(AccountPtr account, const QString &path, const QByteArray &verb, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent);
|
||||
explicit JsonApiJob(AccountPtr account, const QString &path, const UrlQuery &arguments, const QNetworkRequest &req, QObject *parent);
|
||||
|
||||
// the OCS status code: 100 (!) for success
|
||||
int ocsStatus() const;
|
||||
|
||||
const QString &ocsMessage() const;
|
||||
|
||||
// whether the job was successful
|
||||
bool ocsSuccess() const;
|
||||
|
||||
private:
|
||||
int _ocsStatus = 0;
|
||||
QString _ocsMessage;
|
||||
|
||||
protected:
|
||||
using JsonJob::JsonJob;
|
||||
virtual void parse(const QByteArray &data) override;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "libsync/networkjobs/resources.h"
|
||||
|
||||
#include "libsync/account.h"
|
||||
|
||||
#include <QFile>
|
||||
#include <QIcon>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
Q_LOGGING_CATEGORY(lcResources, "sync.networkjob.resource")
|
||||
|
||||
namespace {
|
||||
QString hashMd5(const QString &data)
|
||||
{
|
||||
QCryptographicHash hash(QCryptographicHash::Algorithm::Md5);
|
||||
hash.addData(data.toLatin1());
|
||||
return QString::fromLatin1(hash.result().toHex());
|
||||
}
|
||||
|
||||
QString cacheKey(QNetworkReply *reply)
|
||||
{
|
||||
const QString urlHash = hashMd5(reply->url().toString());
|
||||
const QString eTagHash = hashMd5(reply->header(QNetworkRequest::ETagHeader).toString());
|
||||
|
||||
const auto db = QMimeDatabase();
|
||||
|
||||
QString extension = db.mimeTypeForName(reply->header(QNetworkRequest::ContentTypeHeader).toString()).preferredSuffix();
|
||||
if (extension.isEmpty()) {
|
||||
extension = db.mimeTypeForData(reply).preferredSuffix();
|
||||
}
|
||||
if (extension.isEmpty()) {
|
||||
extension = QStringLiteral("unknown");
|
||||
}
|
||||
|
||||
return QStringLiteral("%1-%2.%3").arg(urlHash, eTagHash, extension);
|
||||
}
|
||||
}
|
||||
|
||||
void ResourceJob::finished()
|
||||
{
|
||||
if (reply()->error() != QNetworkReply::NoError) {
|
||||
qCWarning(lcResources) << u"Network error: " << this << errorString();
|
||||
} else {
|
||||
_cacheKey = cacheKey(reply());
|
||||
}
|
||||
|
||||
// storing the file on disk enables Qt to apply some optimizations, e.g., in QIcon
|
||||
// also, specifically for icons, loading from disk allows easy management of scalable and non-scalable entities
|
||||
if (httpStatusCode() == 200 && reply()->size() > 0) {
|
||||
const QString path = _cache->path(_cacheKey);
|
||||
|
||||
qCDebug(lcResources) << u"cache file path:" << path;
|
||||
|
||||
// furthermore, we can skip writing the file if the cache key has not changed (i.e., a file exists) and the file has come from the network cache
|
||||
if (QFileInfo::exists(path) && reply()->attribute(QNetworkRequest::SourceIsFromCacheAttribute).toBool()) {
|
||||
qCDebug(lcResources) << u"file has come from network cache, skipping writing";
|
||||
} else {
|
||||
QFile cacheFile(path);
|
||||
|
||||
// note: we want to truncate the file if it exists
|
||||
if (!cacheFile.open(QIODevice::WriteOnly)) {
|
||||
qCCritical(lcResources) << u"failed to open cache file for writing:" << cacheFile.fileName();
|
||||
} else {
|
||||
if (!cacheFile.write(reply()->readAll())) {
|
||||
qCCritical(lcResources) << u"failed to write to cache file:" << cacheFile.fileName();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SimpleNetworkJob::finished();
|
||||
}
|
||||
|
||||
QIcon ResourceJob::asIcon() const
|
||||
{
|
||||
// storing the file on disk enables Qt to apply some optimizations (e.g., caching of rendered pixmaps)
|
||||
Q_ASSERT(!_cacheKey.isEmpty());
|
||||
return QIcon(_cache->path(_cacheKey));
|
||||
}
|
||||
|
||||
ResourceJob::ResourceJob(const ResourcesCache *cache, const QUrl &rootUrl, const QString &path, QObject *parent)
|
||||
: SimpleNetworkJob(cache->account()->sharedFromThis(), rootUrl, path, "GET", {}, parent)
|
||||
, _cache(cache)
|
||||
{
|
||||
setStoreInCache(true);
|
||||
}
|
||||
|
||||
ResourcesCache::ResourcesCache(const QString &cacheDirectory, Account *account)
|
||||
: QObject(account)
|
||||
, _account(account)
|
||||
, _cacheDirectory(QStringLiteral("%1/tmp.XXXXXX").arg(cacheDirectory))
|
||||
{
|
||||
Q_ASSERT(_cacheDirectory.isValid());
|
||||
}
|
||||
|
||||
ResourceJob *ResourcesCache::makeGetJob(const QUrl &rootUrl, const QString &path, QObject *parent) const
|
||||
{
|
||||
return new ResourceJob(this, rootUrl, path, parent);
|
||||
}
|
||||
|
||||
ResourceJob *ResourcesCache::makeGetJob(const QString &path, QObject *parent) const
|
||||
{
|
||||
return makeGetJob(_account->url(), path, parent);
|
||||
}
|
||||
|
||||
Account *ResourcesCache::account() const
|
||||
{
|
||||
return _account;
|
||||
}
|
||||
|
||||
QString ResourcesCache::path(const QString &cacheKey) const
|
||||
{
|
||||
Q_ASSERT(!cacheKey.isEmpty());
|
||||
Q_ASSERT(_cacheDirectory.isValid());
|
||||
return _cacheDirectory.filePath(cacheKey);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
|
||||
#include "libsync/networkjobs/simplenetworkjob.h"
|
||||
|
||||
#include <QIcon>
|
||||
#include <QLoggingCategory>
|
||||
#include <QTemporaryDir>
|
||||
|
||||
namespace OCC {
|
||||
|
||||
class ResourcesCache;
|
||||
|
||||
/**
|
||||
* This job automatically downloads all available data from the server and stores it in a temporary cache directory on the disk.
|
||||
* For convenience, a couple of conversion functions are available to convert the binary data to common Qt classes such as QIcon.
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT ResourceJob : public SimpleNetworkJob
|
||||
{
|
||||
public:
|
||||
void finished() override;
|
||||
|
||||
QIcon asIcon() const;
|
||||
|
||||
protected:
|
||||
explicit ResourceJob(const ResourcesCache *cache, const QUrl &rootUrl, const QString &path, QObject *parent);
|
||||
|
||||
private:
|
||||
const ResourcesCache *_cache;
|
||||
QString _cacheKey;
|
||||
|
||||
friend class ResourcesCache;
|
||||
};
|
||||
|
||||
|
||||
class QSFERA_SYNC_EXPORT ResourcesCache : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
*
|
||||
* @param cacheDirectory path to temporary cache parent directory (note: this directory must exist)
|
||||
* @param account
|
||||
*/
|
||||
explicit ResourcesCache(const QString &cacheDirectory, Account *account);
|
||||
|
||||
ResourceJob *makeGetJob(const QUrl &rootUrl, const QString &path, QObject *parent) const;
|
||||
|
||||
ResourceJob *makeGetJob(const QString &path, QObject *parent) const;
|
||||
|
||||
Account *account() const;
|
||||
|
||||
QString path(const QString &cacheKey) const;
|
||||
|
||||
private:
|
||||
Account *_account;
|
||||
QTemporaryDir _cacheDirectory;
|
||||
};
|
||||
|
||||
} // namespace OCC
|
||||
@@ -0,0 +1,98 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#include "simplenetworkjob.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QJsonDocument>
|
||||
|
||||
using namespace OCC;
|
||||
|
||||
SimpleNetworkJob::SimpleNetworkJob(
|
||||
AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QNetworkRequest &req, QObject *parent)
|
||||
: AbstractNetworkJob(account, rootUrl, path, parent)
|
||||
, _request(req)
|
||||
, _verb(verb)
|
||||
{
|
||||
}
|
||||
|
||||
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QObject *parent)
|
||||
: SimpleNetworkJob(account, rootUrl, path, verb, {}, parent)
|
||||
{
|
||||
}
|
||||
|
||||
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const UrlQuery &arguments,
|
||||
const QNetworkRequest &req, QObject *parent)
|
||||
: SimpleNetworkJob(account, rootUrl, path, verb, req, parent)
|
||||
{
|
||||
Q_ASSERT((QList<QByteArray>{"GET", "PUT", "POST", "DELETE", "HEAD", "PATCH"}.contains(verb)));
|
||||
if (!arguments.isEmpty()) {
|
||||
QUrlQuery args;
|
||||
// ensure everything is percent encoded
|
||||
// this is especially important for parameters that contain spaces or +
|
||||
for (const auto &item : arguments) {
|
||||
args.addQueryItem(QString::fromUtf8(QUrl::toPercentEncoding(item.first)), QString::fromUtf8(QUrl::toPercentEncoding(item.second)));
|
||||
}
|
||||
if (verb == QByteArrayLiteral("POST") || verb == QByteArrayLiteral("PUT") || verb == QByteArrayLiteral("PATCH")) {
|
||||
_request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded; charset=UTF-8"));
|
||||
_body = args.query(QUrl::FullyEncoded).toUtf8();
|
||||
_device = std::make_unique<QBuffer>(&_body);
|
||||
} else {
|
||||
setQuery(args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QJsonObject &arguments,
|
||||
const QNetworkRequest &req, QObject *parent)
|
||||
: SimpleNetworkJob(account, rootUrl, path, verb, QJsonDocument(arguments).toJson(), req, parent)
|
||||
{
|
||||
_request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
|
||||
}
|
||||
|
||||
SimpleNetworkJob::SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb,
|
||||
std::unique_ptr<QIODevice> &&requestBody, const QNetworkRequest &req, QObject *parent)
|
||||
: SimpleNetworkJob(account, rootUrl, path, verb, req, parent)
|
||||
{
|
||||
_device = std::move(requestBody);
|
||||
}
|
||||
|
||||
// delay setting _device until we adopted the body
|
||||
SimpleNetworkJob::SimpleNetworkJob(
|
||||
AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QByteArray &&requestBody, const QNetworkRequest &req, QObject *parent)
|
||||
: SimpleNetworkJob(account, rootUrl, path, verb, std::unique_ptr<QIODevice>{}, req, parent)
|
||||
{
|
||||
_body = std::move(requestBody);
|
||||
_device = std::make_unique<QBuffer>(&_body);
|
||||
}
|
||||
|
||||
SimpleNetworkJob::~SimpleNetworkJob() { }
|
||||
void SimpleNetworkJob::start()
|
||||
{
|
||||
Q_ASSERT(!_verb.isEmpty());
|
||||
// AbstractNetworkJob will take ownership of the buffer
|
||||
sendRequest(_verb, _request, std::move(_device));
|
||||
AbstractNetworkJob::start();
|
||||
}
|
||||
|
||||
void SimpleNetworkJob::addNewReplyHook(std::function<void(QNetworkReply *)> &&hook)
|
||||
{
|
||||
_replyHooks.push_back(hook);
|
||||
}
|
||||
|
||||
void SimpleNetworkJob::finished()
|
||||
{
|
||||
if (_device) {
|
||||
_device->close();
|
||||
}
|
||||
if (body()) {
|
||||
body()->close();
|
||||
}
|
||||
}
|
||||
|
||||
void SimpleNetworkJob::newReplyHook(QNetworkReply *reply)
|
||||
{
|
||||
for (const auto &hook : _replyHooks) {
|
||||
hook(reply);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
// SPDX-FileCopyrightText: 2025 Hannah von Reth <h.vonreth@opencloud.eu>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "libsync/abstractcorejob.h"
|
||||
|
||||
namespace OCC {
|
||||
|
||||
|
||||
/**
|
||||
* @brief A basic job around a network request without extra funtionality
|
||||
* @ingroup libsync
|
||||
*
|
||||
* Primarily adds timeout and redirection handling.
|
||||
*/
|
||||
class QSFERA_SYNC_EXPORT SimpleNetworkJob : public AbstractNetworkJob
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
using UrlQuery = QList<QPair<QString, QString>>;
|
||||
|
||||
// fully qualified urls can be passed in the QNetworkRequest
|
||||
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QObject *parent);
|
||||
explicit SimpleNetworkJob(
|
||||
AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QNetworkRequest &req = {}, QObject *parent = nullptr);
|
||||
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb,
|
||||
std::unique_ptr<QIODevice> &&requestBody = {}, const QNetworkRequest &req = {}, QObject *parent = nullptr);
|
||||
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const UrlQuery &arguments,
|
||||
const QNetworkRequest &req = {}, QObject *parent = nullptr);
|
||||
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, const QJsonObject &arguments,
|
||||
const QNetworkRequest &req = {}, QObject *parent = nullptr);
|
||||
explicit SimpleNetworkJob(AccountPtr account, const QUrl &rootUrl, const QString &path, const QByteArray &verb, QByteArray &&requestBody,
|
||||
const QNetworkRequest &req = {}, QObject *parent = nullptr);
|
||||
|
||||
virtual ~SimpleNetworkJob();
|
||||
|
||||
void start() override;
|
||||
|
||||
void addNewReplyHook(std::function<void(QNetworkReply *)> &&hook);
|
||||
|
||||
protected:
|
||||
void finished() override;
|
||||
void newReplyHook(QNetworkReply *) override;
|
||||
|
||||
QNetworkRequest _request;
|
||||
|
||||
private:
|
||||
QByteArray _verb;
|
||||
QByteArray _body;
|
||||
std::unique_ptr<QIODevice> _device;
|
||||
std::vector<std::function<void(QNetworkReply *)>> _replyHooks;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user