217 lines
6.7 KiB
C++
217 lines
6.7 KiB
C++
|
|
#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();
|
||
|
|
}
|