fix: 优化客户端错误提示和校验诊断信息
This commit is contained in:
+367
-209
@@ -25,16 +25,32 @@
|
||||
#include <openssl/err.h>
|
||||
#endif
|
||||
|
||||
UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url");
|
||||
}
|
||||
UpdaterLogic::UpdaterLogic(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url");
|
||||
}
|
||||
|
||||
namespace {
|
||||
QString serverDetailMessage(const QJsonObject& response)
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
// Manifest 由服务端按版本动态生成,描述目标版本包含哪些文件以及每个文件的 SHA256。
|
||||
// Updater 先拿到 Manifest,再请求下载 URL,最后按 Manifest 校验本地文件。
|
||||
m_error.clear();
|
||||
QString url = m_serverAddr + "/api/v1/update/manifest";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
@@ -42,7 +58,7 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
|
||||
body["version"] = targetVer;
|
||||
body["version_id"] = versionId;
|
||||
|
||||
m_http.postRequest(url, body, [this](int code, const QJsonObject& resp)
|
||||
m_http.postRequest(url, body, [this, appId, channel, targetVer, versionId](int code, const QJsonObject& resp)
|
||||
{
|
||||
qDebug() << "Manifest API returned code:" << code;
|
||||
m_manifest = QJsonObject();
|
||||
@@ -68,61 +84,82 @@ void UpdaterLogic::getManifest(const QString& appId, const QString& channel, con
|
||||
m_fileItems.append(fi);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Manifest response missing fields";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get manifest";
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
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. 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();
|
||||
});
|
||||
}
|
||||
|
||||
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";
|
||||
return false;
|
||||
#else
|
||||
QFile keyFile(publicKeyPath);
|
||||
if (!keyFile.open(QIODevice::ReadOnly))
|
||||
{
|
||||
qDebug() << "Cannot open public key file:" << publicKeyPath;
|
||||
return false;
|
||||
}
|
||||
#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";
|
||||
return false;
|
||||
}
|
||||
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";
|
||||
return false;
|
||||
}
|
||||
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";
|
||||
return false;
|
||||
}
|
||||
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)
|
||||
@@ -139,29 +176,35 @@ bool UpdaterLogic::verifySignature(const QByteArray& payload, const QString& sig
|
||||
EVP_MD_CTX_free(mdctx);
|
||||
EVP_PKEY_free(pkey);
|
||||
|
||||
if (!ok)
|
||||
{
|
||||
qDebug() << "Manifest signature verification failed";
|
||||
}
|
||||
return ok;
|
||||
#endif
|
||||
}
|
||||
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";
|
||||
return false;
|
||||
}
|
||||
{
|
||||
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;
|
||||
}
|
||||
QString signature = m_manifest.value("signature").toString();
|
||||
if (signature.isEmpty())
|
||||
{
|
||||
qDebug() << "Manifest signature empty";
|
||||
return false;
|
||||
}
|
||||
if (signature.isEmpty())
|
||||
{
|
||||
qDebug() << "Manifest signature empty";
|
||||
m_error = QCoreApplication::translate("UpdaterLogic",
|
||||
"The manifest does not contain a signature. Stage: manifest signature verification.");
|
||||
return false;
|
||||
}
|
||||
|
||||
QString path = publicKeyPath;
|
||||
if (!QFile::exists(path))
|
||||
@@ -175,18 +218,29 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
{
|
||||
// 启动后的完整性检查依赖本地 Manifest 缓存。
|
||||
// 升级成功后缓存签名清单,下一次离线启动也能校验当前版本文件。
|
||||
if (m_manifest.isEmpty())
|
||||
return false;
|
||||
|
||||
QDir dir(cacheDir);
|
||||
if (!dir.exists() && !dir.mkpath("."))
|
||||
return false;
|
||||
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))
|
||||
return false;
|
||||
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;
|
||||
@@ -194,33 +248,48 @@ bool UpdaterLogic::saveManifestCache(const QString& cacheDir) const
|
||||
|
||||
QJsonDocument doc(wrapper);
|
||||
file.write(doc.toJson(QJsonDocument::Indented));
|
||||
file.close();
|
||||
qDebug() << "Manifest cached to" << filePath;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::loadManifestCache(const QString& cacheDir, const QString& version)
|
||||
{
|
||||
QString filePath = cacheDir + "/manifest_" + version + ".json";
|
||||
QFile file(filePath);
|
||||
if (!file.exists() || !file.open(QIODevice::ReadOnly))
|
||||
return false;
|
||||
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())
|
||||
return false;
|
||||
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("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;
|
||||
}
|
||||
|
||||
QJsonObject wrapper = doc.object();
|
||||
if (!wrapper.contains("manifest") || !wrapper.contains("manifest_text"))
|
||||
return false;
|
||||
|
||||
m_manifest = wrapper["manifest"].toObject();
|
||||
m_manifestText = wrapper["manifest_text"].toString();
|
||||
qDebug() << "Loaded cached manifest" << version;
|
||||
return true;
|
||||
}
|
||||
m_manifest = wrapper["manifest"].toObject();
|
||||
m_manifestText = wrapper["manifest_text"].toString();
|
||||
qDebug() << "Loaded cached manifest" << version;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
QByteArray UpdaterLogic::canonicalManifestBytes(const QJsonObject& manifest) const
|
||||
{
|
||||
@@ -311,43 +380,63 @@ QStringList UpdaterLogic::obsoleteFilesComparedTo(const QJsonObject& oldManifest
|
||||
return obsolete;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::validateLocalFiles(const QString& stagingDir, const QString& installedDir) const
|
||||
{
|
||||
if (m_manifest.isEmpty())
|
||||
return false;
|
||||
|
||||
const QJsonArray files = m_manifest.value("files").toArray();
|
||||
for (const auto& item : files)
|
||||
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))
|
||||
return false;
|
||||
if (isRuntimeProtectedPath(path)) {
|
||||
qDebug() << "Runtime-protected manifest entry ignored:" << path;
|
||||
continue;
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
const QString actualSha = calcLocalFileSha256(fullPath);
|
||||
if (actualSha.compare(expectedSha, Qt::CaseInsensitive) != 0)
|
||||
{
|
||||
qDebug() << "File hash mismatch:" << path << actualSha << expectedSha;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
qDebug() << "Full manifest validation passed using staging plus installed files";
|
||||
return true;
|
||||
}
|
||||
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 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)
|
||||
{
|
||||
@@ -395,9 +484,10 @@ bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString&
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
QString url = m_serverAddr + "/api/v1/update/download-url";
|
||||
void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& targetVer, int versionId)
|
||||
{
|
||||
m_error.clear();
|
||||
QString url = m_serverAddr + "/api/v1/update/download-url";
|
||||
QJsonObject body;
|
||||
body["app_id"] = appId;
|
||||
body["channel"] = channel;
|
||||
@@ -407,7 +497,7 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
|
||||
QJsonArray emptyFiles;
|
||||
body["files"] = emptyFiles;
|
||||
|
||||
m_http.postRequest(url, body, [this](int code, const QJsonObject& resp)
|
||||
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();
|
||||
@@ -427,13 +517,18 @@ void UpdaterLogic::getDownloadUrl(const QString& appId, const QString& channel,
|
||||
qDebug() << "File info:" << fi.path << fi.url << fi.sha256;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to get download URL";
|
||||
}
|
||||
emit fetchUrlFinished();
|
||||
});
|
||||
}
|
||||
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();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
QString UpdaterLogic::calcLocalFileSha256(const QString& filePath) const
|
||||
@@ -454,13 +549,21 @@ QString UpdaterLogic::calcLocalFileSha256(const QString& filePath) const
|
||||
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;
|
||||
if (!QDir().mkpath(QFileInfo(partPath).path())) return false;
|
||||
if (QFile::exists(savePath)
|
||||
&& calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
return true;
|
||||
QFile::remove(savePath);
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -472,23 +575,37 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
}
|
||||
if (expectedSize >= 0 && existingSize == expectedSize && existingSize > 0)
|
||||
{
|
||||
if (calcLocalFileSha256(partPath).compare(expectSha256, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
QFile::remove(savePath);
|
||||
return QFile::rename(partPath, savePath);
|
||||
}
|
||||
QFile::remove(partPath);
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
@@ -533,27 +650,45 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (expectedSize >= 0 && actualSize >= expectedSize)
|
||||
{
|
||||
qDebug() << "Completed partial file has invalid size or SHA; restart:" << savePath;
|
||||
QFile::remove(partPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Download attempt failed; partial file retained:" << attempt + 1
|
||||
<< savePath << httpStatus << networkError << "bytes" << 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)
|
||||
{
|
||||
@@ -566,9 +701,14 @@ bool UpdaterLogic::downloadSingleFile(const QString& url, const QString& savePat
|
||||
QThread::msleep(50);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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
|
||||
{
|
||||
@@ -632,20 +772,31 @@ qint64 UpdaterLogic::estimateAdditionalDiskBytes(const QString& targetDir,
|
||||
return required + reserve;
|
||||
}
|
||||
|
||||
bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targetDir)
|
||||
{
|
||||
if (m_fileItems.isEmpty())
|
||||
{
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir stagingDir(tempDir);
|
||||
if (!FileHelper::createDir(tempDir))
|
||||
return false;
|
||||
const QString resumeCacheDir = QDir(ConfigHelper::instance().updateRoot()).filePath("download_cache");
|
||||
if (!FileHelper::createDir(resumeCacheDir))
|
||||
return false;
|
||||
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");
|
||||
@@ -671,12 +822,15 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
|
||||
for (const auto& fi : m_fileItems)
|
||||
{
|
||||
if (!isSafeRelativePath(fi.path))
|
||||
{
|
||||
qDebug() << "Unsafe relative path in manifest:" << fi.path;
|
||||
m_downloadAllOk = false;
|
||||
return false;
|
||||
}
|
||||
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)) {
|
||||
@@ -693,12 +847,15 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
@@ -717,9 +874,10 @@ bool UpdaterLogic::downloadAllFiles(const QString& tempDir, const QString& targe
|
||||
m_currentDownloadPath, speed);
|
||||
}
|
||||
|
||||
m_downloadAllOk = true;
|
||||
return true;
|
||||
}
|
||||
m_downloadAllOk = true;
|
||||
m_error.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void UpdaterLogic::reportDownloadResult(const QString& appId, const QString& channel,
|
||||
const QString& version, bool success)
|
||||
|
||||
Reference in New Issue
Block a user