refactor: more net cleanup
This runs clang-tidy on some other files in launcher/net/. This also makes use of some JSON wrappers in HttpMetaCache, instead of using the Qt stuff directly. Lastly, this removes useless null checks (crashes don't occur because of this, but because of concurrent usage / free of the QByteArray pointer), and fix a fixme in Download.h
This commit is contained in:
parent
efa3fbff39
commit
040ee919e5
@ -6,6 +6,8 @@ namespace Net {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Sink object for downloads that uses an external QByteArray it doesn't own as a target.
|
* Sink object for downloads that uses an external QByteArray it doesn't own as a target.
|
||||||
|
* FIXME: It is possible that the QByteArray is freed while we're doing some operation on it,
|
||||||
|
* causing a segmentation fault.
|
||||||
*/
|
*/
|
||||||
class ByteArraySink : public Sink {
|
class ByteArraySink : public Sink {
|
||||||
public:
|
public:
|
||||||
@ -16,9 +18,6 @@ class ByteArraySink : public Sink {
|
|||||||
public:
|
public:
|
||||||
auto init(QNetworkRequest& request) -> Task::State override
|
auto init(QNetworkRequest& request) -> Task::State override
|
||||||
{
|
{
|
||||||
if(!m_output)
|
|
||||||
return Task::State::Failed;
|
|
||||||
|
|
||||||
m_output->clear();
|
m_output->clear();
|
||||||
if (initAllValidators(request))
|
if (initAllValidators(request))
|
||||||
return Task::State::Running;
|
return Task::State::Running;
|
||||||
@ -27,9 +26,6 @@ class ByteArraySink : public Sink {
|
|||||||
|
|
||||||
auto write(QByteArray& data) -> Task::State override
|
auto write(QByteArray& data) -> Task::State override
|
||||||
{
|
{
|
||||||
if(!m_output)
|
|
||||||
return Task::State::Failed;
|
|
||||||
|
|
||||||
m_output->append(data);
|
m_output->append(data);
|
||||||
if (writeAllValidators(data))
|
if (writeAllValidators(data))
|
||||||
return Task::State::Running;
|
return Task::State::Running;
|
||||||
@ -38,9 +34,6 @@ class ByteArraySink : public Sink {
|
|||||||
|
|
||||||
auto abort() -> Task::State override
|
auto abort() -> Task::State override
|
||||||
{
|
{
|
||||||
if(!m_output)
|
|
||||||
return Task::State::Failed;
|
|
||||||
|
|
||||||
m_output->clear();
|
m_output->clear();
|
||||||
failAllValidators();
|
failAllValidators();
|
||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
|
@ -1,55 +1,47 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Validator.h"
|
#include "Validator.h"
|
||||||
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
#include <memory>
|
|
||||||
#include <QFile>
|
#include <QFile>
|
||||||
|
|
||||||
namespace Net {
|
namespace Net {
|
||||||
class ChecksumValidator: public Validator
|
class ChecksumValidator : public Validator {
|
||||||
{
|
public:
|
||||||
public: /* con/des */
|
|
||||||
ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray())
|
ChecksumValidator(QCryptographicHash::Algorithm algorithm, QByteArray expected = QByteArray())
|
||||||
:m_checksum(algorithm), m_expected(expected)
|
: m_checksum(algorithm), m_expected(expected){};
|
||||||
{
|
virtual ~ChecksumValidator() = default;
|
||||||
};
|
|
||||||
virtual ~ChecksumValidator() {};
|
|
||||||
|
|
||||||
public: /* methods */
|
public:
|
||||||
bool init(QNetworkRequest &) override
|
auto init(QNetworkRequest&) -> bool override
|
||||||
{
|
{
|
||||||
m_checksum.reset();
|
m_checksum.reset();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool write(QByteArray & data) override
|
|
||||||
|
auto write(QByteArray& data) -> bool override
|
||||||
{
|
{
|
||||||
m_checksum.addData(data);
|
m_checksum.addData(data);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
bool abort() override
|
|
||||||
{
|
auto abort() -> bool override { return true; }
|
||||||
return true;
|
|
||||||
}
|
auto validate(QNetworkReply&) -> bool override
|
||||||
bool validate(QNetworkReply &) override
|
|
||||||
{
|
|
||||||
if(m_expected.size() && m_expected != hash())
|
|
||||||
{
|
{
|
||||||
|
if (m_expected.size() && m_expected != hash()) {
|
||||||
qWarning() << "Checksum mismatch, download is bad.";
|
qWarning() << "Checksum mismatch, download is bad.";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
QByteArray hash()
|
|
||||||
{
|
|
||||||
return m_checksum.result();
|
|
||||||
}
|
|
||||||
void setExpected(QByteArray expected)
|
|
||||||
{
|
|
||||||
m_expected = expected;
|
|
||||||
}
|
|
||||||
|
|
||||||
private: /* data */
|
auto hash() -> QByteArray { return m_checksum.result(); }
|
||||||
|
|
||||||
|
void setExpected(QByteArray expected) { m_expected = expected; }
|
||||||
|
|
||||||
|
private:
|
||||||
QCryptographicHash m_checksum;
|
QCryptographicHash m_checksum;
|
||||||
QByteArray m_expected;
|
QByteArray m_expected;
|
||||||
};
|
};
|
||||||
}
|
} // namespace Net
|
||||||
|
@ -33,30 +33,29 @@ Download::Download() : NetAction()
|
|||||||
m_state = State::Inactive;
|
m_state = State::Inactive;
|
||||||
}
|
}
|
||||||
|
|
||||||
Download::Ptr Download::makeCached(QUrl url, MetaEntryPtr entry, Options options)
|
auto Download::makeCached(QUrl url, MetaEntryPtr entry, Options options) -> Download::Ptr
|
||||||
{
|
{
|
||||||
Download* dl = new Download();
|
auto* dl = new Download();
|
||||||
dl->m_url = url;
|
dl->m_url = url;
|
||||||
dl->m_options = options;
|
dl->m_options = options;
|
||||||
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
|
auto md5Node = new ChecksumValidator(QCryptographicHash::Md5);
|
||||||
auto cachedNode = new MetaCacheSink(entry, md5Node);
|
auto cachedNode = new MetaCacheSink(entry, md5Node);
|
||||||
dl->m_sink.reset(cachedNode);
|
dl->m_sink.reset(cachedNode);
|
||||||
dl->m_target_path = entry->getFullPath();
|
|
||||||
return dl;
|
return dl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Download::Ptr Download::makeByteArray(QUrl url, QByteArray* output, Options options)
|
auto Download::makeByteArray(QUrl url, QByteArray* output, Options options) -> Download::Ptr
|
||||||
{
|
{
|
||||||
Download* dl = new Download();
|
auto* dl = new Download();
|
||||||
dl->m_url = url;
|
dl->m_url = url;
|
||||||
dl->m_options = options;
|
dl->m_options = options;
|
||||||
dl->m_sink.reset(new ByteArraySink(output));
|
dl->m_sink.reset(new ByteArraySink(output));
|
||||||
return dl;
|
return dl;
|
||||||
}
|
}
|
||||||
|
|
||||||
Download::Ptr Download::makeFile(QUrl url, QString path, Options options)
|
auto Download::makeFile(QUrl url, QString path, Options options) -> Download::Ptr
|
||||||
{
|
{
|
||||||
Download* dl = new Download();
|
auto* dl = new Download();
|
||||||
dl->m_url = url;
|
dl->m_url = url;
|
||||||
dl->m_options = options;
|
dl->m_options = options;
|
||||||
dl->m_sink.reset(new FileSink(path));
|
dl->m_sink.reset(new FileSink(path));
|
||||||
@ -143,7 +142,7 @@ void Download::sslErrors(const QList<QSslError>& errors)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Download::handleRedirect()
|
auto Download::handleRedirect() -> bool
|
||||||
{
|
{
|
||||||
QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
|
QUrl redirect = m_reply->header(QNetworkRequest::LocationHeader).toUrl();
|
||||||
if (!redirect.isValid()) {
|
if (!redirect.isValid()) {
|
||||||
@ -230,7 +229,7 @@ void Download::downloadFinished()
|
|||||||
// make sure we got all the remaining data, if any
|
// make sure we got all the remaining data, if any
|
||||||
auto data = m_reply->readAll();
|
auto data = m_reply->readAll();
|
||||||
if (data.size()) {
|
if (data.size()) {
|
||||||
qDebug() << "Writing extra" << data.size() << "bytes to" << m_target_path;
|
qDebug() << "Writing extra" << data.size() << "bytes";
|
||||||
m_state = m_sink->write(data);
|
m_state = m_sink->write(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -243,6 +242,7 @@ void Download::downloadFinished()
|
|||||||
emitFailed();
|
emitFailed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_reply.reset();
|
m_reply.reset();
|
||||||
qDebug() << "Download succeeded:" << m_url.toString();
|
qDebug() << "Download succeeded:" << m_url.toString();
|
||||||
emit succeeded();
|
emit succeeded();
|
||||||
@ -254,17 +254,17 @@ void Download::downloadReadyRead()
|
|||||||
auto data = m_reply->readAll();
|
auto data = m_reply->readAll();
|
||||||
m_state = m_sink->write(data);
|
m_state = m_sink->write(data);
|
||||||
if (m_state == State::Failed) {
|
if (m_state == State::Failed) {
|
||||||
qCritical() << "Failed to process response chunk for " << m_target_path;
|
qCritical() << "Failed to process response chunk";
|
||||||
}
|
}
|
||||||
// qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes";
|
// qDebug() << "Download" << m_url.toString() << "gained" << data.size() << "bytes";
|
||||||
} else {
|
} else {
|
||||||
qCritical() << "Cannot write to " << m_target_path << ", illegal status" << m_status;
|
qCritical() << "Cannot write download data! illegal status " << m_status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Net
|
} // namespace Net
|
||||||
|
|
||||||
bool Net::Download::abort()
|
auto Net::Download::abort() -> bool
|
||||||
{
|
{
|
||||||
if (m_reply) {
|
if (m_reply) {
|
||||||
m_reply->abort();
|
m_reply->abort();
|
||||||
|
@ -15,46 +15,39 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "NetAction.h"
|
|
||||||
#include "HttpMetaCache.h"
|
#include "HttpMetaCache.h"
|
||||||
#include "Validator.h"
|
#include "NetAction.h"
|
||||||
#include "Sink.h"
|
#include "Sink.h"
|
||||||
|
#include "Validator.h"
|
||||||
|
|
||||||
#include "QObjectPtr.h"
|
#include "QObjectPtr.h"
|
||||||
|
|
||||||
namespace Net {
|
namespace Net {
|
||||||
class Download : public NetAction
|
class Download : public NetAction {
|
||||||
{
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
typedef shared_qobject_ptr<class Download> Ptr;
|
using Ptr = shared_qobject_ptr<class Download>;
|
||||||
enum class Option
|
enum class Option { NoOptions = 0, AcceptLocalFiles = 1 };
|
||||||
{
|
|
||||||
NoOptions = 0,
|
|
||||||
AcceptLocalFiles = 1
|
|
||||||
};
|
|
||||||
Q_DECLARE_FLAGS(Options, Option)
|
Q_DECLARE_FLAGS(Options, Option)
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
explicit Download();
|
explicit Download();
|
||||||
public:
|
|
||||||
virtual ~Download(){};
|
|
||||||
static Download::Ptr makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions);
|
|
||||||
static Download::Ptr makeByteArray(QUrl url, QByteArray *output, Options options = Option::NoOptions);
|
|
||||||
static Download::Ptr makeFile(QUrl url, QString path, Options options = Option::NoOptions);
|
|
||||||
|
|
||||||
public:
|
public:
|
||||||
QString getTargetFilepath()
|
~Download() override = default;
|
||||||
{
|
|
||||||
return m_target_path;
|
static auto makeCached(QUrl url, MetaEntryPtr entry, Options options = Option::NoOptions) -> Download::Ptr;
|
||||||
}
|
static auto makeByteArray(QUrl url, QByteArray* output, Options options = Option::NoOptions) -> Download::Ptr;
|
||||||
|
static auto makeFile(QUrl url, QString path, Options options = Option::NoOptions) -> Download::Ptr;
|
||||||
|
|
||||||
|
public:
|
||||||
void addValidator(Validator* v);
|
void addValidator(Validator* v);
|
||||||
bool abort() override;
|
auto abort() -> bool override;
|
||||||
bool canAbort() const override { return true; };
|
auto canAbort() const -> bool override { return true; };
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool handleRedirect();
|
auto handleRedirect() -> bool;
|
||||||
|
|
||||||
protected slots:
|
protected slots:
|
||||||
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
|
void downloadProgress(qint64 bytesReceived, qint64 bytesTotal) override;
|
||||||
@ -67,11 +60,9 @@ public slots:
|
|||||||
void executeTask() override;
|
void executeTask() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// FIXME: remove this, it has no business being here.
|
|
||||||
QString m_target_path;
|
|
||||||
std::unique_ptr<Sink> m_sink;
|
std::unique_ptr<Sink> m_sink;
|
||||||
Options m_options;
|
Options m_options;
|
||||||
};
|
};
|
||||||
}
|
} // namespace Net
|
||||||
|
|
||||||
Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options)
|
Q_DECLARE_OPERATORS_FOR_FLAGS(Net::Download::Options)
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
#include "FileSink.h"
|
#include "FileSink.h"
|
||||||
|
|
||||||
#include <QFile>
|
|
||||||
|
|
||||||
#include "FileSystem.h"
|
#include "FileSystem.h"
|
||||||
|
|
||||||
namespace Net {
|
namespace Net {
|
||||||
@ -9,20 +7,19 @@ namespace Net {
|
|||||||
Task::State FileSink::init(QNetworkRequest& request)
|
Task::State FileSink::init(QNetworkRequest& request)
|
||||||
{
|
{
|
||||||
auto result = initCache(request);
|
auto result = initCache(request);
|
||||||
if(result != Task::State::Running)
|
if (result != Task::State::Running) {
|
||||||
{
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a new save file and open it for writing
|
// create a new save file and open it for writing
|
||||||
if (!FS::ensureFilePathExists(m_filename))
|
if (!FS::ensureFilePathExists(m_filename)) {
|
||||||
{
|
|
||||||
qCritical() << "Could not create folder for " + m_filename;
|
qCritical() << "Could not create folder for " + m_filename;
|
||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
wroteAnyData = false;
|
wroteAnyData = false;
|
||||||
m_output_file.reset(new QSaveFile(m_filename));
|
m_output_file.reset(new QSaveFile(m_filename));
|
||||||
if (!m_output_file->open(QIODevice::WriteOnly))
|
if (!m_output_file->open(QIODevice::WriteOnly)) {
|
||||||
{
|
|
||||||
qCritical() << "Could not open " + m_filename + " for writing";
|
qCritical() << "Could not open " + m_filename + " for writing";
|
||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
}
|
}
|
||||||
@ -32,21 +29,16 @@ Task::State FileSink::init(QNetworkRequest& request)
|
|||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
Task::State FileSink::initCache(QNetworkRequest &)
|
|
||||||
{
|
|
||||||
return Task::State::Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task::State FileSink::write(QByteArray& data)
|
Task::State FileSink::write(QByteArray& data)
|
||||||
{
|
{
|
||||||
if (!writeAllValidators(data) || m_output_file->write(data) != data.size())
|
if (!writeAllValidators(data) || m_output_file->write(data) != data.size()) {
|
||||||
{
|
|
||||||
qCritical() << "Failed writing into " + m_filename;
|
qCritical() << "Failed writing into " + m_filename;
|
||||||
m_output_file->cancelWriting();
|
m_output_file->cancelWriting();
|
||||||
m_output_file.reset();
|
m_output_file.reset();
|
||||||
wroteAnyData = false;
|
wroteAnyData = false;
|
||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
}
|
}
|
||||||
|
|
||||||
wroteAnyData = true;
|
wroteAnyData = true;
|
||||||
return Task::State::Running;
|
return Task::State::Running;
|
||||||
}
|
}
|
||||||
@ -64,33 +56,38 @@ Task::State FileSink::finalize(QNetworkReply& reply)
|
|||||||
QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
QVariant statusCodeV = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||||
bool validStatus = false;
|
bool validStatus = false;
|
||||||
int statusCode = statusCodeV.toInt(&validStatus);
|
int statusCode = statusCodeV.toInt(&validStatus);
|
||||||
if(validStatus)
|
if (validStatus) {
|
||||||
{
|
|
||||||
// this leaves out 304 Not Modified
|
// this leaves out 304 Not Modified
|
||||||
gotFile = statusCode == 200 || statusCode == 203;
|
gotFile = statusCode == 200 || statusCode == 203;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we wrote any data to the save file, we try to commit the data to the real file.
|
// if we wrote any data to the save file, we try to commit the data to the real file.
|
||||||
// if it actually got a proper file, we write it even if it was empty
|
// if it actually got a proper file, we write it even if it was empty
|
||||||
if (gotFile || wroteAnyData)
|
if (gotFile || wroteAnyData) {
|
||||||
{
|
|
||||||
// ask validators for data consistency
|
// ask validators for data consistency
|
||||||
// we only do this for actual downloads, not 'your data is still the same' cache hits
|
// we only do this for actual downloads, not 'your data is still the same' cache hits
|
||||||
if (!finalizeAllValidators(reply))
|
if (!finalizeAllValidators(reply))
|
||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
|
|
||||||
// nothing went wrong...
|
// nothing went wrong...
|
||||||
if (!m_output_file->commit())
|
if (!m_output_file->commit()) {
|
||||||
{
|
|
||||||
qCritical() << "Failed to commit changes to " << m_filename;
|
qCritical() << "Failed to commit changes to " << m_filename;
|
||||||
m_output_file->cancelWriting();
|
m_output_file->cancelWriting();
|
||||||
return Task::State::Failed;
|
return Task::State::Failed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// then get rid of the save file
|
// then get rid of the save file
|
||||||
m_output_file.reset();
|
m_output_file.reset();
|
||||||
|
|
||||||
return finalizeCache(reply);
|
return finalizeCache(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task::State FileSink::initCache(QNetworkRequest&)
|
||||||
|
{
|
||||||
|
return Task::State::Running;
|
||||||
|
}
|
||||||
|
|
||||||
Task::State FileSink::finalizeCache(QNetworkReply&)
|
Task::State FileSink::finalizeCache(QNetworkReply&)
|
||||||
{
|
{
|
||||||
return Task::State::Succeeded;
|
return Task::State::Succeeded;
|
||||||
@ -101,4 +98,4 @@ bool FileSink::hasLocalData()
|
|||||||
QFileInfo info(m_filename);
|
QFileInfo info(m_filename);
|
||||||
return info.exists() && info.size() != 0;
|
return info.exists() && info.size() != 0;
|
||||||
}
|
}
|
||||||
}
|
} // namespace Net
|
||||||
|
@ -15,29 +15,26 @@
|
|||||||
|
|
||||||
#include "HttpMetaCache.h"
|
#include "HttpMetaCache.h"
|
||||||
#include "FileSystem.h"
|
#include "FileSystem.h"
|
||||||
|
#include "Json.h"
|
||||||
|
|
||||||
#include <QFileInfo>
|
|
||||||
#include <QFile>
|
|
||||||
#include <QDateTime>
|
|
||||||
#include <QCryptographicHash>
|
#include <QCryptographicHash>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
#include <QJsonDocument>
|
auto MetaEntry::getFullPath() -> QString
|
||||||
#include <QJsonArray>
|
|
||||||
#include <QJsonObject>
|
|
||||||
|
|
||||||
QString MetaEntry::getFullPath()
|
|
||||||
{
|
{
|
||||||
// FIXME: make local?
|
// FIXME: make local?
|
||||||
return FS::PathCombine(basePath, relativePath);
|
return FS::PathCombine(basePath, relativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpMetaCache::HttpMetaCache(QString path) : QObject()
|
HttpMetaCache::HttpMetaCache(QString path) : QObject(), m_index_file(path)
|
||||||
{
|
{
|
||||||
m_index_file = path;
|
|
||||||
saveBatchingTimer.setSingleShot(true);
|
saveBatchingTimer.setSingleShot(true);
|
||||||
saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer);
|
saveBatchingTimer.setTimerType(Qt::VeryCoarseTimer);
|
||||||
|
|
||||||
connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow()));
|
connect(&saveBatchingTimer, SIGNAL(timeout()), SLOT(SaveNow()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,28 +44,27 @@ HttpMetaCache::~HttpMetaCache()
|
|||||||
SaveNow();
|
SaveNow();
|
||||||
}
|
}
|
||||||
|
|
||||||
MetaEntryPtr HttpMetaCache::getEntry(QString base, QString resource_path)
|
auto HttpMetaCache::getEntry(QString base, QString resource_path) -> MetaEntryPtr
|
||||||
{
|
{
|
||||||
// no base. no base path. can't store
|
// no base. no base path. can't store
|
||||||
if (!m_entries.contains(base))
|
if (!m_entries.contains(base)) {
|
||||||
{
|
|
||||||
// TODO: log problem
|
// TODO: log problem
|
||||||
return MetaEntryPtr();
|
return {};
|
||||||
}
|
|
||||||
EntryMap &map = m_entries[base];
|
|
||||||
if (map.entry_list.contains(resource_path))
|
|
||||||
{
|
|
||||||
return map.entry_list[resource_path];
|
|
||||||
}
|
|
||||||
return MetaEntryPtr();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag)
|
EntryMap& map = m_entries[base];
|
||||||
|
if (map.entry_list.contains(resource_path)) {
|
||||||
|
return map.entry_list[resource_path];
|
||||||
|
}
|
||||||
|
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto HttpMetaCache::resolveEntry(QString base, QString resource_path, QString expected_etag) -> MetaEntryPtr
|
||||||
{
|
{
|
||||||
auto entry = getEntry(base, resource_path);
|
auto entry = getEntry(base, resource_path);
|
||||||
// it's not present? generate a default stale entry
|
// it's not present? generate a default stale entry
|
||||||
if (!entry)
|
if (!entry) {
|
||||||
{
|
|
||||||
return staleEntry(base, resource_path);
|
return staleEntry(base, resource_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,15 +73,13 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS
|
|||||||
QFileInfo finfo(real_path);
|
QFileInfo finfo(real_path);
|
||||||
|
|
||||||
// is the file really there? if not -> stale
|
// is the file really there? if not -> stale
|
||||||
if (!finfo.isFile() || !finfo.isReadable())
|
if (!finfo.isFile() || !finfo.isReadable()) {
|
||||||
{
|
|
||||||
// if the file doesn't exist, we disown the entry
|
// if the file doesn't exist, we disown the entry
|
||||||
selected_base.entry_list.remove(resource_path);
|
selected_base.entry_list.remove(resource_path);
|
||||||
return staleEntry(base, resource_path);
|
return staleEntry(base, resource_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!expected_etag.isEmpty() && expected_etag != entry->etag)
|
if (!expected_etag.isEmpty() && expected_etag != entry->etag) {
|
||||||
{
|
|
||||||
// if the etag doesn't match expected, we disown the entry
|
// if the etag doesn't match expected, we disown the entry
|
||||||
selected_base.entry_list.remove(resource_path);
|
selected_base.entry_list.remove(resource_path);
|
||||||
return staleEntry(base, resource_path);
|
return staleEntry(base, resource_path);
|
||||||
@ -93,18 +87,15 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS
|
|||||||
|
|
||||||
// if the file changed, check md5sum
|
// if the file changed, check md5sum
|
||||||
qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch();
|
qint64 file_last_changed = finfo.lastModified().toUTC().toMSecsSinceEpoch();
|
||||||
if (file_last_changed != entry->local_changed_timestamp)
|
if (file_last_changed != entry->local_changed_timestamp) {
|
||||||
{
|
|
||||||
QFile input(real_path);
|
QFile input(real_path);
|
||||||
input.open(QIODevice::ReadOnly);
|
input.open(QIODevice::ReadOnly);
|
||||||
QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5)
|
QString md5sum = QCryptographicHash::hash(input.readAll(), QCryptographicHash::Md5).toHex().constData();
|
||||||
.toHex()
|
if (entry->md5sum != md5sum) {
|
||||||
.constData();
|
|
||||||
if (entry->md5sum != md5sum)
|
|
||||||
{
|
|
||||||
selected_base.entry_list.remove(resource_path);
|
selected_base.entry_list.remove(resource_path);
|
||||||
return staleEntry(base, resource_path);
|
return staleEntry(base, resource_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
// md5sums matched... keep entry and save the new state to file
|
// md5sums matched... keep entry and save the new state to file
|
||||||
entry->local_changed_timestamp = file_last_changed;
|
entry->local_changed_timestamp = file_last_changed;
|
||||||
SaveEventually();
|
SaveEventually();
|
||||||
@ -115,42 +106,42 @@ MetaEntryPtr HttpMetaCache::resolveEntry(QString base, QString resource_path, QS
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HttpMetaCache::updateEntry(MetaEntryPtr stale_entry)
|
auto HttpMetaCache::updateEntry(MetaEntryPtr stale_entry) -> bool
|
||||||
{
|
{
|
||||||
if (!m_entries.contains(stale_entry->baseId))
|
if (!m_entries.contains(stale_entry->baseId)) {
|
||||||
{
|
qCritical() << "Cannot add entry with unknown base: " << stale_entry->baseId.toLocal8Bit();
|
||||||
qCritical() << "Cannot add entry with unknown base: "
|
|
||||||
<< stale_entry->baseId.toLocal8Bit();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (stale_entry->stale)
|
|
||||||
{
|
if (stale_entry->stale) {
|
||||||
qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
|
qCritical() << "Cannot add stale entry: " << stale_entry->getFullPath().toLocal8Bit();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry;
|
m_entries[stale_entry->baseId].entry_list[stale_entry->relativePath] = stale_entry;
|
||||||
SaveEventually();
|
SaveEventually();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HttpMetaCache::evictEntry(MetaEntryPtr entry)
|
auto HttpMetaCache::evictEntry(MetaEntryPtr entry) -> bool
|
||||||
{
|
|
||||||
if(entry)
|
|
||||||
{
|
{
|
||||||
|
if (!entry)
|
||||||
|
return false;
|
||||||
|
|
||||||
entry->stale = true;
|
entry->stale = true;
|
||||||
SaveEventually();
|
SaveEventually();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
MetaEntryPtr HttpMetaCache::staleEntry(QString base, QString resource_path)
|
auto HttpMetaCache::staleEntry(QString base, QString resource_path) -> MetaEntryPtr
|
||||||
{
|
{
|
||||||
auto foo = new MetaEntry();
|
auto foo = new MetaEntry();
|
||||||
foo->baseId = base;
|
foo->baseId = base;
|
||||||
foo->basePath = getBasePath(base);
|
foo->basePath = getBasePath(base);
|
||||||
foo->relativePath = resource_path;
|
foo->relativePath = resource_path;
|
||||||
foo->stale = true;
|
foo->stale = true;
|
||||||
|
|
||||||
return MetaEntryPtr(foo);
|
return MetaEntryPtr(foo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,19 +150,20 @@ void HttpMetaCache::addBase(QString base, QString base_root)
|
|||||||
// TODO: report error
|
// TODO: report error
|
||||||
if (m_entries.contains(base))
|
if (m_entries.contains(base))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// TODO: check if the base path is valid
|
// TODO: check if the base path is valid
|
||||||
EntryMap foo;
|
EntryMap foo;
|
||||||
foo.base_path = base_root;
|
foo.base_path = base_root;
|
||||||
m_entries[base] = foo;
|
m_entries[base] = foo;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString HttpMetaCache::getBasePath(QString base)
|
auto HttpMetaCache::getBasePath(QString base) -> QString
|
||||||
{
|
|
||||||
if (m_entries.contains(base))
|
|
||||||
{
|
{
|
||||||
|
if (m_entries.contains(base)) {
|
||||||
return m_entries[base].base_path;
|
return m_entries[base].base_path;
|
||||||
}
|
}
|
||||||
return QString();
|
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
void HttpMetaCache::Load()
|
void HttpMetaCache::Load()
|
||||||
@ -184,41 +176,35 @@ void HttpMetaCache::Load()
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
QJsonDocument json = QJsonDocument::fromJson(index.readAll());
|
QJsonDocument json = QJsonDocument::fromJson(index.readAll());
|
||||||
if (!json.isObject())
|
|
||||||
return;
|
auto root = Json::requireObject(json, "HttpMetaCache root");
|
||||||
auto root = json.object();
|
|
||||||
// check file version first
|
// check file version first
|
||||||
auto version_val = root.value("version");
|
auto version_val = Json::ensureString(root, "version");
|
||||||
if (!version_val.isString())
|
if (version_val != "1")
|
||||||
return;
|
|
||||||
if (version_val.toString() != "1")
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// read the entry array
|
// read the entry array
|
||||||
auto entries_val = root.value("entries");
|
auto array = Json::ensureArray(root, "entries");
|
||||||
if (!entries_val.isArray())
|
for (auto element : array) {
|
||||||
return;
|
auto element_obj = Json::ensureObject(element);
|
||||||
QJsonArray array = entries_val.toArray();
|
auto base = Json::ensureString(element_obj, "base");
|
||||||
for (auto element : array)
|
|
||||||
{
|
|
||||||
if (!element.isObject())
|
|
||||||
return;
|
|
||||||
auto element_obj = element.toObject();
|
|
||||||
QString base = element_obj.value("base").toString();
|
|
||||||
if (!m_entries.contains(base))
|
if (!m_entries.contains(base))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
auto& entrymap = m_entries[base];
|
auto& entrymap = m_entries[base];
|
||||||
|
|
||||||
auto foo = new MetaEntry();
|
auto foo = new MetaEntry();
|
||||||
foo->baseId = base;
|
foo->baseId = base;
|
||||||
QString path = foo->relativePath = element_obj.value("path").toString();
|
foo->relativePath = Json::ensureString(element_obj, "path");
|
||||||
foo->md5sum = element_obj.value("md5sum").toString();
|
foo->md5sum = Json::ensureString(element_obj, "md5sum");
|
||||||
foo->etag = element_obj.value("etag").toString();
|
foo->etag = Json::ensureString(element_obj, "etag");
|
||||||
foo->local_changed_timestamp = element_obj.value("last_changed_timestamp").toDouble();
|
foo->local_changed_timestamp = Json::ensureDouble(element_obj, "last_changed_timestamp");
|
||||||
foo->remote_changed_timestamp =
|
foo->remote_changed_timestamp = Json::ensureString(element_obj, "remote_changed_timestamp");
|
||||||
element_obj.value("remote_changed_timestamp").toString();
|
|
||||||
// presumed innocent until closer examination
|
// presumed innocent until closer examination
|
||||||
foo->stale = false;
|
foo->stale = false;
|
||||||
entrymap.entry_list[path] = MetaEntryPtr(foo);
|
|
||||||
|
entrymap.entry_list[foo->relativePath] = MetaEntryPtr(foo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -233,40 +219,34 @@ void HttpMetaCache::SaveNow()
|
|||||||
{
|
{
|
||||||
if (m_index_file.isNull())
|
if (m_index_file.isNull())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
QJsonObject toplevel;
|
QJsonObject toplevel;
|
||||||
toplevel.insert("version", QJsonValue(QString("1")));
|
Json::writeString(toplevel, "version", "1");
|
||||||
|
|
||||||
QJsonArray entriesArr;
|
QJsonArray entriesArr;
|
||||||
for (auto group : m_entries)
|
for (auto group : m_entries) {
|
||||||
{
|
for (auto entry : group.entry_list) {
|
||||||
for (auto entry : group.entry_list)
|
|
||||||
{
|
|
||||||
// do not save stale entries. they are dead.
|
// do not save stale entries. they are dead.
|
||||||
if(entry->stale)
|
if (entry->stale) {
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
QJsonObject entryObj;
|
QJsonObject entryObj;
|
||||||
entryObj.insert("base", QJsonValue(entry->baseId));
|
Json::writeString(entryObj, "base", entry->baseId);
|
||||||
entryObj.insert("path", QJsonValue(entry->relativePath));
|
Json::writeString(entryObj, "path", entry->relativePath);
|
||||||
entryObj.insert("md5sum", QJsonValue(entry->md5sum));
|
Json::writeString(entryObj, "md5sum", entry->md5sum);
|
||||||
entryObj.insert("etag", QJsonValue(entry->etag));
|
Json::writeString(entryObj, "etag", entry->etag);
|
||||||
entryObj.insert("last_changed_timestamp",
|
entryObj.insert("last_changed_timestamp", QJsonValue(double(entry->local_changed_timestamp)));
|
||||||
QJsonValue(double(entry->local_changed_timestamp)));
|
|
||||||
if (!entry->remote_changed_timestamp.isEmpty())
|
if (!entry->remote_changed_timestamp.isEmpty())
|
||||||
entryObj.insert("remote_changed_timestamp",
|
entryObj.insert("remote_changed_timestamp", QJsonValue(entry->remote_changed_timestamp));
|
||||||
QJsonValue(entry->remote_changed_timestamp));
|
|
||||||
entriesArr.append(entryObj);
|
entriesArr.append(entryObj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toplevel.insert("entries", entriesArr);
|
toplevel.insert("entries", entriesArr);
|
||||||
|
|
||||||
QJsonDocument doc(toplevel);
|
try {
|
||||||
try
|
Json::write(toplevel, m_index_file);
|
||||||
{
|
} catch (const Exception& e) {
|
||||||
FS::write(m_index_file, doc.toJson());
|
|
||||||
}
|
|
||||||
catch (const Exception &e)
|
|
||||||
{
|
|
||||||
qWarning() << e.what();
|
qWarning() << e.what();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,56 +14,35 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <QString>
|
|
||||||
#include <QMap>
|
|
||||||
#include <qtimer.h>
|
#include <qtimer.h>
|
||||||
|
#include <QMap>
|
||||||
|
#include <QString>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
class HttpMetaCache;
|
class HttpMetaCache;
|
||||||
|
|
||||||
class MetaEntry
|
class MetaEntry {
|
||||||
{
|
|
||||||
friend class HttpMetaCache;
|
friend class HttpMetaCache;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
MetaEntry() {}
|
MetaEntry() = default;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
bool isStale()
|
auto isStale() -> bool { return stale; }
|
||||||
{
|
void setStale(bool stale) { this->stale = stale; }
|
||||||
return stale;
|
|
||||||
}
|
auto getFullPath() -> QString;
|
||||||
void setStale(bool stale)
|
|
||||||
{
|
auto getRemoteChangedTimestamp() -> QString { return remote_changed_timestamp; }
|
||||||
this->stale = stale;
|
void setRemoteChangedTimestamp(QString remote_changed_timestamp) { this->remote_changed_timestamp = remote_changed_timestamp; }
|
||||||
}
|
void setLocalChangedTimestamp(qint64 timestamp) { local_changed_timestamp = timestamp; }
|
||||||
QString getFullPath();
|
|
||||||
QString getRemoteChangedTimestamp()
|
auto getETag() -> QString { return etag; }
|
||||||
{
|
void setETag(QString etag) { this->etag = etag; }
|
||||||
return remote_changed_timestamp;
|
|
||||||
}
|
auto getMD5Sum() -> QString { return md5sum; }
|
||||||
void setRemoteChangedTimestamp(QString remote_changed_timestamp)
|
void setMD5Sum(QString md5sum) { this->md5sum = md5sum; }
|
||||||
{
|
|
||||||
this->remote_changed_timestamp = remote_changed_timestamp;
|
|
||||||
}
|
|
||||||
void setLocalChangedTimestamp(qint64 timestamp)
|
|
||||||
{
|
|
||||||
local_changed_timestamp = timestamp;
|
|
||||||
}
|
|
||||||
QString getETag()
|
|
||||||
{
|
|
||||||
return etag;
|
|
||||||
}
|
|
||||||
void setETag(QString etag)
|
|
||||||
{
|
|
||||||
this->etag = etag;
|
|
||||||
}
|
|
||||||
QString getMD5Sum()
|
|
||||||
{
|
|
||||||
return md5sum;
|
|
||||||
}
|
|
||||||
void setMD5Sum(QString md5sum)
|
|
||||||
{
|
|
||||||
this->md5sum = md5sum;
|
|
||||||
}
|
|
||||||
protected:
|
protected:
|
||||||
QString baseId;
|
QString baseId;
|
||||||
QString basePath;
|
QString basePath;
|
||||||
@ -75,48 +54,48 @@ protected:
|
|||||||
bool stale = true;
|
bool stale = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
typedef std::shared_ptr<MetaEntry> MetaEntryPtr;
|
using MetaEntryPtr = std::shared_ptr<MetaEntry>;
|
||||||
|
|
||||||
class HttpMetaCache : public QObject
|
class HttpMetaCache : public QObject {
|
||||||
{
|
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
// supply path to the cache index file
|
// supply path to the cache index file
|
||||||
HttpMetaCache(QString path = QString());
|
HttpMetaCache(QString path = QString());
|
||||||
~HttpMetaCache();
|
~HttpMetaCache() override;
|
||||||
|
|
||||||
// get the entry solely from the cache
|
// get the entry solely from the cache
|
||||||
// you probably don't want this, unless you have some specific caching needs.
|
// you probably don't want this, unless you have some specific caching needs.
|
||||||
MetaEntryPtr getEntry(QString base, QString resource_path);
|
auto getEntry(QString base, QString resource_path) -> MetaEntryPtr;
|
||||||
|
|
||||||
// get the entry from cache and verify that it isn't stale (within reason)
|
// get the entry from cache and verify that it isn't stale (within reason)
|
||||||
MetaEntryPtr resolveEntry(QString base, QString resource_path,
|
auto resolveEntry(QString base, QString resource_path, QString expected_etag = QString()) -> MetaEntryPtr;
|
||||||
QString expected_etag = QString());
|
|
||||||
|
|
||||||
// add a previously resolved stale entry
|
// add a previously resolved stale entry
|
||||||
bool updateEntry(MetaEntryPtr stale_entry);
|
auto updateEntry(MetaEntryPtr stale_entry) -> bool;
|
||||||
|
|
||||||
// evict selected entry from cache
|
// evict selected entry from cache
|
||||||
bool evictEntry(MetaEntryPtr entry);
|
auto evictEntry(MetaEntryPtr entry) -> bool;
|
||||||
|
|
||||||
void addBase(QString base, QString base_root);
|
void addBase(QString base, QString base_root);
|
||||||
|
|
||||||
// (re)start a timer that calls SaveNow later.
|
// (re)start a timer that calls SaveNow later.
|
||||||
void SaveEventually();
|
void SaveEventually();
|
||||||
void Load();
|
void Load();
|
||||||
QString getBasePath(QString base);
|
|
||||||
public
|
auto getBasePath(QString base) -> QString;
|
||||||
slots:
|
|
||||||
|
public slots:
|
||||||
void SaveNow();
|
void SaveNow();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// create a new stale entry, given the parameters
|
// create a new stale entry, given the parameters
|
||||||
MetaEntryPtr staleEntry(QString base, QString resource_path);
|
auto staleEntry(QString base, QString resource_path) -> MetaEntryPtr;
|
||||||
struct EntryMap
|
|
||||||
{
|
struct EntryMap {
|
||||||
QString base_path;
|
QString base_path;
|
||||||
QMap<QString, MetaEntryPtr> entry_list;
|
QMap<QString, MetaEntryPtr> entry_list;
|
||||||
};
|
};
|
||||||
|
|
||||||
QMap<QString, EntryMap> m_entries;
|
QMap<QString, EntryMap> m_entries;
|
||||||
QString m_index_file;
|
QString m_index_file;
|
||||||
QTimer saveBatchingTimer;
|
QTimer saveBatchingTimer;
|
||||||
|
@ -1,10 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
namespace Net
|
namespace Net {
|
||||||
{
|
enum class Mode { Offline, Online };
|
||||||
enum class Mode
|
|
||||||
{
|
|
||||||
Offline,
|
|
||||||
Online
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
@ -8,14 +8,15 @@ namespace Net {
|
|||||||
class Sink {
|
class Sink {
|
||||||
public:
|
public:
|
||||||
Sink() = default;
|
Sink() = default;
|
||||||
virtual ~Sink(){};
|
virtual ~Sink() = default;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
virtual Task::State init(QNetworkRequest& request) = 0;
|
virtual auto init(QNetworkRequest& request) -> Task::State = 0;
|
||||||
virtual Task::State write(QByteArray& data) = 0;
|
virtual auto write(QByteArray& data) -> Task::State = 0;
|
||||||
virtual Task::State abort() = 0;
|
virtual auto abort() -> Task::State = 0;
|
||||||
virtual Task::State finalize(QNetworkReply& reply) = 0;
|
virtual auto finalize(QNetworkReply& reply) -> Task::State = 0;
|
||||||
virtual bool hasLocalData() = 0;
|
|
||||||
|
virtual auto hasLocalData() -> bool = 0;
|
||||||
|
|
||||||
void addValidator(Validator* validator)
|
void addValidator(Validator* validator)
|
||||||
{
|
{
|
||||||
@ -24,7 +25,15 @@ class Sink {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected: /* methods */
|
protected:
|
||||||
|
bool initAllValidators(QNetworkRequest& request)
|
||||||
|
{
|
||||||
|
for (auto& validator : validators) {
|
||||||
|
if (!validator->init(request))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
bool finalizeAllValidators(QNetworkReply& reply)
|
bool finalizeAllValidators(QNetworkReply& reply)
|
||||||
{
|
{
|
||||||
for (auto& validator : validators) {
|
for (auto& validator : validators) {
|
||||||
@ -41,14 +50,6 @@ class Sink {
|
|||||||
}
|
}
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
bool initAllValidators(QNetworkRequest& request)
|
|
||||||
{
|
|
||||||
for (auto& validator : validators) {
|
|
||||||
if (!validator->init(request))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
bool writeAllValidators(QByteArray& data)
|
bool writeAllValidators(QByteArray& data)
|
||||||
{
|
{
|
||||||
for (auto& validator : validators) {
|
for (auto& validator : validators) {
|
||||||
@ -58,7 +59,7 @@ class Sink {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected: /* data */
|
protected:
|
||||||
std::vector<std::shared_ptr<Validator>> validators;
|
std::vector<std::shared_ptr<Validator>> validators;
|
||||||
};
|
};
|
||||||
} // namespace Net
|
} // namespace Net
|
||||||
|
Loading…
Reference in New Issue
Block a user