#include "UpdateLogic.h" #include "ConfigHelper.h" #include #include #include #include #include #include #include #include namespace { QString trimBaseUrl(QString value) { value = value.trimmed(); while (value.endsWith(QLatin1Char('/'))) value.chop(1); return value; } void addQueryValue(QUrlQuery& query, const QString& key, const QString& value) { const QString trimmed = value.trimmed(); if (!trimmed.isEmpty()) query.addQueryItem(key, trimmed); } QString serverMessage(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 object = detail.toObject(); const QString detailMsg = object.value(QStringLiteral("msg")).toString(); if (!detailMsg.isEmpty()) return detailMsg; const QString error = object.value(QStringLiteral("error")).toString(); if (!error.isEmpty()) return error; } return detail.toString(); } QJsonObject responseDataObject(const QJsonObject& response) { return response.value(QStringLiteral("data")).isObject() ? response.value(QStringLiteral("data")).toObject() : response; } QString configValue(const QString& key, const QString& fallback = QString()) { const QString value = ConfigHelper::instance().getValue(QString(), key).trimmed(); return value.isEmpty() ? fallback : value; } } // namespace UpdateLogic::UpdateLogic(QObject* parent) : QObject(parent) { ConfigHelper& cfg = ConfigHelper::instance(); m_serverAddr = trimBaseUrl(cfg.getValue("Server", "api_base_url")); m_appId = cfg.getValue("App", "product_code").trimmed(); if (m_appId.isEmpty()) m_appId = cfg.getValue("App", "app_id").trimmed(); m_curVer = cfg.getValue("App", "current_version").trimmed(); m_channel = cfg.getValue("App", "channel").trimmed(); if (m_channel.isEmpty()) m_channel = QStringLiteral("stable"); qDebug() << "Read server addr:" << m_serverAddr; qDebug() << "Read product code:" << m_appId; } void UpdateLogic::checkUpdate() { m_error.clear(); m_needUpdate = false; m_networkOk = false; m_lastStatusCode = 0; m_latestVer.clear(); m_checkResp = QJsonObject(); if (m_serverAddr.isEmpty() || m_appId.isEmpty() || m_curVer.isEmpty()) { m_error = QCoreApplication::translate( "UpdateLogic", "Update configuration is incomplete. Server, product_code and current_version are required."); qDebug() << "Config incomplete, abort update check:" << m_error; return; } if (configValue(QStringLiteral("client_token")).isEmpty()) { m_lastStatusCode = 401; m_error = QCoreApplication::translate( "UpdateLogic", "Update configuration is incomplete. client_token is required."); m_checkResp.insert(QStringLiteral("msg"), m_error); qDebug() << "Client token missing, abort update check:" << m_error; return; } QUrl url(m_serverAddr + QStringLiteral("/api/v1/client/update/authorized-check")); QUrlQuery query; addQueryValue(query, QStringLiteral("productCode"), m_appId); addQueryValue(query, QStringLiteral("currentVersion"), m_curVer); addQueryValue(query, QStringLiteral("clientVersion"), configValue(QStringLiteral("client_protocol"), QStringLiteral("3"))); addQueryValue(query, QStringLiteral("channel"), m_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](int code, const QJsonObject& resp) { qDebug() << "Check update HTTP code:" << code; m_lastStatusCode = code; m_checkResp = resp; m_networkOk = (code == 200); if (code != 200) { m_needUpdate = false; m_error = serverMessage(resp); qDebug() << "Check update api failed:" << m_error; return; } const QJsonArray releases = resp.value(QStringLiteral("data")).toArray(); QJsonObject selected; for (const QJsonValue& value : releases) { const QJsonObject release = value.toObject(); const bool hasPackages = !release.value(QStringLiteral("packages")).toArray().isEmpty(); const bool hasManifest = release.value(QStringLiteral("manifest")).isObject() || !release.value(QStringLiteral("manifestUri")).toString().isEmpty(); if (hasPackages && hasManifest) { selected = release; break; } } if (selected.isEmpty()) { m_needUpdate = false; if (!releases.isEmpty()) m_latestVer = releases.first().toObject().value(QStringLiteral("version")).toString(); qDebug() << "No entitled downloadable update for current client."; return; } m_checkResp = selected; m_checkResp.insert(QStringLiteral("need_update"), true); m_checkResp.insert(QStringLiteral("latest_version"), selected.value(QStringLiteral("version")).toString()); m_checkResp.insert(QStringLiteral("release_id"), selected.value(QStringLiteral("id")).toString()); m_needUpdate = true; m_latestVer = selected.value(QStringLiteral("version")).toString(); qDebug() << "Need update:" << m_needUpdate; qDebug() << "Latest version:" << m_latestVer; qDebug() << "Release id:" << selected.value(QStringLiteral("id")).toString(); }); } void UpdateLogic::getDownloadUrl(const QString& appId, const QString& channel, const QString& ver, int verId) { Q_UNUSED(appId); Q_UNUSED(channel); Q_UNUSED(ver); Q_UNUSED(verId); qDebug() << "SimCAE Hub manifest already contains authorized package download URLs."; } bool UpdateLogic::cacheManifest(const QString& appId, const QString& channel, const QString& version, int versionId, const QString& cacheDir) { Q_UNUSED(versionId); m_error.clear(); if (appId.isEmpty() || channel.isEmpty() || version.isEmpty()) { m_error = QCoreApplication::translate( "UpdateLogic", "Current version manifest identity is incomplete. Stage: cache current version manifest. Product: %1, channel: %2, version: %3.") .arg(appId, channel, version); return false; } QUrl url(m_serverAddr + QStringLiteral("/api/v1/client/update/manifest")); QUrlQuery query; addQueryValue(query, QStringLiteral("productCode"), appId); addQueryValue(query, QStringLiteral("version"), version); 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); QJsonObject response; int statusCode = 0; m_http.getRequest(url.toString(QUrl::FullyEncoded), [&](int code, const QJsonObject& resp) { statusCode = code; response = resp; }); if (statusCode != 200) { const QString message = serverMessage(response); m_error = QCoreApplication::translate( "UpdateLogic", "Cannot download manifest for the current local version. Stage: cache current version manifest. HTTP status: %1. Product: %2, channel: %3, version: %4.%5") .arg(QString::number(statusCode), appId, channel, version, message.isEmpty() ? QString() : QCoreApplication::translate("UpdateLogic", "\nServer message: %1").arg(message)); return false; } QJsonObject wrapper = responseDataObject(response); const QJsonObject manifest = wrapper.value(QStringLiteral("manifest")).toObject(); QString manifestText = wrapper.value(QStringLiteral("manifestText")).toString(); if (manifestText.isEmpty()) manifestText = wrapper.value(QStringLiteral("manifest_text")).toString(); if (manifest.isEmpty() || manifestText.isEmpty()) { m_error = QCoreApplication::translate( "UpdateLogic", "The manifest response for the current local version is incomplete. Stage: cache current version manifest. Product: %1, channel: %2, version: %3.") .arg(appId, channel, version); return false; } const QString manifestProduct = manifest.value(QStringLiteral("productCode")).toString( manifest.value(QStringLiteral("app_id")).toString()); if (manifestProduct != appId || manifest.value(QStringLiteral("channel")).toString() != channel || manifest.value(QStringLiteral("version")).toString() != version) { m_error = QCoreApplication::translate( "UpdateLogic", "The manifest identity does not match the current local version. Stage: cache current version manifest. Expected product/channel/version: %1 / %2 / %3. Manifest product/channel/version: %4 / %5 / %6.") .arg(appId, channel, version, manifestProduct, manifest.value(QStringLiteral("channel")).toString(), manifest.value(QStringLiteral("version")).toString()); return false; } QDir dir(cacheDir); if (!dir.exists() && !dir.mkpath(".")) { m_error = QCoreApplication::translate( "UpdateLogic", "Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1.") .arg(cacheDir); return false; } wrapper.insert(QStringLiteral("manifest"), manifest); wrapper.insert(QStringLiteral("manifestText"), manifestText); wrapper.insert(QStringLiteral("manifest_text"), manifestText); QSaveFile file(dir.filePath(QStringLiteral("manifest_") + version + QStringLiteral(".json"))); const QByteArray bytes = QJsonDocument(wrapper).toJson(QJsonDocument::Indented); if (!file.open(QIODevice::WriteOnly) || file.write(bytes) != bytes.size() || !file.commit()) { m_error = QCoreApplication::translate( "UpdateLogic", "Cannot save manifest cache. Stage: cache current version manifest. File: %1. Error: %2.") .arg(file.fileName(), file.errorString()); return false; } qDebug() << "Current version manifest cached to" << file.fileName(); return true; } bool UpdateLogic::refreshGitTagsFile(const QString& outputPath) { Q_UNUSED(outputPath); m_error.clear(); qDebug() << "Git tag export is not part of the current SimCAE Hub admin pages; skipped."; return true; } void UpdateLogic::reportUpdateResult(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."; }