feat(client): 迁移Hub更新客户端实现

This commit is contained in:
2026-09-08 17:08:58 +08:00
parent 0c500024e6
commit 8dfef45bc4
34 changed files with 2032 additions and 2903 deletions
+277 -218
View File
@@ -13,10 +13,13 @@
#include <QCryptographicHash>
#include <QDir>
#include <QJsonDocument>
#include <QSaveFile>
#include <QApplication>
#include <algorithm>
#include "ConfigHelper.h"
#include <QSaveFile>
#include <QApplication>
#include <QUrl>
#include <QUrlQuery>
#include <algorithm>
#include "ConfigHelper.h"
#include "UpdatePathPolicy.h"
#ifdef HAVE_OPENSSL
#include <openssl/pem.h>
@@ -25,15 +28,44 @@
#include <openssl/err.h>
#endif
UpdaterLogic::UpdaterLogic(QObject* parent)
: QObject(parent)
namespace {
QString trimBaseUrl(QString value)
{
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url");
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);
}
namespace {
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();
@@ -44,52 +76,115 @@ QString serverDetailMessage(const QJsonObject& response)
}
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;
}
void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
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();
QString url = m_serverAddr + "/api/v1/update/manifest";
QJsonObject body;
body["app_id"] = appId;
body["channel"] = channel;
body["version"] = targetVer;
body["version_id"] = versionId;
m_http.postRequest(url, body, [this, appId, channel, targetVer, versionId](int code, const QJsonObject& resp)
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();
if (code == 200)
{
if (resp.contains("manifest_text") && resp.contains("manifest"))
{
m_manifestText = resp["manifest_text"].toString();
m_manifest = resp["manifest"].toObject();
qDebug() << "Received manifest version:" << m_manifest.value("version").toString();
m_fileItems.clear();
QJsonArray files = m_manifest.value("files").toArray();
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("path").toString();
fi.sha256 = fileObj.value("sha256").toString();
fi.size = fileObj.value("size").toVariant().toLongLong();
fi.url = m_serverAddr + "/api/v1/update/file/" + fi.path; // placeholder, actual download URL uses download-url or signed object URL
m_fileItems.append(fi);
}
}
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. App: %1, channel: %2, version: %3, version id: %4.")
.arg(appId, channel, targetVer, QString::number(versionId));
"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
@@ -97,8 +192,8 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
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. App: %2, channel: %3, version: %4, version id: %5.%6")
.arg(QString::number(code), appId, channel, targetVer, QString::number(versionId),
"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();
@@ -197,13 +292,30 @@ bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const
"No manifest is available for signature verification. Stage: manifest signature verification. The target manifest may not have been downloaded successfully.");
return false;
}
QString signature = m_manifest.value("signature").toString();
if (signature.isEmpty())
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)
{
qDebug() << "Manifest signature empty";
m_error = QCoreApplication::translate("UpdaterLogic",
"The manifest does not contain a signature. Stage: manifest signature verification.");
return false;
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;
@@ -242,9 +354,15 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
return false;
}
QJsonObject wrapper;
wrapper["manifest"] = m_manifest;
wrapper["manifest_text"] = m_manifestText;
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));
@@ -277,15 +395,27 @@ bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& ver
}
QJsonObject wrapper = doc.object();
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text")) {
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["manifest_text"].toString();
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;
@@ -339,40 +469,19 @@ QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest
if (isSafeRelativePath(path)) newPaths.insert(path.toCaseFolded());
}
QSet<QString> protectedPaths{
QStringLiteral("bootstrap"),
QStringLiteral("bootstrap.exe"),
QStringLiteral("launcher"),
QStringLiteral("launcher.exe"),
QStringLiteral("updater"),
QStringLiteral("updater.exe"),
QStringLiteral("client.ini"),
QStringLiteral("config/app_config.json"),
QStringLiteral("config/local_state.json"),
QStringLiteral("config/client_identity.dat"),
QStringLiteral("config/version_policy.dat")
};
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
if (!runtimePrefix.isEmpty()) {
const QStringList runtimeProtected{
QStringLiteral("bootstrap"), QStringLiteral("bootstrap.exe"),
QStringLiteral("launcher"), QStringLiteral("launcher.exe"),
QStringLiteral("updater"), QStringLiteral("updater.exe"),
QStringLiteral("client.ini"), QStringLiteral("config/app_config.json"),
QStringLiteral("config/local_state.json"), QStringLiteral("config/client_identity.dat"),
QStringLiteral("config/version_policy.dat")
};
for (const QString& path : runtimeProtected)
protectedPaths.insert(runtimePrefix + "/" + path);
}
QStringList obsolete;
QSet<QString> seen;
for (const QJsonValue& value : oldManifest.value("files").toArray()) {
const QString path = QDir::fromNativeSeparators(value.toObject().value("path").toString());
const QString folded = path.toCaseFolded();
if (!isSafeRelativePath(path) || protectedPaths.contains(folded)
|| newPaths.contains(folded) || seen.contains(folded))
continue;
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);
}
@@ -423,6 +532,15 @@ bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString&
.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)
{
@@ -462,17 +580,25 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
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();
if (manifest.value("app_id") != packageMeta.value("app_id") || 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; }
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();
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; }
@@ -486,48 +612,18 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
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();
QString url = m_serverAddr + "/api/v1/update/download-url";
QJsonObject body;
body["app_id"] = appId;
body["channel"] = channel;
body["version"] = targetVer;
body["version_id"] = versionId;
QJsonArray emptyFiles;
body["files"] = emptyFiles;
m_http.postRequest(url, body, [this, appId, channel, targetVer, versionId](int code, const QJsonObject& resp)
{
qDebug() << "Download URL API returned code:" << code;
m_fileItems.clear();
if (code == 200)
{
QJsonArray fileArr = resp["files"].toArray();
for (auto item : fileArr)
{
QJsonObject obj = item.toObject();
FileDownloadItem fi;
fi.path = obj["path"].toString();
fi.url = obj["url"].toString();
fi.sha256 = obj["sha256"].toString();
fi.size = obj["size"].toVariant().toLongLong();
m_fileItems.append(fi);
qDebug() << "File info:" << fi.path << fi.url << fi.sha256;
}
}
else
{
qDebug() << "Failed to get download URL";
const QString detail = serverDetailMessage(resp);
m_error = QCoreApplication::translate("UpdaterLogic",
"Cannot get secure download URLs. Stage: request download URLs. HTTP status: %1. App: %2, channel: %3, version: %4, version id: %5.%6")
.arg(QString::number(code), appId, channel, targetVer, QString::number(versionId),
detail.isEmpty() ? QString() : QCoreApplication::translate("UpdaterLogic", "\nServer message: %1").arg(detail));
}
emit fetchUrlFinished();
});
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();
}
@@ -607,12 +703,15 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
return false;
}
QNetworkAccessManager manager;
manager.setProxy(QNetworkProxy::NoProxy);
QNetworkRequest request(url);
request.setTransferTimeout(60000);
if (existingSize > 0)
request.setRawHeader("Range", QByteArray("bytes=") + QByteArray::number(existingSize) + "-");
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;
@@ -636,12 +735,12 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
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)
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;
@@ -710,41 +809,16 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
return false;
}
bool UpdaterLogic::isRuntimeProtectedPath(const QString& path) const
{
const QString normalized = QDir::fromNativeSeparators(path).toCaseFolded();
QSet<QString> protectedPaths{
QStringLiteral("bootstrap"),
QStringLiteral("bootstrap.exe"),
QStringLiteral("client.ini"),
QStringLiteral("config/app_config.json"),
QStringLiteral("config/local_state.json"),
QStringLiteral("config/client_identity.dat"),
QStringLiteral("config/version_policy.dat")
};
const QString runtimePrefix = ConfigHelper::instance().runtimeRelativePath().toCaseFolded();
if (!runtimePrefix.isEmpty()) {
const QStringList runtimeProtected{
QStringLiteral("bootstrap"), QStringLiteral("bootstrap.exe"), QStringLiteral("client.ini"),
QStringLiteral("config/app_config.json"), QStringLiteral("config/local_state.json"),
QStringLiteral("config/client_identity.dat"), QStringLiteral("config/version_policy.dat")
};
for (const QString& protectedPath : runtimeProtected)
protectedPaths.insert(runtimePrefix + "/" + protectedPath);
}
return protectedPaths.contains(normalized);
}
bool UpdaterLogic::isSafeRelativePath(const QString& path) const
{
const QString normalized = QDir::fromNativeSeparators(path);
const QString clean = QDir::cleanPath(normalized);
return !clean.isEmpty()
&& !QDir::isAbsolutePath(clean)
&& clean != ".."
&& !clean.startsWith("../")
&& !clean.contains(":");
}
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
@@ -879,42 +953,27 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
return true;
}
void UpdaterLogic::reportDownloadResult(const QString& appId, const QString& channel,
const QString& version, bool success)
{
QJsonArray files;
for (const FileDownloadItem& item : m_fileItems)
files.append(QJsonObject{{"path", item.path}, {"size", item.size}});
QJsonObject body{{"app_id", appId}, {"channel", channel}, {"version", version},
{"result", success ? "success" : "fail"}, {"files", files}};
m_http.postRequest(m_serverAddr + "/api/v1/update/download-report", body,
[](int code, const QJsonObject&) { qDebug() << "Download result report returned code:" << code; });
}
void UpdaterLogic::reportResult(const QString& deviceId,
const QString& fromVer,
const QString& toVer,
bool success)
{
QString url = m_serverAddr + "/api/v1/update/report";
QJsonObject body;
body["app_id"] = ConfigHelper::instance().getValue("App", "app_id");
body["device_id"] = deviceId;
body["from_version"] = fromVer;
body["to_version"] = toVer;
if (success)
body["result"] = "success";
else
body["result"] = "fail";
m_http.postRequest(url, body, [](int code, const QJsonObject& resp)
{
Q_UNUSED(resp);
qDebug() << "Update result report returned code:" << code;
});
}
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
{