seasonal builds
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
#include "seasonalpackdownloader.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QSaveFile>
|
||||
#include <QTimer>
|
||||
|
||||
namespace {
|
||||
|
||||
// Пак большой, но идёт с нашего же сервера: минуты простоя без единого байта
|
||||
// означают, что соединение умерло, а не что файл тяжёлый.
|
||||
constexpr int kTransferTimeoutMs = 60000;
|
||||
|
||||
// Столько же, сколько у остальных установщиков: сигналы прогресса приходят
|
||||
// пачками, а перерисовывать панель чаще десяти раз в секунду незачем.
|
||||
constexpr int kProgressIntervalMs = 100;
|
||||
|
||||
} // namespace
|
||||
|
||||
SeasonalPackDownloader::SeasonalPackDownloader(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_network(new QNetworkAccessManager(this))
|
||||
, m_progressTimer(new QTimer(this))
|
||||
{
|
||||
m_progressTimer->setInterval(kProgressIntervalMs);
|
||||
connect(m_progressTimer, &QTimer::timeout, this, &SeasonalPackDownloader::flushProgress);
|
||||
}
|
||||
|
||||
SeasonalPackDownloader::~SeasonalPackDownloader()
|
||||
{
|
||||
if (m_reply)
|
||||
m_reply->abort();
|
||||
}
|
||||
|
||||
double SeasonalPackDownloader::fraction() const
|
||||
{
|
||||
if (m_bytesTotal <= 0)
|
||||
return -1.0;
|
||||
return qBound(0.0, double(m_bytesDone) / double(m_bytesTotal), 1.0);
|
||||
}
|
||||
|
||||
void SeasonalPackDownloader::download(const QUrl &url,
|
||||
const QString &targetPath,
|
||||
const QString &sha256,
|
||||
qint64 expectedSize,
|
||||
const QString &label)
|
||||
{
|
||||
if (m_running) {
|
||||
emit failed(label, tr("Загрузка сборки уже идёт"));
|
||||
return;
|
||||
}
|
||||
|
||||
m_running = true;
|
||||
m_canceling = false;
|
||||
m_targetPath = targetPath;
|
||||
m_sha256 = sha256;
|
||||
m_label = label;
|
||||
m_bytesDone = 0;
|
||||
m_bytesTotal = expectedSize;
|
||||
m_stage = tr("Загрузка файлов сборки");
|
||||
m_currentFile = QFileInfo(targetPath).fileName();
|
||||
|
||||
if (!QDir().mkpath(QFileInfo(targetPath).absolutePath())) {
|
||||
fail(tr("Не удалось создать %1")
|
||||
.arg(QDir::toNativeSeparators(QFileInfo(targetPath).absolutePath())));
|
||||
return;
|
||||
}
|
||||
|
||||
m_file = std::make_unique<QSaveFile>(targetPath);
|
||||
if (!m_file->open(QIODevice::WriteOnly)) {
|
||||
fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(targetPath)));
|
||||
return;
|
||||
}
|
||||
m_hash.reset();
|
||||
|
||||
emit started(m_label);
|
||||
|
||||
QNetworkRequest request{url};
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setTransferTimeout(kTransferTimeoutMs);
|
||||
|
||||
m_reply = m_network->get(request);
|
||||
connect(m_reply, &QNetworkReply::readyRead, this, [this] {
|
||||
const QByteArray chunk = m_reply->readAll();
|
||||
if (chunk.isEmpty())
|
||||
return;
|
||||
m_hash.addData(chunk);
|
||||
// Диск кончился или файл забрали из-под нас: продолжать качать незачем,
|
||||
// ошибку разберёт обработчик finished.
|
||||
if (m_file->write(chunk) != chunk.size())
|
||||
m_reply->abort();
|
||||
});
|
||||
connect(m_reply, &QNetworkReply::downloadProgress, this,
|
||||
[this](qint64 received, qint64 total) {
|
||||
m_bytesDone = received;
|
||||
// Размер из каталога может разойтись с реальным — верим серверу.
|
||||
if (total > 0)
|
||||
m_bytesTotal = total;
|
||||
scheduleProgress();
|
||||
});
|
||||
connect(m_reply, &QNetworkReply::finished, this, [this] {
|
||||
QNetworkReply *reply = m_reply;
|
||||
m_reply = nullptr;
|
||||
reply->deleteLater();
|
||||
|
||||
if (m_canceling) {
|
||||
m_file->cancelWriting();
|
||||
m_file.reset();
|
||||
const QString label = m_label;
|
||||
reset();
|
||||
emit canceled(label);
|
||||
return;
|
||||
}
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
m_file->cancelWriting();
|
||||
m_file.reset();
|
||||
fail(tr("Не удалось скачать сборку: %1").arg(reply->errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString actual = QString::fromLatin1(m_hash.result().toHex());
|
||||
if (actual.compare(m_sha256, Qt::CaseInsensitive) != 0) {
|
||||
m_file->cancelWriting();
|
||||
m_file.reset();
|
||||
fail(tr("Контрольная сумма архива сборки не совпала — загрузка повреждена"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_file->commit()) {
|
||||
m_file.reset();
|
||||
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(m_targetPath)));
|
||||
return;
|
||||
}
|
||||
m_file.reset();
|
||||
|
||||
const QString path = m_targetPath;
|
||||
reset();
|
||||
emit finished(path);
|
||||
});
|
||||
|
||||
scheduleProgress();
|
||||
}
|
||||
|
||||
void SeasonalPackDownloader::cancel()
|
||||
{
|
||||
if (!m_running || m_canceling)
|
||||
return;
|
||||
|
||||
m_canceling = true;
|
||||
if (m_reply) {
|
||||
m_reply->abort();
|
||||
return;
|
||||
}
|
||||
|
||||
// Запрос ещё не создан — обрывать нечего, отвечаем сразу.
|
||||
if (m_file)
|
||||
m_file->cancelWriting();
|
||||
m_file.reset();
|
||||
const QString label = m_label;
|
||||
reset();
|
||||
emit canceled(label);
|
||||
}
|
||||
|
||||
void SeasonalPackDownloader::fail(const QString &message)
|
||||
{
|
||||
const QString label = m_label;
|
||||
if (m_file)
|
||||
m_file->cancelWriting();
|
||||
m_file.reset();
|
||||
// Обрезанный пак нельзя ни распаковать, ни докачать: следующая попытка
|
||||
// начнётся с чистого файла.
|
||||
if (!m_targetPath.isEmpty())
|
||||
QFile::remove(m_targetPath);
|
||||
reset();
|
||||
emit failed(label, message);
|
||||
}
|
||||
|
||||
void SeasonalPackDownloader::reset()
|
||||
{
|
||||
m_running = false;
|
||||
m_canceling = false;
|
||||
m_progressTimer->stop();
|
||||
m_progressDirty = false;
|
||||
m_file.reset();
|
||||
m_targetPath.clear();
|
||||
m_sha256.clear();
|
||||
m_label.clear();
|
||||
m_stage.clear();
|
||||
m_currentFile.clear();
|
||||
m_bytesDone = 0;
|
||||
m_bytesTotal = 0;
|
||||
emit progressChanged();
|
||||
}
|
||||
|
||||
void SeasonalPackDownloader::scheduleProgress()
|
||||
{
|
||||
m_progressDirty = true;
|
||||
if (!m_progressTimer->isActive())
|
||||
m_progressTimer->start();
|
||||
}
|
||||
|
||||
void SeasonalPackDownloader::flushProgress()
|
||||
{
|
||||
if (!m_progressDirty) {
|
||||
m_progressTimer->stop();
|
||||
return;
|
||||
}
|
||||
m_progressDirty = false;
|
||||
emit progressChanged();
|
||||
}
|
||||
Reference in New Issue
Block a user