Files

218 lines
6.6 KiB
C++
Raw Permalink Normal View History

2026-08-31 00:59:14 +03:00
#include "seasonalpackdownloader.h"
#include "localization.h"
2026-08-31 00:59:14 +03:00
#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, Loc::text("seasonal.error.downloadInProgress"));
2026-08-31 00:59:14 +03:00
return;
}
m_running = true;
m_canceling = false;
m_targetPath = targetPath;
m_sha256 = sha256;
m_label = label;
m_bytesDone = 0;
m_bytesTotal = expectedSize;
m_stage = Loc::text("seasonal.progress.downloadingFiles");
2026-08-31 00:59:14 +03:00
m_currentFile = QFileInfo(targetPath).fileName();
if (!QDir().mkpath(QFileInfo(targetPath).absolutePath())) {
fail(Loc::text("common.error.createFailed")
2026-08-31 00:59:14 +03:00
.arg(QDir::toNativeSeparators(QFileInfo(targetPath).absolutePath())));
return;
}
m_file = std::make_unique<QSaveFile>(targetPath);
if (!m_file->open(QIODevice::WriteOnly)) {
fail(Loc::text("common.error.openForWriteFailed").arg(QDir::toNativeSeparators(targetPath)));
2026-08-31 00:59:14 +03:00
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(Loc::text("seasonal.error.downloadFailed").arg(reply->errorString()));
2026-08-31 00:59:14 +03:00
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(Loc::text("seasonal.error.checksumMismatch"));
2026-08-31 00:59:14 +03:00
return;
}
if (!m_file->commit()) {
m_file.reset();
fail(Loc::text("common.error.saveFailed").arg(QDir::toNativeSeparators(m_targetPath)));
2026-08-31 00:59:14 +03:00
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();
}