#include "seasonalpackdownloader.h" #include "localization.h" #include #include #include #include #include #include #include #include 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")); 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"); m_currentFile = QFileInfo(targetPath).fileName(); if (!QDir().mkpath(QFileInfo(targetPath).absolutePath())) { fail(Loc::text("common.error.createFailed") .arg(QDir::toNativeSeparators(QFileInfo(targetPath).absolutePath()))); return; } m_file = std::make_unique(targetPath); if (!m_file->open(QIODevice::WriteOnly)) { fail(Loc::text("common.error.openForWriteFailed").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(Loc::text("seasonal.error.downloadFailed").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(Loc::text("seasonal.error.checksumMismatch")); return; } if (!m_file->commit()) { m_file.reset(); fail(Loc::text("common.error.saveFailed").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(); }