#include "UpdaterLogic.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ConfigHelper.h" #ifdef HAVE_OPENSSL #include #include #include #include #endif UpdaterLogic::UpdaterLogic(QObject* parent) : QObject(parent) { m_serverAddr = ConfigHelper::instance().getValue("Server", "api_base_url"); } void UpdaterLogic::getManifest(const QString& appId, const QString& channel, const QString& targetVer, int versionId) { 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](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(); 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); } } else { qDebug() << "Manifest response missing fields"; } } else { qDebug() << "Failed to get manifest"; } 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; } 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; } EVP_PKEY* pkey = PEM_read_bio_PUBKEY(bio, NULL, NULL, NULL); BIO_free(bio); if (!pkey) { qDebug() << "PEM_read_bio_PUBKEY failed"; 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; } 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(signature.constData()), signature.size()) == 1) { ok = true; } } } EVP_MD_CTX_free(mdctx); EVP_PKEY_free(pkey); if (!ok) { qDebug() << "Manifest signature verification failed"; } return ok; #endif } bool UpdaterLogic::verifyManifestSignature(const QString& publicKeyPath) const { if (m_manifest.isEmpty() || m_manifestText.isEmpty()) { qDebug() << "No manifest available to verify"; return false; } QString signature = m_manifest.value("signature").toString(); if (signature.isEmpty()) { qDebug() << "Manifest signature empty"; return false; } 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 { if (m_manifest.isEmpty()) return false; QDir dir(cacheDir); if (!dir.exists() && !dir.mkpath(".")) 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; QJsonObject wrapper; wrapper["manifest"] = m_manifest; wrapper["manifest_text"] = m_manifestText; 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; QByteArray raw = file.readAll(); file.close(); QJsonDocument doc = QJsonDocument::fromJson(raw); if (!doc.isObject()) 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; } 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()); 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 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()); } const QSet protectedPaths{ QStringLiteral("bootstrap.exe"), QStringLiteral("launcher.exe"), 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") }; QStringList obsolete; QSet 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; seen.insert(folded); obsolete.append(path); } obsolete.sort(Qt::CaseInsensitive); 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) { 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; } 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; } bool UpdaterLogic::loadOfflinePackage(const QString& packagePath, const QString& stagingDir) { m_offlineError.clear(); QFile package(packagePath); if (!package.open(QIODevice::ReadOnly)) { m_offlineError = "无法打开离线更新包"; return false; } if (package.read(8) != QByteArray("MUPD0001", 8)) { m_offlineError = "离线包格式标识无效"; return false; } const QByteArray lengthBytes = package.read(8); if (lengthBytes.size() != 8) { m_offlineError = "离线包头不完整"; return false; } quint64 headerSize = 0; for (int i = 0; i < 8; ++i) headerSize |= quint64(static_cast(lengthBytes[i])) << (i * 8); if (headerSize == 0 || headerSize > 64ULL * 1024 * 1024 || headerSize > quint64(package.size() - 16)) { m_offlineError = "离线包头长度无效"; return false; } QJsonParseError error; const QJsonDocument wrapperDoc = QJsonDocument::fromJson(package.read(qint64(headerSize)), &error); if (error.error != QJsonParseError::NoError || !wrapperDoc.isObject()) { m_offlineError = "离线包头 JSON 无效"; 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 = "离线包 RSA 签名无效"; return false; } const QJsonObject packageMeta = QJsonDocument::fromJson(packageText, &error).object(); if (error.error != QJsonParseError::NoError || packageMeta.value("format").toString() != "MUPD0001") { m_offlineError = "离线包签名元数据无效"; return false; } if (QString::fromLatin1(QCryptographicHash::hash(manifestText, QCryptographicHash::Sha256).toHex()) != packageMeta.value("manifest_sha256").toString()) { m_offlineError = "Manifest 摘要与包签名不一致"; return false; } QJsonObject manifest = QJsonDocument::fromJson(manifestText, &error).object(); if (error.error != QJsonParseError::NoError || manifest.isEmpty()) { m_offlineError = "离线 Manifest 无效"; 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 = "包信息与 Manifest 身份不一致"; return false; } if (!verifyManifestSignature()) { m_offlineError = "离线 Manifest RSA 签名无效"; 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 = "离线包包含不安全路径或越界数据: " + path; return false; } FileDownloadItem fi{path, QString(), item.value("sha256").toString(), size}; m_fileItems.append(fi); if (stagingDir.isEmpty()) continue; const QString target = QDir(stagingDir).filePath(path); if (!QDir().mkpath(QFileInfo(target).path()) || !package.seek(payloadStart + offset)) { m_offlineError = "无法准备离线文件: " + path; return false; } QSaveFile output(target); if (!output.open(QIODevice::WriteOnly)) { m_offlineError = "无法创建暂存文件: " + path; return false; } QCryptographicHash hash(QCryptographicHash::Sha256); qint64 remaining = size; while (remaining > 0) { const QByteArray block = package.read(qMin(remaining, 1024 * 1024)); if (block.isEmpty() || output.write(block) != block.size()) { output.cancelWriting(); m_offlineError = "离线文件读取失败: " + 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 = "离线文件 Hash 或写入失败: " + path; return false; } } if (entries.size() != m_manifest.value("files").toArray().size()) { m_offlineError = "离线包文件数量与 Manifest 不一致"; return false; } 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"; 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](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"; } 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; if (!QDir().mkpath(QFileInfo(partPath).path())) return false; if (QFile::exists(savePath) && calcLocalFileSha256(savePath).compare(expectSha256, Qt::CaseInsensitive) == 0) 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); return QFile::rename(partPath, savePath); } 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; } QNetworkAccessManager manager; manager.setProxy(QNetworkProxy::NoProxy); QNetworkRequest request(url); request.setTransferTimeout(60000); 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) { // 服务端忽略 Range,当前文件是“旧片段 + 完整响应”,必须安全重下。 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; 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 (attempt < 3) { const unsigned long delayMs = static_cast(500 * (1 << attempt)); QElapsedTimer delay; delay.start(); while (delay.elapsed() < static_cast(delayMs)) { QApplication::processEvents(); QThread::msleep(50); } } } return false; } bool UpdaterLogic::isRuntimeProtectedPath(const QString& path) const { const QString normalized = QDir::fromNativeSeparators(path).toCaseFolded(); static const QSet protectedPaths{ 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") }; 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(":"); } qint64 UpdaterLogic::estimateAdditionalDiskBytes(const QString& targetDir, const QStringList& obsoletePaths) const { qint64 required = 0; const QString cacheDir = QDir(targetDir).filePath("update/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(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(128LL * 1024 * 1024, required / 10); 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(targetDir).filePath("update/download_cache"); if (!FileHelper::createDir(resumeCacheDir)) return false; QSet 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; 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; 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; 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; }); } QList UpdaterLogic::getFileList() const { return m_fileItems; }