982 lines
46 KiB
C++
982 lines
46 KiB
C++
#include "UpdaterLogic.h"
|
|
#include <QSet>
|
|
#include <QDebug>
|
|
#include <QFileInfo>
|
|
#include <QFile>
|
|
#include <QCoreApplication>
|
|
#include <QEventLoop>
|
|
#include <QElapsedTimer>
|
|
#include <QThread>
|
|
#include <QNetworkRequest>
|
|
#include <QNetworkProxy>
|
|
#include <QJsonArray>
|
|
#include <QCryptographicHash>
|
|
#include <QDir>
|
|
#include <QJsonDocument>
|
|
#include <QSaveFile>
|
|
#include <QApplication>
|
|
#include <QUrl>
|
|
#include <QUrlQuery>
|
|
#include <algorithm>
|
|
#include "ConfigHelper.h"
|
|
#include "UpdatePathPolicy.h"
|
|
|
|
#ifdef HAVE_OPENSSL
|
|
#include <openssl/pem.h>
|
|
#include <openssl/evp.h>
|
|
#include <openssl/bio.h>
|
|
#include <openssl/err.h>
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
QString trimBaseUrl(QString value)
|
|
{
|
|
value = value.trimmed();
|
|
while (value.endsWith(QLatin1Char('/')))
|
|
value.chop(1);
|
|
return value;
|
|
}
|
|
|
|
QString configValue(const QString& key, const QString& fallback = QString())
|
|
{
|
|
const QString value = ConfigHelper::instance().getValue(QString(), key).trimmed();
|
|
return value.isEmpty() ? fallback : value;
|
|
}
|
|
|
|
bool configFlag(const QString& key)
|
|
{
|
|
const QString value = configValue(key).toLower();
|
|
return value == QStringLiteral("true")
|
|
|| value == QStringLiteral("1")
|
|
|| value == QStringLiteral("yes")
|
|
|| value == QStringLiteral("on");
|
|
}
|
|
|
|
void addQueryValue(QUrlQuery& query, const QString& key, const QString& value)
|
|
{
|
|
const QString trimmed = value.trimmed();
|
|
if (!trimmed.isEmpty())
|
|
query.addQueryItem(key, trimmed);
|
|
}
|
|
|
|
QString serverDetailMessage(const QJsonObject& response)
|
|
{
|
|
const QString msg = response.value(QStringLiteral("msg")).toString();
|
|
if (!msg.isEmpty())
|
|
return msg;
|
|
|
|
const QJsonValue detail = response.value(QStringLiteral("detail"));
|
|
if (detail.isObject()) {
|
|
const QJsonObject obj = detail.toObject();
|
|
const QString msg = obj.value(QStringLiteral("msg")).toString();
|
|
if (!msg.isEmpty()) return msg;
|
|
const QString error = obj.value(QStringLiteral("error")).toString();
|
|
if (!error.isEmpty()) return error;
|
|
}
|
|
return detail.toString();
|
|
}
|
|
|
|
qint64 manifestFileSize(const QJsonObject& file)
|
|
{
|
|
if (file.contains(QStringLiteral("sizeBytes")))
|
|
return file.value(QStringLiteral("sizeBytes")).toVariant().toLongLong();
|
|
if (file.contains(QStringLiteral("size")))
|
|
return file.value(QStringLiteral("size")).toVariant().toLongLong();
|
|
return -1;
|
|
}
|
|
|
|
QString absoluteDownloadUrl(const QString& baseUrl, const QString& downloadUrl)
|
|
{
|
|
const QString trimmed = downloadUrl.trimmed();
|
|
if (trimmed.startsWith(QStringLiteral("http://"), Qt::CaseInsensitive)
|
|
|| trimmed.startsWith(QStringLiteral("https://"), Qt::CaseInsensitive))
|
|
return trimmed;
|
|
if (trimmed.startsWith(QLatin1Char('/')))
|
|
return trimBaseUrl(baseUrl) + trimmed;
|
|
return trimBaseUrl(baseUrl) + QLatin1Char('/') + trimmed;
|
|
}
|
|
}
|
|
|
|
UpdaterLogic::UpdaterLogic(QObject* parent)
|
|
: QObject(parent)
|
|
{
|
|
m_serverAddr = trimBaseUrl(ConfigHelper::instance().getValue("Server", "api_base_url"));
|
|
}
|
|
|
|
void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer,
|
|
int versionId, const QString& releaseId)
|
|
{
|
|
// Manifest 由服务端按版本动态生成,描述目标版本包含哪些文件以及每个文件的 SHA256。
|
|
// Updater 先拿到 Manifest,再请求下载 URL,最后按 Manifest 校验本地文件。
|
|
m_error.clear();
|
|
Q_UNUSED(versionId);
|
|
m_manifestSha256.clear();
|
|
m_manifestSignature.clear();
|
|
m_manifestSignatureAlg.clear();
|
|
m_manifestKeyId.clear();
|
|
m_manifestSigned = false;
|
|
m_fileItems.clear();
|
|
|
|
QUrl url(m_serverAddr + QStringLiteral("/api/v1/client/update/manifest"));
|
|
QUrlQuery query;
|
|
addQueryValue(query, QStringLiteral("releaseId"), releaseId);
|
|
addQueryValue(query, QStringLiteral("productCode"), appId);
|
|
addQueryValue(query, QStringLiteral("version"), targetVer);
|
|
addQueryValue(query, QStringLiteral("clientVersion"), configValue(QStringLiteral("client_protocol"), QStringLiteral("3")));
|
|
addQueryValue(query, QStringLiteral("channel"), channel);
|
|
addQueryValue(query, QStringLiteral("os"), configValue(QStringLiteral("platform")));
|
|
addQueryValue(query, QStringLiteral("architecture"), configValue(QStringLiteral("arch")));
|
|
addQueryValue(query, QStringLiteral("abi"), configValue(QStringLiteral("abi")));
|
|
url.setQuery(query);
|
|
|
|
m_http.getRequest(url.toString(QUrl::FullyEncoded),
|
|
[this, appId, channel, targetVer, releaseId](int code, const QJsonObject& resp)
|
|
{
|
|
qDebug() << "Manifest API returned code:" << code;
|
|
m_manifest = QJsonObject();
|
|
m_manifestText.clear();
|
|
m_manifestSha256.clear();
|
|
m_manifestSignature.clear();
|
|
m_manifestSignatureAlg.clear();
|
|
m_manifestKeyId.clear();
|
|
m_manifestSigned = false;
|
|
|
|
if (code == 200)
|
|
{
|
|
const QJsonObject envelope = resp.value(QStringLiteral("data")).isObject()
|
|
? resp.value(QStringLiteral("data")).toObject()
|
|
: resp;
|
|
QString manifestText = envelope.value(QStringLiteral("manifestText")).toString();
|
|
if (manifestText.isEmpty())
|
|
manifestText = envelope.value(QStringLiteral("manifest_text")).toString();
|
|
const QJsonObject manifest = envelope.value(QStringLiteral("manifest")).toObject();
|
|
if (!manifestText.isEmpty() && !manifest.isEmpty())
|
|
{
|
|
m_manifestText = manifestText;
|
|
m_manifest = manifest;
|
|
m_manifestSha256 = envelope.value(QStringLiteral("manifestSha256")).toString(
|
|
envelope.value(QStringLiteral("manifest_sha256")).toString());
|
|
m_manifestSignature = envelope.value(QStringLiteral("signature")).toString(
|
|
m_manifest.value(QStringLiteral("signature")).toString());
|
|
m_manifestSignatureAlg = envelope.value(QStringLiteral("signatureAlg")).toString(
|
|
envelope.value(QStringLiteral("signature_alg")).toString());
|
|
m_manifestKeyId = envelope.value(QStringLiteral("keyId")).toString(
|
|
envelope.value(QStringLiteral("key_id")).toString());
|
|
m_manifestSigned = envelope.value(QStringLiteral("signed")).toBool(!m_manifestSignature.isEmpty());
|
|
qDebug() << "Received manifest version:" << m_manifest.value("version").toString();
|
|
m_fileItems.clear();
|
|
QJsonArray files = m_manifest.value("files").toArray();
|
|
for (const QJsonValue& fileItem : files)
|
|
{
|
|
QJsonObject fileObj = fileItem.toObject();
|
|
FileDownloadItem fi;
|
|
fi.path = fileObj.value(QStringLiteral("path")).toString();
|
|
fi.sha256 = fileObj.value(QStringLiteral("sha256")).toString();
|
|
fi.size = manifestFileSize(fileObj);
|
|
fi.url = absoluteDownloadUrl(m_serverAddr,
|
|
fileObj.value(QStringLiteral("downloadUrl")).toString());
|
|
m_fileItems.append(fi);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "Manifest response missing fields";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Target version manifest response is incomplete. Stage: download target manifest. Product: %1, channel: %2, version: %3, release id: %4.")
|
|
.arg(appId, channel, targetVer, releaseId);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "Failed to get manifest";
|
|
const QString detail = serverDetailMessage(resp);
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot download target version manifest. Stage: download target manifest. HTTP status: %1. Product: %2, channel: %3, version: %4, release id: %5.%6")
|
|
.arg(QString::number(code), appId, channel, targetVer, releaseId,
|
|
detail.isEmpty() ? QString() : QCoreApplication::translate("UpdaterLogic", "\nServer message: %1").arg(detail));
|
|
}
|
|
emit fetchUrlFinished();
|
|
});
|
|
}
|
|
|
|
bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& signatureBase64, const QString& publicKeyPath) const
|
|
{
|
|
#ifndef HAVE_OPENSSL
|
|
Q_UNUSED(payload);
|
|
Q_UNUSED(signatureBase64);
|
|
Q_UNUSED(publicKeyPath);
|
|
qDebug() << "OpenSSL not available, cannot verify manifest signature";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot verify manifest signature because OpenSSL support is unavailable. Stage: manifest signature verification.");
|
|
return false;
|
|
#else
|
|
QFile keyFile(publicKeyPath);
|
|
if (!keyFile.open(QIODevice::ReadOnly))
|
|
{
|
|
qDebug() << "Cannot open public key file:" << publicKeyPath;
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot open manifest public key. Stage: manifest signature verification. Public key path: %1.")
|
|
.arg(publicKeyPath);
|
|
return false;
|
|
}
|
|
|
|
QByteArray keyData = keyFile.readAll();
|
|
keyFile.close();
|
|
|
|
BIO* bio = BIO_new_mem_buf(keyData.constData(), keyData.size());
|
|
if (!bio)
|
|
{
|
|
qDebug() << "BIO_new_mem_buf failed";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot parse manifest public key buffer. Stage: manifest signature verification. Public key path: %1.")
|
|
.arg(publicKeyPath);
|
|
return false;
|
|
}
|
|
|
|
EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL);
|
|
BIO_free(bio);
|
|
if (!pkey)
|
|
{
|
|
qDebug() << "PEM_read_bio_PUBKEY failed";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Manifest public key is invalid. Stage: manifest signature verification. Public key path: %1.")
|
|
.arg(publicKeyPath);
|
|
return false;
|
|
}
|
|
|
|
QByteArray signature = QByteArray::fromBase64(signatureBase64.toUtf8());
|
|
EVP_MD_CTX* mdctx = EVP_MD_CTX_new();
|
|
if (!mdctx)
|
|
{
|
|
EVP_PKEY_free(pkey);
|
|
qDebug() << "EVP_MD_CTX_new failed";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot create OpenSSL verification context. Stage: manifest signature verification.");
|
|
return false;
|
|
}
|
|
|
|
bool ok = false;
|
|
if (EVP_DigestVerifyInit(mdctx, NULL, EVP_sha256(), NULL, pkey) == 1)
|
|
{
|
|
if (EVP_DigestVerifyUpdate(mdctx, payload.constData(), payload.size()) == 1)
|
|
{
|
|
if (EVP_DigestVerifyFinal(mdctx, reinterpret_cast<const unsigned char*>(signature.constData()), signature.size()) == 1)
|
|
{
|
|
ok = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
EVP_MD_CTX_free(mdctx);
|
|
EVP_PKEY_free(pkey);
|
|
|
|
if (!ok)
|
|
{
|
|
qDebug() << "Manifest signature verification failed";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Manifest RSA signature is invalid. Stage: manifest signature verification. This usually means the manifest was not signed by the matching server private key, the client public key is wrong, or the manifest content was changed.");
|
|
}
|
|
return ok;
|
|
#endif
|
|
}
|
|
|
|
bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const
|
|
{
|
|
// 验签用的是客户端随 SDK 分发的公钥。
|
|
// 只要服务端私钥没有泄漏,客户端就能发现被篡改的 Manifest。
|
|
if (m_manifest.isEmpty() || m_manifestText.isEmpty())
|
|
{
|
|
qDebug() << "No manifest available to verify";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"No manifest is available for signature verification. Stage: manifest signature verification. The target manifest may not have been downloaded successfully.");
|
|
return false;
|
|
}
|
|
if (!m_manifestSha256.isEmpty()) {
|
|
const QString actualSha = QString::fromLatin1(
|
|
QCryptographicHash::hash(m_manifestText.toUtf8(), QCryptographicHash::Sha256).toHex());
|
|
if (actualSha.compare(m_manifestSha256, Qt::CaseInsensitive) != 0) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Manifest SHA-256 does not match the server envelope. Stage: manifest digest verification.\nExpected SHA-256: %1\nActual SHA-256: %2")
|
|
.arg(m_manifestSha256, actualSha);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
const bool requireSignature = configFlag(QStringLiteral("require_manifest_signature"));
|
|
const QString signature = m_manifestSignature.trimmed();
|
|
if (signature.isEmpty() || !m_manifestSigned)
|
|
{
|
|
if (requireSignature) {
|
|
qDebug() << "Manifest signature empty";
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"The manifest does not contain a signature, but require_manifest_signature is enabled. Stage: manifest signature verification.");
|
|
return false;
|
|
}
|
|
qDebug() << "Manifest is unsigned; digest verification passed and require_manifest_signature is disabled.";
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
|
|
QString path = publicKeyPath;
|
|
if (!QFile::exists(path))
|
|
{
|
|
path = QApplication::applicationDirPath() + "/" + publicKeyPath;
|
|
}
|
|
return verifySignature(m_manifestText.toUtf8(), signature, path);
|
|
}
|
|
|
|
bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
|
{
|
|
// 启动后的完整性检查依赖本地 Manifest 缓存。
|
|
// 升级成功后缓存签名清单,下一次离线启动也能校验当前版本文件。
|
|
if (m_manifest.isEmpty()) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot save manifest cache because the target manifest is empty. Stage: save target manifest cache.");
|
|
return false;
|
|
}
|
|
|
|
QDir dir(cacheDir);
|
|
if (!dir.exists() && !dir.mkpath(".")) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot create manifest cache directory. Stage: save target manifest cache. Directory: %1.")
|
|
.arg(cacheDir);
|
|
return false;
|
|
}
|
|
|
|
QString version = m_manifest.value("version").toString();
|
|
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
|
QFile file(filePath);
|
|
if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot write manifest cache file. Stage: save target manifest cache. File: %1. Error: %2.")
|
|
.arg(filePath, file.errorString());
|
|
return false;
|
|
}
|
|
|
|
QJsonObject wrapper;
|
|
wrapper["manifest"] = m_manifest;
|
|
wrapper["manifestText"] = m_manifestText;
|
|
wrapper["manifest_text"] = m_manifestText;
|
|
wrapper["manifestSha256"] = m_manifestSha256;
|
|
wrapper["signature"] = m_manifestSignature;
|
|
wrapper["signatureAlg"] = m_manifestSignatureAlg;
|
|
wrapper["keyId"] = m_manifestKeyId;
|
|
wrapper["signed"] = m_manifestSigned;
|
|
|
|
QJsonDocument doc(wrapper);
|
|
file.write(doc.toJson(QJsonDocument::Indented));
|
|
file.close();
|
|
qDebug() << "Manifest cached to" << filePath;
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
|
|
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
|
|
{
|
|
m_error.clear();
|
|
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
|
QFile file(filePath);
|
|
if (!file.exists() || !file.open(QIODevice::ReadOnly)) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot read cached signed manifest. Stage: read local manifest cache. Version: %1. File: %2. This cache is created after a version is installed successfully.")
|
|
.arg(version, filePath);
|
|
return false;
|
|
}
|
|
|
|
QByteArray raw = file.readAll();
|
|
file.close();
|
|
QJsonDocument doc = QJsonDocument::fromJson(raw);
|
|
if (!doc.isObject()) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cached signed manifest is not valid JSON. Stage: read local manifest cache. Version: %1. File: %2.")
|
|
.arg(version, filePath);
|
|
return false;
|
|
}
|
|
|
|
QJsonObject wrapper = doc.object();
|
|
if (!wrapper.contains("manifest")
|
|
|| (!wrapper.contains("manifestText") && !wrapper.contains("manifest_text"))) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cached signed manifest is incomplete. Stage: read local manifest cache. Version: %1. File: %2.")
|
|
.arg(version, filePath);
|
|
return false;
|
|
}
|
|
|
|
m_manifest = wrapper["manifest"].toObject();
|
|
m_manifestText = wrapper.value(QStringLiteral("manifestText")).toString();
|
|
if (m_manifestText.isEmpty())
|
|
m_manifestText = wrapper.value(QStringLiteral("manifest_text")).toString();
|
|
m_manifestSha256 = wrapper.value(QStringLiteral("manifestSha256")).toString(
|
|
wrapper.value(QStringLiteral("manifest_sha256")).toString());
|
|
m_manifestSignature = wrapper.value(QStringLiteral("signature")).toString(
|
|
m_manifest.value(QStringLiteral("signature")).toString());
|
|
m_manifestSignatureAlg = wrapper.value(QStringLiteral("signatureAlg")).toString(
|
|
wrapper.value(QStringLiteral("signature_alg")).toString());
|
|
m_manifestKeyId = wrapper.value(QStringLiteral("keyId")).toString(
|
|
wrapper.value(QStringLiteral("key_id")).toString());
|
|
m_manifestSigned = wrapper.value(QStringLiteral("signed")).toBool(!m_manifestSignature.isEmpty());
|
|
qDebug() << "Loaded cached manifest" << version;
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
|
|
QByteArray UpdaterLogic::canonicalManifestBytes(const QJsonObject& manifest) const
|
|
{
|
|
QJsonObject copy = manifest;
|
|
copy.remove("signature");
|
|
|
|
auto sortObject = [&](auto&& self, const QJsonObject& obj) -> QJsonObject {
|
|
QStringList keys = obj.keys();
|
|
std::sort(keys.begin(), keys.end(), std::less<QString>());
|
|
QJsonObject sorted;
|
|
for (const auto& key : keys)
|
|
{
|
|
QJsonValue value = obj.value(key);
|
|
if (value.isObject())
|
|
sorted.insert(key, self(self, value.toObject()));
|
|
else if (value.isArray())
|
|
{
|
|
QJsonArray sortedArray;
|
|
for (const auto& element : value.toArray())
|
|
{
|
|
if (element.isObject())
|
|
sortedArray.append(self(self, element.toObject()));
|
|
else
|
|
sortedArray.append(element);
|
|
}
|
|
sorted.insert(key, sortedArray);
|
|
}
|
|
else
|
|
{
|
|
sorted.insert(key, value);
|
|
}
|
|
}
|
|
return sorted;
|
|
};
|
|
|
|
QJsonObject sorted = sortObject(sortObject, copy);
|
|
QJsonDocument doc(sorted);
|
|
return doc.toJson(QJsonDocument::Compact);
|
|
}
|
|
|
|
|
|
QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest) const
|
|
{
|
|
QSet<QString> newPaths;
|
|
for (const QJsonValue& value : m_manifest.value("files").toArray()) {
|
|
const QString path = QDir::fromNativeSeparators(value.toObject().value("path").toString());
|
|
if (isSafeRelativePath(path)) newPaths.insert(path.toCaseFolded());
|
|
}
|
|
|
|
QStringList obsolete;
|
|
QSet<QString> seen;
|
|
for (const QJsonValue& value : oldManifest.value("files").toArray()) {
|
|
const QJsonObject item = value.toObject();
|
|
if (item.contains(QStringLiteral("required"))
|
|
&& !item.value(QStringLiteral("required")).toBool(true)) {
|
|
continue;
|
|
}
|
|
const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
|
const QString folded = path.toCaseFolded();
|
|
if (!isSafeRelativePath(path) || isRuntimeProtectedPath(path)
|
|
|| newPaths.contains(folded) || seen.contains(folded))
|
|
continue;
|
|
seen.insert(folded);
|
|
obsolete.append(path);
|
|
}
|
|
obsolete.sort(Qt::CaseInsensitive);
|
|
return obsolete;
|
|
}
|
|
|
|
bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const
|
|
{
|
|
m_error.clear();
|
|
const QString stage = installedDir.isEmpty()
|
|
? QCoreApplication::translate("UpdaterLogic", "installed version verification")
|
|
: QCoreApplication::translate("UpdaterLogic", "downloaded/staged file verification");
|
|
const QString version = m_manifest.value(QStringLiteral("version")).toString();
|
|
if (m_manifest.isEmpty()) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"No manifest is available. Stage: %1. The updater cannot know which files and hashes should be verified.")
|
|
.arg(stage);
|
|
return false;
|
|
}
|
|
|
|
const QJsonArray files = m_manifest.value("files").toArray();
|
|
for (const auto& item : files)
|
|
{
|
|
const QJsonObject fileObject = item.toObject();
|
|
const QString path = QDir::fromNativeSeparators(fileObject.value("path").toString());
|
|
const QString expectedSha = fileObject.value("sha256").toString();
|
|
if (!isSafeRelativePath(path)) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Manifest contains an unsafe file path. Stage: %1. Version: %2. Path: %3.")
|
|
.arg(stage, version, path);
|
|
return false;
|
|
}
|
|
if (isRuntimeProtectedPath(path)) {
|
|
qDebug() << "Runtime-protected manifest entry ignored:" << path;
|
|
continue;
|
|
}
|
|
|
|
QString fullPath = QDir(stagingDir).filePath(path);
|
|
if (!QFile::exists(fullPath) && !installedDir.isEmpty())
|
|
fullPath = QDir(installedDir).filePath(path);
|
|
|
|
if (!QFile::exists(fullPath))
|
|
{
|
|
qDebug() << "Manifest file missing from staging and installation:" << path;
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"A required file is missing. Stage: %1. Version: %2. Manifest path: %3. Checked path: %4. If this is a downloaded update, the file was not downloaded or staged correctly; if this is startup verification, the installed file may have been deleted.")
|
|
.arg(stage, version, path, fullPath);
|
|
return false;
|
|
}
|
|
const qint64 expectedSize = manifestFileSize(fileObject);
|
|
if (expectedSize >= 0 && QFileInfo(fullPath).size() != expectedSize)
|
|
{
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"File size does not match the signed manifest. Stage: %1. Version: %2. Manifest path: %3. Local path: %4.\nExpected size: %5 bytes\nActual size: %6 bytes")
|
|
.arg(stage, version, path, fullPath,
|
|
QString::number(expectedSize), QString::number(QFileInfo(fullPath).size()));
|
|
return false;
|
|
}
|
|
const QString actualSha = calcLocalFileSha256(fullPath);
|
|
if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0)
|
|
{
|
|
qDebug() << "File hash mismatch:" << path << actualSha << expectedSha;
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"File SHA-256 does not match the signed manifest. Stage: %1. Version: %2. Manifest path: %3. Local path: %4.\nExpected SHA-256: %5\nActual SHA-256: %6\nIf this happens during download, clear the update cache and retry. If this happens during startup or after installation, the local file differs from the published version.")
|
|
.arg(stage, version, path, fullPath, expectedSha, actualSha.isEmpty() ? QCoreApplication::translate("UpdaterLogic", "<cannot read file>") : actualSha);
|
|
return false;
|
|
}
|
|
}
|
|
qDebug() << "Full manifest validation passed using staging plus installed files";
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
|
|
bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString& stagingDir)
|
|
{
|
|
m_offlineError.clear();
|
|
QFile package(packagePath);
|
|
if (!package.open(QIODevice::ReadOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot open the offline update package"); return false; }
|
|
if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package format identifier is invalid"); return false; }
|
|
const QByteArray lengthBytes = package.read(8);
|
|
if (lengthBytes.size() != 8) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header is incomplete"); return false; }
|
|
quint64 headerSize = 0;
|
|
for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast<unsigned char>(lengthBytes[i])) << (i * 8);
|
|
if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header length is invalid"); return false; }
|
|
QJsonParseError error;
|
|
const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error);
|
|
if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package header JSON is invalid"); return false; }
|
|
const QJsonObject wrapper = wrapperDoc.object();
|
|
const QByteArray packageText = wrapper.value("package_text").toString().toUtf8();
|
|
const QByteArray manifestText = wrapper.value("manifest_text").toString().toUtf8();
|
|
const QString publicKey = QApplication::applicationDirPath() + "/config/manifest_public_key.pem";
|
|
if (!verifySignature(packageText, wrapper.value("package_signature").toString(), publicKey)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package RSA signature is invalid"); return false; }
|
|
const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object();
|
|
if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package signature metadata is invalid"); return false; }
|
|
if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The manifest digest does not match the package signature"); return false; }
|
|
QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object();
|
|
if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest is invalid"); return false; }
|
|
manifest.insert("signature", wrapper.value("manifest_signature").toString());
|
|
m_manifest = manifest; m_manifestText = QString::fromUtf8(manifestText); m_fileItems.clear();
|
|
m_manifestSha256 = packageMeta.value("manifest_sha256").toString();
|
|
m_manifestSignature = wrapper.value("manifest_signature").toString();
|
|
m_manifestSignatureAlg = wrapper.value("signature_alg").toString("RSA-SHA256");
|
|
m_manifestKeyId = wrapper.value("key_id").toString();
|
|
m_manifestSigned = !m_manifestSignature.isEmpty();
|
|
const QString manifestProduct = manifest.value("productCode").toString(manifest.value("app_id").toString());
|
|
const QString packageProduct = packageMeta.value("productCode").toString(packageMeta.value("app_id").toString());
|
|
if (manifestProduct != packageProduct || manifest.value("channel") != packageMeta.value("channel") || manifest.value("version") != packageMeta.value("version")) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Package information does not match manifest identity"); return false; }
|
|
if (!verifyManifestSignature()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline manifest RSA signature is invalid"); return false; }
|
|
const qint64 payloadStart = 16 + qint64(headerSize);
|
|
const QJsonArray entries = packageMeta.value("files").toArray();
|
|
for (const QJsonValue& value : entries) {
|
|
const QJsonObject item = value.toObject(); const QString path = QDir::fromNativeSeparators(item.value("path").toString());
|
|
const qint64 offset = item.value("offset").toVariant().toLongLong(); const qint64 size = item.value("size").toVariant().toLongLong();
|
|
if (!isSafeRelativePath(path) || offset < 0 || size < 0 || payloadStart + offset + size > package.size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package contains an unsafe path or out-of-range data: %1").arg(path); return false; }
|
|
FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi);
|
|
if (isRuntimeProtectedPath(path)) continue;
|
|
if (stagingDir.isEmpty()) continue;
|
|
const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot prepare offline file: %1").arg(path); return false; }
|
|
QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "Cannot create staged file: %1").arg(path); return false; }
|
|
QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size;
|
|
while (remaining > 0) { const QByteArray block = package.read(qMin<qint64>(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Failed to read offline file: %1").arg(path); return false; } hash.addData(block); remaining -= block.size(); }
|
|
if (QString::fromLatin1(hash.result().toHex()).compare(fi.sha256, Qt::CaseInsensitive) != 0 || !output.commit()) { output.cancelWriting(); m_offlineError = QCoreApplication::translate("UpdaterLogic", "Offline file hash verification or write failed: %1").arg(path); return false; }
|
|
}
|
|
if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = QCoreApplication::translate("UpdaterLogic", "The offline package file count does not match the manifest"); return false; }
|
|
return true;
|
|
}
|
|
|
|
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
|
{
|
|
Q_UNUSED(appId);
|
|
Q_UNUSED(channel);
|
|
Q_UNUSED(targetVer);
|
|
Q_UNUSED(versionId);
|
|
m_error.clear();
|
|
if (m_fileItems.isEmpty()) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"The manifest does not contain any downloadable package URL. Stage: prepare authorized downloads.");
|
|
} else {
|
|
qDebug() << "Authorized download URLs were loaded from the SimCAE Hub manifest.";
|
|
}
|
|
emit fetchUrlFinished();
|
|
}
|
|
|
|
|
|
QString UpdaterLogic::calcLocalFileSha256(const QString& filePath) const
|
|
{
|
|
QFile f(filePath);
|
|
if (!f.open(QIODevice::ReadOnly))
|
|
return "";
|
|
QCryptographicHash hash(QCryptographicHash::Sha256);
|
|
while (!f.atEnd())
|
|
{
|
|
QByteArray block = f.read(4096);
|
|
hash.addData(block);
|
|
}
|
|
f.close();
|
|
return hash.result().toHex();
|
|
}
|
|
|
|
bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePath,
|
|
const QString& expectSha256, qint64 expectedSize,
|
|
const QString& resumePartPath)
|
|
{
|
|
const QString partPath = resumePartPath.isEmpty() ? savePath + ".part" : resumePartPath;
|
|
const QString displayPath = m_currentDownloadPath.isEmpty() ? savePath : m_currentDownloadPath;
|
|
if (!QDir().mkpath(QFileInfo(partPath).path())) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot create partial download directory. Stage: download file. File: %1. Partial file: %2.")
|
|
.arg(displayPath, partPath);
|
|
return false;
|
|
}
|
|
if (QFile::exists(savePath)
|
|
&& calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0) {
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
QFile::remove(savePath);
|
|
|
|
for (int attempt = 0; attempt < 4; ++attempt)
|
|
{
|
|
qint64 existingSize = QFileInfo(partPath).size();
|
|
if (expectedSize >= 0 && existingSize > expectedSize)
|
|
{
|
|
QFile::remove(partPath);
|
|
existingSize = 0;
|
|
}
|
|
if (expectedSize >= 0 && existingSize == expectedSize && existingSize > 0)
|
|
{
|
|
if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
|
{
|
|
QFile::remove(savePath);
|
|
if (QFile::rename(partPath, savePath)) {
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Downloaded file passed SHA-256 verification, but cannot move it into the staging directory. Stage: download file. File: %1. From: %2. To: %3.")
|
|
.arg(displayPath, partPath, savePath);
|
|
return false;
|
|
}
|
|
const QString actualSha = calcLocalFileSha256(partPath);
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cached partial file has the expected size but wrong SHA-256. Stage: resume download. File: %1.\nExpected SHA-256: %2\nActual SHA-256: %3\nThe partial cache will be deleted and downloaded again.")
|
|
.arg(displayPath, expectSha256, actualSha);
|
|
QFile::remove(partPath);
|
|
existingSize = 0;
|
|
}
|
|
|
|
QFile partFile(partPath);
|
|
const QIODevice::OpenMode mode = QIODevice::WriteOnly
|
|
| (existingSize > 0 ? QIODevice::Append : QIODevice::Truncate);
|
|
if (!partFile.open(mode))
|
|
{
|
|
qDebug() << "Cannot open partial download:" << partPath;
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot open partial download file. Stage: download file. File: %1. Partial file: %2. Error: %3.")
|
|
.arg(displayPath, partPath, partFile.errorString());
|
|
return false;
|
|
}
|
|
|
|
QNetworkAccessManager manager;
|
|
manager.setProxy(QNetworkProxy::NoProxy);
|
|
QNetworkRequest request{QUrl(url)};
|
|
request.setTransferTimeout(60000);
|
|
const QString clientToken = configValue(QStringLiteral("client_token"));
|
|
if (!clientToken.isEmpty())
|
|
request.setRawHeader("X-Client-Token", clientToken.toUtf8());
|
|
if (existingSize > 0)
|
|
request.setRawHeader("Range", QByteArray("bytes=") + QByteArray::number(existingSize) + "-");
|
|
|
|
QNetworkReply* reply = manager.get(request);
|
|
QEventLoop loop;
|
|
bool writeOk = true;
|
|
QObject::connect(reply, &QNetworkReply::readyRead, [&]() {
|
|
const QByteArray data = reply->readAll();
|
|
if (!data.isEmpty()) {
|
|
if (partFile.write(data) != data.size()) writeOk = false;
|
|
m_sessionDownloadedBytes += data.size();
|
|
const qint64 received = qMin(m_downloadTotalBytes,
|
|
m_downloadCompletedBytes + partFile.size());
|
|
const double speed = m_downloadTimer.elapsed() > 0
|
|
? (m_sessionDownloadedBytes * 1000.0 / m_downloadTimer.elapsed()) : 0.0;
|
|
emit downloadProgress(received, m_downloadTotalBytes, m_currentDownloadPath, speed);
|
|
}
|
|
});
|
|
QObject::connect(reply, &QNetworkReply::finished, &loop, &QEventLoop::quit);
|
|
loop.exec();
|
|
const QByteArray remaining = reply->readAll();
|
|
if (!remaining.isEmpty() && partFile.write(remaining) != remaining.size()) writeOk = false;
|
|
partFile.flush();
|
|
partFile.close();
|
|
|
|
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
|
const bool networkOk = reply->error() == QNetworkReply::NoError;
|
|
const QString networkError = reply->errorString();
|
|
reply->deleteLater();
|
|
|
|
if (existingSize > 0 && httpStatus == 200)
|
|
{
|
|
// The server ignored Range; the current file is "old fragment + full response" and must be redownloaded safely.
|
|
qDebug() << "Server ignored Range; restart full download:" << savePath;
|
|
QFile::remove(partPath);
|
|
}
|
|
else if (networkOk && writeOk && (httpStatus == 200 || httpStatus == 206))
|
|
{
|
|
const qint64 actualSize = QFileInfo(partPath).size();
|
|
if ((expectedSize < 0 || actualSize == expectedSize)
|
|
&& calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
|
{
|
|
QFile::remove(savePath);
|
|
if (QFile::rename(partPath, savePath))
|
|
{
|
|
qDebug() << "Download completed/resumed & sha pass:" << savePath;
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Downloaded file passed SHA-256 verification, but cannot move it into the staging directory. Stage: download file. File: %1. From: %2. To: %3.")
|
|
.arg(displayPath, partPath, savePath);
|
|
return false;
|
|
}
|
|
else if (expectedSize >= 0 && actualSize >= expectedSize)
|
|
{
|
|
qDebug() << "Completed partial file has invalid size or SHA; restart:" << savePath;
|
|
const QString actualSha = calcLocalFileSha256(partPath);
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Downloaded file does not match the signed manifest. Stage: download file. File: %1. HTTP status: %2.\nExpected size: %3 bytes\nActual size: %4 bytes\nExpected SHA-256: %5\nActual SHA-256: %6\nThe partial cache will be deleted and downloaded again.")
|
|
.arg(displayPath, QString::number(httpStatus), QString::number(expectedSize), QString::number(actualSize),
|
|
expectSha256, actualSha.isEmpty() ? QCoreApplication::translate("UpdaterLogic", "<cannot read file>") : actualSha);
|
|
QFile::remove(partPath);
|
|
}
|
|
else {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Downloaded file is incomplete. Stage: download file. File: %1. HTTP status: %2. Expected size: %3 bytes, current size: %4 bytes.")
|
|
.arg(displayPath, QString::number(httpStatus), QString::number(expectedSize), QString::number(actualSize));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
qDebug() << "Download attempt failed; partial file retained:" << attempt + 1
|
|
<< savePath << httpStatus << networkError << "bytes" << QFileInfo(partPath).size();
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Download request failed. Stage: download file. File: %1. Attempt: %2/4. HTTP status: %3. Network error: %4. Partial file: %5.")
|
|
.arg(displayPath, QString::number(attempt + 1), QString::number(httpStatus), networkError, partPath);
|
|
}
|
|
|
|
if (attempt < 3)
|
|
{
|
|
const unsigned long delayMs = static_cast<unsigned long>(500 * (1 << attempt));
|
|
QElapsedTimer delay;
|
|
delay.start();
|
|
while (delay.elapsed() < static_cast<qint64>(delayMs))
|
|
{
|
|
QApplication::processEvents();
|
|
QThread::msleep(50);
|
|
}
|
|
}
|
|
}
|
|
if (m_error.isEmpty()) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"File download failed after retries. Stage: download file. File: %1.")
|
|
.arg(displayPath);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool UpdaterLogic::isRuntimeProtectedPath(const QString& path) const
|
|
{
|
|
return UpdatePathPolicy::isFullUpdateProtectedPath(
|
|
path, ConfigHelper::instance().runtimeRelativePath());
|
|
}
|
|
|
|
bool UpdaterLogic::isSafeRelativePath(const QString& path) const
|
|
{
|
|
return UpdatePathPolicy::isSafeRelativePath(path);
|
|
}
|
|
|
|
qint64 UpdaterLogic::estimateAdditionalDiskBytes(const QString& targetDir,
|
|
const QStringList& obsoletePaths) const
|
|
{
|
|
qint64 required = 0;
|
|
const QString cacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
|
|
for (const auto& item : m_fileItems) {
|
|
const QString relativePath = QDir::fromNativeSeparators(item.path);
|
|
if (isRuntimeProtectedPath(relativePath)) continue;
|
|
const QString installed = QDir(targetDir).filePath(relativePath);
|
|
if (QFile::exists(installed)
|
|
&& calcLocalFileSha256(installed).compare(item.sha256, Qt::CaseInsensitive) == 0)
|
|
continue;
|
|
|
|
const qint64 partialSize = QFileInfo(QDir(cacheDir).filePath(
|
|
item.sha256.toLower() + ".part")).size();
|
|
required += qMax<qint64>(0, item.size - qMin(item.size, partialSize));
|
|
if (QFile::exists(installed)) required += QFileInfo(installed).size();
|
|
}
|
|
for (const QString& path : obsoletePaths) {
|
|
const QString installed = QDir(targetDir).filePath(path);
|
|
if (QFile::exists(installed)) required += QFileInfo(installed).size();
|
|
}
|
|
const qint64 reserve = qMax<qint64>(128LL * 1024 * 1024, required / 10);
|
|
return required + reserve;
|
|
}
|
|
|
|
bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir)
|
|
{
|
|
m_error.clear();
|
|
if (m_fileItems.isEmpty())
|
|
{
|
|
m_downloadAllOk = false;
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"The target version manifest contains no downloadable files. Stage: prepare downloads.");
|
|
return false;
|
|
}
|
|
|
|
QDir stagingDir(tempDir);
|
|
if (!FileHelper::createDir(tempDir)) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot create update staging directory. Stage: prepare downloads. Directory: %1.")
|
|
.arg(tempDir);
|
|
return false;
|
|
}
|
|
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
|
|
if (!FileHelper::createDir(resumeCacheDir)) {
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot create download cache directory. Stage: prepare downloads. Directory: %1.")
|
|
.arg(resumeCacheDir);
|
|
return false;
|
|
}
|
|
QSet<QString> activePartialNames;
|
|
for (const auto& item : m_fileItems)
|
|
activePartialNames.insert(item.sha256.toLower() + ".part");
|
|
QDir cacheDir(resumeCacheDir);
|
|
for (const QString& fileName : cacheDir.entryList(QStringList() << "*.part", QDir::Files)) {
|
|
if (!activePartialNames.contains(fileName.toLower()))
|
|
cacheDir.remove(fileName);
|
|
}
|
|
|
|
m_downloadTotalBytes = 0;
|
|
m_downloadCompletedBytes = 0;
|
|
m_sessionDownloadedBytes = 0;
|
|
for (const auto& item : m_fileItems) {
|
|
const QString itemPath = QDir::fromNativeSeparators(item.path);
|
|
if (isRuntimeProtectedPath(itemPath)) continue;
|
|
const QString installed = QDir(targetDir).filePath(itemPath);
|
|
if (!QFile::exists(installed)
|
|
|| calcLocalFileSha256(installed).compare(item.sha256, Qt::CaseInsensitive) != 0)
|
|
m_downloadTotalBytes += item.size;
|
|
}
|
|
m_downloadTimer.restart();
|
|
emit downloadProgress(0, m_downloadTotalBytes, QString(), 0.0);
|
|
|
|
for (const auto& fi : m_fileItems)
|
|
{
|
|
if (!isSafeRelativePath(fi.path))
|
|
{
|
|
qDebug() << "Unsafe relative path in manifest:" << fi.path;
|
|
m_downloadAllOk = false;
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Manifest contains an unsafe file path. Stage: prepare file download. Path: %1.")
|
|
.arg(fi.path);
|
|
return false;
|
|
}
|
|
|
|
const QString relativePath = QDir::fromNativeSeparators(fi.path);
|
|
if (isRuntimeProtectedPath(relativePath)) {
|
|
qDebug() << "Runtime-protected file skipped:" << relativePath;
|
|
continue;
|
|
}
|
|
const QString installedFile = QDir(targetDir).filePath(relativePath);
|
|
if (QFile::exists(installedFile)
|
|
&& calcLocalFileSha256(installedFile).compare(fi.sha256, Qt::CaseInsensitive) == 0)
|
|
{
|
|
qDebug() << "Unchanged file, skip download:" << relativePath;
|
|
continue;
|
|
}
|
|
|
|
const QString tempFile = QDir(tempDir).filePath(relativePath);
|
|
const QString parentDir = QFileInfo(tempFile).path();
|
|
if (!QDir().mkpath(parentDir))
|
|
{
|
|
qDebug() << "Cannot create staging subdirectory:" << parentDir;
|
|
m_downloadAllOk = false;
|
|
m_error = QCoreApplication::translate("UpdaterLogic",
|
|
"Cannot create staging subdirectory. Stage: prepare file download. File: %1. Directory: %2.")
|
|
.arg(relativePath, parentDir);
|
|
return false;
|
|
}
|
|
|
|
const QString resumePartPath = QDir(resumeCacheDir).filePath(fi.sha256.toLower() + ".part");
|
|
m_currentDownloadPath = relativePath;
|
|
const qint64 resumedBytes = qMin(fi.size, QFileInfo(resumePartPath).size());
|
|
const double speed = m_downloadTimer.elapsed() > 0
|
|
? (m_sessionDownloadedBytes * 1000.0 / m_downloadTimer.elapsed()) : 0.0;
|
|
emit downloadProgress(m_downloadCompletedBytes + resumedBytes,
|
|
m_downloadTotalBytes, m_currentDownloadPath, speed);
|
|
if (!downloadSingleFile(fi.url, tempFile, fi.sha256, fi.size, resumePartPath))
|
|
{
|
|
m_downloadAllOk = false;
|
|
return false;
|
|
}
|
|
m_downloadCompletedBytes += fi.size;
|
|
emit downloadProgress(m_downloadCompletedBytes, m_downloadTotalBytes,
|
|
m_currentDownloadPath, speed);
|
|
}
|
|
|
|
m_downloadAllOk = true;
|
|
m_error.clear();
|
|
return true;
|
|
}
|
|
|
|
void UpdaterLogic::reportDownloadResult(const QString& appId, const QString& channel,
|
|
const QString& version, bool success)
|
|
{
|
|
Q_UNUSED(appId);
|
|
Q_UNUSED(channel);
|
|
Q_UNUSED(version);
|
|
Q_UNUSED(success);
|
|
qDebug() << "Download result report is not part of the current SimCAE Hub API; skipped.";
|
|
}
|
|
|
|
void UpdaterLogic::reportResult(const QString& deviceId,
|
|
const QString& fromVer,
|
|
const QString& toVer,
|
|
bool success)
|
|
{
|
|
Q_UNUSED(deviceId);
|
|
Q_UNUSED(fromVer);
|
|
Q_UNUSED(toVer);
|
|
Q_UNUSED(success);
|
|
qDebug() << "Update result report is not part of the current SimCAE Hub API; skipped.";
|
|
}
|
|
|
|
QList<FileDownloadItem> UpdaterLogic::getFileList() const
|
|
{
|
|
return m_fileItems;
|
|
}
|