2026-09-08 17:08:58 +08:00
#include "UpdateLogic.h"
#include "ConfigHelper.h"
#include <QCoreApplication>
#include <QDebug>
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QSaveFile>
#include <QUrl>
#include <QUrlQuery>
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." ;
}
2026-07-20 06:43:46 +00:00
bool UpdateLogic :: cacheManifest ( const QString & appId , const QString & channel , const QString & version ,
int versionId , const QString & cacheDir )
2026-09-08 17:08:58 +08:00
{
Q_UNUSED ( versionId );
2026-07-21 03:02:42 +00:00
m_error . clear ();
2026-09-08 17:08:58 +08:00
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 );
2026-07-21 03:02:42 +00:00
return false ;
}
2026-09-08 17:08:58 +08:00
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 ;
});
2026-07-21 03:02:42 +00:00
if ( statusCode != 200 ) {
2026-09-08 17:08:58 +08:00
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 ,
2026-07-21 03:02:42 +00:00
message . isEmpty () ? QString () : QCoreApplication :: translate ( "UpdateLogic" , " \n Server message: %1" ). arg ( message ));
return false ;
}
2026-09-08 17:08:58 +08:00
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 ();
2026-07-21 03:02:42 +00:00
if ( manifest . isEmpty () || manifestText . isEmpty ()) {
2026-09-08 17:08:58 +08:00
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 );
2026-07-21 03:02:42 +00:00
return false ;
}
2026-09-08 17:08:58 +08:00
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." )
2026-07-21 03:02:42 +00:00
. arg ( appId , channel , version ,
2026-09-08 17:08:58 +08:00
manifestProduct ,
manifest . value ( QStringLiteral ( "channel" )). toString (),
manifest . value ( QStringLiteral ( "version" )). toString ());
2026-07-21 03:02:42 +00:00
return false ;
}
2026-09-08 17:08:58 +08:00
2026-07-21 03:02:42 +00:00
QDir dir ( cacheDir );
if ( ! dir . exists () && ! dir . mkpath ( "." )) {
2026-09-08 17:08:58 +08:00
m_error = QCoreApplication :: translate (
"UpdateLogic" ,
2026-07-21 03:02:42 +00:00
"Cannot create manifest cache directory. Stage: cache current version manifest. Directory: %1." )
. arg ( cacheDir );
return false ;
}
2026-09-08 17:08:58 +08:00
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" )));
2026-07-21 03:02:42 +00:00
const QByteArray bytes = QJsonDocument ( wrapper ). toJson ( QJsonDocument :: Indented );
if ( ! file . open ( QIODevice :: WriteOnly ) || file . write ( bytes ) != bytes . size () || ! file . commit ()) {
2026-09-08 17:08:58 +08:00
m_error = QCoreApplication :: translate (
"UpdateLogic" ,
"Cannot save manifest cache. Stage: cache current version manifest. File: %1. Error: %2." )
2026-07-21 03:02:42 +00:00
. arg ( file . fileName (), file . errorString ());
return false ;
}
2026-09-08 17:08:58 +08:00
qDebug () << "Current version manifest cached to" << file . fileName ();
2026-07-20 06:43:46 +00:00
return true ;
}
bool UpdateLogic :: refreshGitTagsFile ( const QString & outputPath )
{
2026-09-08 17:08:58 +08:00
Q_UNUSED ( outputPath );
2026-07-20 06:43:46 +00:00
m_error . clear ();
2026-09-08 17:08:58 +08:00
qDebug () << "Git tag export is not part of the current SimCAE Hub admin pages; skipped." ;
2026-07-20 06:43:46 +00:00
return true ;
}
void UpdateLogic :: reportUpdateResult ( const QString & deviceId , const QString & fromVer , const QString & toVer , bool success )
{
2026-09-08 17:08:58 +08:00
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." ;
}