java implementation and downloading
This commit is contained in:
@@ -0,0 +1,783 @@
|
||||
#include "javainstaller.h"
|
||||
|
||||
#include "launcherpaths.h"
|
||||
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QProcess>
|
||||
#include <QSaveFile>
|
||||
#include <QTimer>
|
||||
#include <QtCore/private/qzipreader_p.h>
|
||||
|
||||
#include <utility>
|
||||
|
||||
// Читатель архива вместе с разобранным оглавлением: fileInfoList() каждый раз
|
||||
// заново разбирает центральный каталог zip, а записей в JDK десятки тысяч.
|
||||
struct ZipExtraction
|
||||
{
|
||||
explicit ZipExtraction(const QString &path)
|
||||
: reader(path)
|
||||
{
|
||||
}
|
||||
|
||||
QZipReader reader;
|
||||
QList<QZipReader::FileInfo> entries;
|
||||
int index = 0;
|
||||
};
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kTransferTimeoutMs = 30000;
|
||||
constexpr int kMaxParallel = 4;
|
||||
constexpr int kMaxAttempts = 3;
|
||||
constexpr int kProgressIntervalMs = 100;
|
||||
|
||||
// Записей zip за один проход таймера. Сотня файлов по паре мегабайт — это
|
||||
// заметная, но ещё не видимая глазом задержка кадра.
|
||||
constexpr int kZipChunk = 24;
|
||||
|
||||
bool isTransient(QNetworkReply::NetworkError error, int status)
|
||||
{
|
||||
if (status >= 500)
|
||||
return true;
|
||||
switch (error) {
|
||||
case QNetworkReply::TimeoutError:
|
||||
case QNetworkReply::TemporaryNetworkFailureError:
|
||||
case QNetworkReply::NetworkSessionFailedError:
|
||||
case QNetworkReply::ProxyTimeoutError:
|
||||
case QNetworkReply::UnknownNetworkError:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Путь из манифеста или архива не должен уводить за пределы папки сборки:
|
||||
// «../../» в имени записи — известный способ подменить чужой файл.
|
||||
bool isInside(const QString &root, const QString &path)
|
||||
{
|
||||
const QString normalizedRoot = QDir::cleanPath(root) + u'/';
|
||||
const QString normalized = QDir::cleanPath(path);
|
||||
return normalized.startsWith(normalizedRoot);
|
||||
}
|
||||
|
||||
void makeExecutable(const QString &path)
|
||||
{
|
||||
const QFileDevice::Permissions current = QFile::permissions(path);
|
||||
QFile::setPermissions(path,
|
||||
current | QFileDevice::ExeOwner | QFileDevice::ExeGroup
|
||||
| QFileDevice::ExeOther);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
JavaInstaller::JavaInstaller(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_network(new QNetworkAccessManager(this))
|
||||
, m_extractTimer(new QTimer(this))
|
||||
, m_progressTimer(new QTimer(this))
|
||||
{
|
||||
m_extractTimer->setInterval(0);
|
||||
connect(m_extractTimer, &QTimer::timeout, this, &JavaInstaller::extractZipChunk);
|
||||
|
||||
m_progressTimer->setInterval(kProgressIntervalMs);
|
||||
connect(m_progressTimer, &QTimer::timeout, this, &JavaInstaller::flushProgress);
|
||||
}
|
||||
|
||||
JavaInstaller::~JavaInstaller() = default;
|
||||
|
||||
qint64 JavaInstaller::bytesDone() const
|
||||
{
|
||||
qint64 total = m_bytesSettled;
|
||||
for (const auto &context : m_active)
|
||||
total += context->received;
|
||||
return total;
|
||||
}
|
||||
|
||||
double JavaInstaller::fraction() const
|
||||
{
|
||||
if (m_bytesTotal > 0)
|
||||
return qBound(0.0, double(bytesDone()) / double(m_bytesTotal), 1.0);
|
||||
if (m_stepsTotal > 0)
|
||||
return qBound(0.0, double(m_stepsDone) / double(m_stepsTotal), 1.0);
|
||||
return -1.0;
|
||||
}
|
||||
|
||||
void JavaInstaller::install(const JavaRuntimeEntry &entry)
|
||||
{
|
||||
if (m_running)
|
||||
return;
|
||||
if (!entry.isValid()) {
|
||||
emit failed(entry.version, tr("Сборка Java недоступна для загрузки"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString error;
|
||||
if (!LauncherPaths::ensureRootExists(&error)) {
|
||||
emit failed(entry.version, tr("Папка лаунчера недоступна: %1").arg(error));
|
||||
return;
|
||||
}
|
||||
|
||||
m_entry = entry;
|
||||
m_label = tr("%1 %2").arg(javaKindTitle(entry.kind), entry.version);
|
||||
m_targetDir = JavaRuntimeStore::dirFor(entry.id);
|
||||
if (m_targetDir.isEmpty()) {
|
||||
emit failed(m_label, tr("Недопустимый идентификатор сборки Java"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Переустановка поверх недокачанной прошлой попытки: старое дерево мешает
|
||||
// и размером, и тем, что locateBinary нашла бы в нём чужой java.
|
||||
if (!JavaRuntimeStore::remove(entry.id, &error)) {
|
||||
emit failed(m_label, error);
|
||||
return;
|
||||
}
|
||||
if (!QDir().mkpath(m_targetDir)) {
|
||||
emit failed(m_label, tr("Не удалось создать %1").arg(QDir::toNativeSeparators(m_targetDir)));
|
||||
return;
|
||||
}
|
||||
|
||||
m_running = true;
|
||||
m_canceling = false;
|
||||
m_bytesSettled = 0;
|
||||
m_bytesTotal = 0;
|
||||
m_stepsDone = 0;
|
||||
m_stepsTotal = 0;
|
||||
m_currentFile.clear();
|
||||
m_links.clear();
|
||||
m_queue.clear();
|
||||
|
||||
emit started(m_label);
|
||||
if (entry.archive == QLatin1String("mojang"))
|
||||
beginMojang();
|
||||
else
|
||||
beginArchive();
|
||||
}
|
||||
|
||||
void JavaInstaller::cancel()
|
||||
{
|
||||
if (!m_running || m_canceling)
|
||||
return;
|
||||
|
||||
m_canceling = true;
|
||||
m_extractTimer->stop();
|
||||
|
||||
if (m_reply)
|
||||
m_reply->abort();
|
||||
for (const auto &context : std::as_const(m_active))
|
||||
context->reply->abort();
|
||||
if (m_tar)
|
||||
m_tar->kill();
|
||||
|
||||
// Прерванная установка оставляет дерево наполовину собранным: запускать из
|
||||
// него нечего, а locateBinary могла бы найти в нём java.
|
||||
if (m_active.isEmpty() && !m_reply && !m_tar) {
|
||||
const QString label = m_label;
|
||||
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||||
reset();
|
||||
emit canceled(label);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Temurin: один архив ─────────────────────────────────────────────────────
|
||||
|
||||
void JavaInstaller::beginArchive()
|
||||
{
|
||||
if (m_entry.archive != QLatin1String("zip")
|
||||
&& m_entry.archive != QLatin1String("tar.gz")) {
|
||||
fail(tr("Неизвестный формат архива: %1").arg(m_entry.archive));
|
||||
return;
|
||||
}
|
||||
|
||||
const QString name = m_entry.url.fileName();
|
||||
m_archivePath = LauncherPaths::javaDownloadDir() + u'/'
|
||||
+ (name.isEmpty() ? m_entry.id + QStringLiteral(".archive") : name);
|
||||
m_bytesTotal = m_entry.size;
|
||||
setStage(tr("Загрузка Java %1").arg(m_entry.version));
|
||||
downloadArchive();
|
||||
}
|
||||
|
||||
void JavaInstaller::downloadArchive()
|
||||
{
|
||||
m_archiveFile = std::make_unique<QSaveFile>(m_archivePath);
|
||||
if (!m_archiveFile->open(QIODevice::WriteOnly)) {
|
||||
fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(m_archivePath)));
|
||||
return;
|
||||
}
|
||||
m_archiveHash.reset();
|
||||
m_currentFile = QFileInfo(m_archivePath).fileName();
|
||||
|
||||
QNetworkRequest request{m_entry.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_archiveHash.addData(chunk);
|
||||
if (m_archiveFile->write(chunk) != chunk.size())
|
||||
m_reply->abort();
|
||||
});
|
||||
connect(m_reply, &QNetworkReply::downloadProgress, this,
|
||||
[this](qint64 received, qint64 total) {
|
||||
m_bytesSettled = received;
|
||||
if (m_bytesTotal <= 0 && total > 0)
|
||||
m_bytesTotal = total;
|
||||
scheduleProgress();
|
||||
});
|
||||
connect(m_reply, &QNetworkReply::finished, this,
|
||||
[this] { handleArchiveFinished(m_reply); });
|
||||
|
||||
scheduleProgress();
|
||||
}
|
||||
|
||||
void JavaInstaller::handleArchiveFinished(QNetworkReply *reply)
|
||||
{
|
||||
reply->deleteLater();
|
||||
m_reply = nullptr;
|
||||
|
||||
if (m_canceling) {
|
||||
m_archiveFile->cancelWriting();
|
||||
m_archiveFile.reset();
|
||||
const QString label = m_label;
|
||||
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||||
reset();
|
||||
emit canceled(label);
|
||||
return;
|
||||
}
|
||||
if (!m_running)
|
||||
return;
|
||||
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
m_archiveFile->cancelWriting();
|
||||
m_archiveFile.reset();
|
||||
fail(tr("Не удалось скачать Java %1: %2").arg(m_entry.version, reply->errorString()));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_entry.checksum.isEmpty()) {
|
||||
const QString actual = QString::fromLatin1(m_archiveHash.result().toHex());
|
||||
if (actual.compare(m_entry.checksum, Qt::CaseInsensitive) != 0) {
|
||||
m_archiveFile->cancelWriting();
|
||||
m_archiveFile.reset();
|
||||
fail(tr("Контрольная сумма архива Java не совпала — загрузка повреждена"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!m_archiveFile->commit()) {
|
||||
m_archiveFile.reset();
|
||||
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(m_archivePath)));
|
||||
return;
|
||||
}
|
||||
m_archiveFile.reset();
|
||||
|
||||
startExtraction();
|
||||
}
|
||||
|
||||
void JavaInstaller::startExtraction()
|
||||
{
|
||||
// Байты кончились, дальше считаем шаги: панель по нулевому итогу перестанет
|
||||
// показывать мегабайты, а проценты продолжит.
|
||||
m_bytesTotal = 0;
|
||||
m_bytesSettled = 0;
|
||||
setStage(tr("Распаковка Java %1").arg(m_entry.version));
|
||||
|
||||
if (m_entry.archive == QLatin1String("tar.gz")) {
|
||||
startTarExtraction();
|
||||
return;
|
||||
}
|
||||
|
||||
m_zip = std::make_unique<ZipExtraction>(m_archivePath);
|
||||
if (!m_zip->reader.isReadable()) {
|
||||
m_zip.reset();
|
||||
fail(tr("Не удалось прочитать архив %1").arg(QDir::toNativeSeparators(m_archivePath)));
|
||||
return;
|
||||
}
|
||||
|
||||
m_zip->entries = m_zip->reader.fileInfoList();
|
||||
m_stepsDone = 0;
|
||||
m_stepsTotal = m_zip->entries.size();
|
||||
m_extractTimer->start();
|
||||
}
|
||||
|
||||
void JavaInstaller::extractZipChunk()
|
||||
{
|
||||
if (m_canceling || !m_running || !m_zip) {
|
||||
m_extractTimer->stop();
|
||||
return;
|
||||
}
|
||||
|
||||
const QList<QZipReader::FileInfo> &entries = m_zip->entries;
|
||||
const int end = qMin(m_zip->index + kZipChunk, entries.size());
|
||||
for (; m_zip->index < end; ++m_zip->index) {
|
||||
const QZipReader::FileInfo &info = entries.at(m_zip->index);
|
||||
const QString target = m_targetDir + u'/' + info.filePath;
|
||||
if (!isInside(m_targetDir, target)) {
|
||||
m_extractTimer->stop();
|
||||
fail(tr("Архив Java содержит недопустимый путь: %1").arg(info.filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.isDir) {
|
||||
QDir().mkpath(target);
|
||||
continue;
|
||||
}
|
||||
// Ссылок в zip-сборках Temurin не бывает — под Unix они отдают tar.gz.
|
||||
if (!info.isFile)
|
||||
continue;
|
||||
|
||||
if (!QDir().mkpath(QFileInfo(target).absolutePath())) {
|
||||
m_extractTimer->stop();
|
||||
fail(tr("Не удалось создать %1")
|
||||
.arg(QDir::toNativeSeparators(QFileInfo(target).absolutePath())));
|
||||
return;
|
||||
}
|
||||
|
||||
const QByteArray data = m_zip->reader.fileData(info.filePath);
|
||||
QFile file(target);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(data) != data.size()) {
|
||||
m_extractTimer->stop();
|
||||
fail(tr("Не удалось распаковать %1").arg(info.filePath));
|
||||
return;
|
||||
}
|
||||
file.close();
|
||||
if (info.permissions != QFileDevice::Permissions())
|
||||
file.setPermissions(info.permissions);
|
||||
}
|
||||
|
||||
m_stepsDone = m_zip->index;
|
||||
m_currentFile = m_zip->index > 0 ? entries.at(m_zip->index - 1).filePath : QString();
|
||||
scheduleProgress();
|
||||
|
||||
if (m_zip->index >= entries.size()) {
|
||||
m_extractTimer->stop();
|
||||
m_zip.reset();
|
||||
complete();
|
||||
}
|
||||
}
|
||||
|
||||
void JavaInstaller::startTarExtraction()
|
||||
{
|
||||
// tar есть в любой macOS и практически в любом дистрибутиве Linux; своего
|
||||
// распаковщика gzip-tar в Qt нет, а тянуть ради него зависимость незачем.
|
||||
m_tar = new QProcess(this);
|
||||
m_tar->setWorkingDirectory(m_targetDir);
|
||||
m_tar->setProgram(QStringLiteral("tar"));
|
||||
m_tar->setArguments({QStringLiteral("-xzf"), m_archivePath,
|
||||
QStringLiteral("-C"), m_targetDir});
|
||||
|
||||
connect(m_tar, &QProcess::errorOccurred, this, [this](QProcess::ProcessError) {
|
||||
if (!m_tar)
|
||||
return;
|
||||
const QString message = m_tar->errorString();
|
||||
m_tar->deleteLater();
|
||||
m_tar = nullptr;
|
||||
if (m_canceling) {
|
||||
const QString label = m_label;
|
||||
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||||
reset();
|
||||
emit canceled(label);
|
||||
return;
|
||||
}
|
||||
fail(tr("Не удалось распаковать архив Java: %1").arg(message));
|
||||
});
|
||||
|
||||
connect(m_tar, &QProcess::finished, this, [this](int exitCode, QProcess::ExitStatus status) {
|
||||
if (!m_tar)
|
||||
return;
|
||||
const QByteArray errorOutput = m_tar->readAllStandardError();
|
||||
m_tar->deleteLater();
|
||||
m_tar = nullptr;
|
||||
|
||||
if (m_canceling) {
|
||||
const QString label = m_label;
|
||||
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||||
reset();
|
||||
emit canceled(label);
|
||||
return;
|
||||
}
|
||||
if (status != QProcess::NormalExit || exitCode != 0) {
|
||||
fail(tr("Распаковка архива Java не удалась: %1")
|
||||
.arg(QString::fromLocal8Bit(errorOutput).trimmed()));
|
||||
return;
|
||||
}
|
||||
complete();
|
||||
});
|
||||
|
||||
m_tar->start();
|
||||
}
|
||||
|
||||
// ── Mojang: дерево файлов ───────────────────────────────────────────────────
|
||||
|
||||
void JavaInstaller::beginMojang()
|
||||
{
|
||||
setStage(tr("Список файлов Java %1").arg(m_entry.version));
|
||||
m_currentFile = QStringLiteral("manifest.json");
|
||||
|
||||
QNetworkRequest request{m_entry.url};
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setTransferTimeout(kTransferTimeoutMs);
|
||||
|
||||
m_reply = m_network->get(request);
|
||||
connect(m_reply, &QNetworkReply::finished, this, [this] {
|
||||
QNetworkReply *reply = m_reply;
|
||||
reply->deleteLater();
|
||||
m_reply = nullptr;
|
||||
|
||||
if (m_canceling) {
|
||||
const QString label = m_label;
|
||||
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||||
reset();
|
||||
emit canceled(label);
|
||||
return;
|
||||
}
|
||||
if (!m_running)
|
||||
return;
|
||||
if (reply->error() != QNetworkReply::NoError) {
|
||||
fail(tr("Не удалось получить состав рантайма: %1").arg(reply->errorString()));
|
||||
return;
|
||||
}
|
||||
handleManifest(reply->readAll());
|
||||
});
|
||||
}
|
||||
|
||||
void JavaInstaller::handleManifest(const QByteArray &payload)
|
||||
{
|
||||
const QJsonObject files = QJsonDocument::fromJson(payload)
|
||||
.object()
|
||||
.value(QStringLiteral("files"))
|
||||
.toObject();
|
||||
if (files.isEmpty()) {
|
||||
fail(tr("Состав рантайма Java не удалось разобрать"));
|
||||
return;
|
||||
}
|
||||
if (!prepareTree(files))
|
||||
return;
|
||||
|
||||
setStage(tr("Загрузка Java %1").arg(m_entry.version));
|
||||
if (m_queue.isEmpty()) {
|
||||
if (applyLinks())
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
pump();
|
||||
}
|
||||
|
||||
bool JavaInstaller::prepareTree(const QJsonObject &files)
|
||||
{
|
||||
m_bytesTotal = 0;
|
||||
|
||||
for (auto it = files.constBegin(); it != files.constEnd(); ++it) {
|
||||
const QString target = m_targetDir + u'/' + it.key();
|
||||
if (!isInside(m_targetDir, target)) {
|
||||
fail(tr("Состав рантайма содержит недопустимый путь: %1").arg(it.key()));
|
||||
return false;
|
||||
}
|
||||
|
||||
const QJsonObject node = it.value().toObject();
|
||||
const QString type = node.value(QStringLiteral("type")).toString();
|
||||
|
||||
if (type == QLatin1String("directory")) {
|
||||
if (!QDir().mkpath(target)) {
|
||||
fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(target)));
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (type == QLatin1String("link")) {
|
||||
// Ссылки создаём в конце: их цели ещё не скачаны.
|
||||
m_links << qMakePair(target, node.value(QStringLiteral("target")).toString());
|
||||
continue;
|
||||
}
|
||||
if (type != QLatin1String("file"))
|
||||
continue;
|
||||
|
||||
const QJsonObject raw = node.value(QStringLiteral("downloads"))
|
||||
.toObject()
|
||||
.value(QStringLiteral("raw"))
|
||||
.toObject();
|
||||
const QUrl url(raw.value(QStringLiteral("url")).toString());
|
||||
if (url.scheme() != QLatin1String("https"))
|
||||
continue;
|
||||
|
||||
JavaFileTask task;
|
||||
task.url = url;
|
||||
task.path = target;
|
||||
task.sha1 = raw.value(QStringLiteral("sha1")).toString();
|
||||
task.size = static_cast<qint64>(raw.value(QStringLiteral("size")).toDouble());
|
||||
task.executable = node.value(QStringLiteral("executable")).toBool();
|
||||
|
||||
m_bytesTotal += task.size;
|
||||
m_queue.enqueue(task);
|
||||
}
|
||||
|
||||
m_stepsTotal = m_queue.size();
|
||||
m_stepsDone = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void JavaInstaller::pump()
|
||||
{
|
||||
if (!m_running || m_canceling)
|
||||
return;
|
||||
|
||||
while (m_active.size() < kMaxParallel && !m_queue.isEmpty())
|
||||
startTask(m_queue.dequeue());
|
||||
|
||||
if (m_active.isEmpty() && m_queue.isEmpty()) {
|
||||
if (applyLinks())
|
||||
complete();
|
||||
}
|
||||
}
|
||||
|
||||
void JavaInstaller::startTask(JavaFileTask task)
|
||||
{
|
||||
const QFileInfo info(task.path);
|
||||
if (!QDir().mkpath(info.absolutePath())) {
|
||||
fail(tr("Не удалось создать %1").arg(QDir::toNativeSeparators(info.absolutePath())));
|
||||
return;
|
||||
}
|
||||
|
||||
auto context = std::make_shared<JavaActiveDownload>();
|
||||
context->task = task;
|
||||
context->file = std::make_unique<QSaveFile>(task.path);
|
||||
if (!context->file->open(QIODevice::WriteOnly)) {
|
||||
fail(tr("Не удалось открыть на запись %1").arg(QDir::toNativeSeparators(task.path)));
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest request{task.url};
|
||||
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
|
||||
QNetworkRequest::NoLessSafeRedirectPolicy);
|
||||
request.setTransferTimeout(kTransferTimeoutMs);
|
||||
|
||||
context->reply = m_network->get(request);
|
||||
m_active.append(context);
|
||||
m_currentFile = QDir(m_targetDir).relativeFilePath(task.path);
|
||||
|
||||
connect(context->reply, &QNetworkReply::readyRead, this, [this, context] {
|
||||
const QByteArray chunk = context->reply->readAll();
|
||||
if (chunk.isEmpty())
|
||||
return;
|
||||
context->hash.addData(chunk);
|
||||
if (context->file->write(chunk) != chunk.size())
|
||||
context->reply->abort();
|
||||
});
|
||||
connect(context->reply, &QNetworkReply::downloadProgress, this,
|
||||
[this, context](qint64 received, qint64) {
|
||||
context->received = received;
|
||||
scheduleProgress();
|
||||
});
|
||||
connect(context->reply, &QNetworkReply::finished, this,
|
||||
[this, context] { handleFileFinished(context); });
|
||||
|
||||
scheduleProgress();
|
||||
}
|
||||
|
||||
void JavaInstaller::handleFileFinished(const std::shared_ptr<JavaActiveDownload> &context)
|
||||
{
|
||||
context->reply->deleteLater();
|
||||
m_active.removeOne(context);
|
||||
|
||||
if (m_canceling) {
|
||||
context->file->cancelWriting();
|
||||
if (m_active.isEmpty()) {
|
||||
const QString label = m_label;
|
||||
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||||
reset();
|
||||
emit canceled(label);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!m_running)
|
||||
return;
|
||||
|
||||
const int status =
|
||||
context->reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (context->reply->error() != QNetworkReply::NoError) {
|
||||
context->file->cancelWriting();
|
||||
const QString reason = context->reply->errorString();
|
||||
if (isTransient(context->reply->error(), status)) {
|
||||
if (!retryOrFail(context->task, reason))
|
||||
return;
|
||||
} else {
|
||||
fail(tr("Не удалось скачать %1: %2")
|
||||
.arg(QDir(m_targetDir).relativeFilePath(context->task.path), reason));
|
||||
return;
|
||||
}
|
||||
pump();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!context->task.sha1.isEmpty()) {
|
||||
const QString actual = QString::fromLatin1(context->hash.result().toHex());
|
||||
if (actual.compare(context->task.sha1, Qt::CaseInsensitive) != 0) {
|
||||
context->file->cancelWriting();
|
||||
if (!retryOrFail(context->task, tr("контрольная сумма не совпала")))
|
||||
return;
|
||||
pump();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!context->file->commit()) {
|
||||
fail(tr("Не удалось сохранить %1").arg(QDir::toNativeSeparators(context->task.path)));
|
||||
return;
|
||||
}
|
||||
if (context->task.executable)
|
||||
makeExecutable(context->task.path);
|
||||
|
||||
m_bytesSettled += context->received;
|
||||
++m_stepsDone;
|
||||
scheduleProgress();
|
||||
pump();
|
||||
}
|
||||
|
||||
bool JavaInstaller::retryOrFail(JavaFileTask task, const QString &reason)
|
||||
{
|
||||
if (++task.attempts >= kMaxAttempts) {
|
||||
fail(tr("Не удалось скачать %1: %2")
|
||||
.arg(QDir(m_targetDir).relativeFilePath(task.path), reason));
|
||||
return false;
|
||||
}
|
||||
m_queue.enqueue(task);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JavaInstaller::applyLinks()
|
||||
{
|
||||
for (const auto &link : std::as_const(m_links)) {
|
||||
const QString linkPath = link.first;
|
||||
if (QFileInfo::exists(linkPath))
|
||||
continue;
|
||||
if (!QDir().mkpath(QFileInfo(linkPath).absolutePath()))
|
||||
continue;
|
||||
|
||||
// Цель относительна самой ссылке — QFile::link кладёт её как есть.
|
||||
if (!QFile::link(link.second, linkPath)) {
|
||||
// Под Windows символических ссылок в рантаймах Mojang нет, но если
|
||||
// они появятся, копия работает не хуже.
|
||||
const QString source =
|
||||
QDir::cleanPath(QFileInfo(linkPath).absolutePath() + u'/' + link.second);
|
||||
if (!QFile::copy(source, linkPath)) {
|
||||
fail(tr("Не удалось создать ссылку %1")
|
||||
.arg(QDir(m_targetDir).relativeFilePath(linkPath)));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Завершение ──────────────────────────────────────────────────────────────
|
||||
|
||||
void JavaInstaller::complete()
|
||||
{
|
||||
setStage(tr("Проверка сборки Java"));
|
||||
|
||||
const QString javaPath = JavaRuntimeStore::locateBinary(m_targetDir);
|
||||
if (javaPath.isEmpty()) {
|
||||
fail(tr("В скачанной сборке не нашлось исполняемого файла java"));
|
||||
return;
|
||||
}
|
||||
#if !defined(Q_OS_WIN)
|
||||
// tar сохраняет права, а вот zip и файлы Mojang без флага executable — нет.
|
||||
makeExecutable(javaPath);
|
||||
#endif
|
||||
|
||||
InstalledJavaRuntime runtime;
|
||||
runtime.id = m_entry.id;
|
||||
runtime.kind = javaKindKey(m_entry.kind);
|
||||
runtime.major = m_entry.major;
|
||||
runtime.version = m_entry.version;
|
||||
runtime.javaPath = javaPath;
|
||||
runtime.size = m_entry.size > 0 ? m_entry.size : m_bytesTotal;
|
||||
|
||||
QString error;
|
||||
if (!JavaRuntimeStore::writeMeta(runtime, &error)) {
|
||||
fail(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Архив нужен был только на распаковку — двести мегабайт в кэше ни к чему.
|
||||
if (!m_archivePath.isEmpty())
|
||||
QFile::remove(m_archivePath);
|
||||
|
||||
const QString id = m_entry.id;
|
||||
reset();
|
||||
emit finished(id, javaPath);
|
||||
}
|
||||
|
||||
void JavaInstaller::fail(const QString &message)
|
||||
{
|
||||
const QString label = m_label;
|
||||
m_extractTimer->stop();
|
||||
m_zip.reset();
|
||||
if (!m_archivePath.isEmpty())
|
||||
QFile::remove(m_archivePath);
|
||||
// Половина рантайма хуже, чем его отсутствие: следующая попытка начнётся с чистой папки.
|
||||
JavaRuntimeStore::remove(m_entry.id, nullptr);
|
||||
reset();
|
||||
emit failed(label, message);
|
||||
}
|
||||
|
||||
void JavaInstaller::reset()
|
||||
{
|
||||
m_running = false;
|
||||
m_canceling = false;
|
||||
m_progressTimer->stop();
|
||||
m_extractTimer->stop();
|
||||
m_zip.reset();
|
||||
m_queue.clear();
|
||||
m_active.clear();
|
||||
m_links.clear();
|
||||
m_archiveFile.reset();
|
||||
m_archivePath.clear();
|
||||
m_bytesSettled = 0;
|
||||
m_bytesTotal = 0;
|
||||
m_stepsDone = 0;
|
||||
m_stepsTotal = 0;
|
||||
m_stage.clear();
|
||||
m_currentFile.clear();
|
||||
m_entry = {};
|
||||
m_label.clear();
|
||||
emit progressChanged();
|
||||
}
|
||||
|
||||
void JavaInstaller::setStage(const QString &stage)
|
||||
{
|
||||
if (m_stage == stage)
|
||||
return;
|
||||
m_stage = stage;
|
||||
emit progressChanged();
|
||||
}
|
||||
|
||||
void JavaInstaller::scheduleProgress()
|
||||
{
|
||||
m_progressDirty = true;
|
||||
if (!m_progressTimer->isActive())
|
||||
m_progressTimer->start();
|
||||
}
|
||||
|
||||
void JavaInstaller::flushProgress()
|
||||
{
|
||||
if (!m_progressDirty) {
|
||||
m_progressTimer->stop();
|
||||
return;
|
||||
}
|
||||
m_progressDirty = false;
|
||||
emit progressChanged();
|
||||
}
|
||||
Reference in New Issue
Block a user