forked from chinosk/gkms-localify-dmm
490 lines
15 KiB
C++
490 lines
15 KiB
C++
#include "stdinclude.hpp"
|
||
#include "cpprest/http_client.h"
|
||
#include "cpprest/filestream.h"
|
||
#include "nlohmann/json.hpp"
|
||
#include "GakumasLocalify/Log.h"
|
||
#include "gkmsGUI/GUII18n.hpp"
|
||
#include <atomic>
|
||
#include <algorithm>
|
||
#include <cctype>
|
||
#include <format>
|
||
#include <sstream>
|
||
#include "unzip.hpp"
|
||
|
||
extern std::filesystem::path gakumasLocalPath;
|
||
extern std::filesystem::path ProgramConfigJson;
|
||
extern bool downloading;
|
||
extern float downloadProgress;
|
||
extern std::function<void()> g_reload_all_data;
|
||
std::string resourceVersionCache = "";
|
||
std::string textureVersionCache = "";
|
||
|
||
namespace GkmsResourceUpdate {
|
||
std::atomic_bool updateJobRunning = false;
|
||
|
||
class UpdateJobGuard {
|
||
public:
|
||
explicit UpdateJobGuard(const char* jobName) : active(false) {
|
||
bool expected = false;
|
||
if (!updateJobRunning.compare_exchange_strong(expected, true)) {
|
||
GakumasLocal::Log::InfoFmt("Skip %s: another resource update job is running.", jobName);
|
||
return;
|
||
}
|
||
active = true;
|
||
downloading = true;
|
||
downloadProgress = 0.0f;
|
||
}
|
||
|
||
~UpdateJobGuard() {
|
||
if (!active) return;
|
||
downloading = false;
|
||
updateJobRunning = false;
|
||
}
|
||
|
||
explicit operator bool() const {
|
||
return active;
|
||
}
|
||
|
||
private:
|
||
bool active;
|
||
};
|
||
|
||
std::string trimString(std::string content) {
|
||
auto is_not_space = [](unsigned char ch) {
|
||
return !std::isspace(ch);
|
||
};
|
||
content.erase(content.begin(), std::find_if(content.begin(), content.end(), is_not_space));
|
||
content.erase(std::find_if(content.rbegin(), content.rend(), is_not_space).base(), content.end());
|
||
return content;
|
||
}
|
||
|
||
std::string readTrimmedFile(const std::filesystem::path& filePath) {
|
||
std::ifstream file(filePath);
|
||
if (!file) {
|
||
return "Unknown";
|
||
}
|
||
|
||
std::stringstream buffer;
|
||
buffer << file.rdbuf();
|
||
return trimString(buffer.str());
|
||
}
|
||
|
||
std::filesystem::path getTextureResourceRoot() {
|
||
return gakumasLocalPath / "texture2d";
|
||
}
|
||
|
||
std::filesystem::path findTextureZipSourceDir(const std::filesystem::path& extractDir) {
|
||
std::error_code ec;
|
||
std::filesystem::path fallback;
|
||
for (const auto& entry : std::filesystem::recursive_directory_iterator(
|
||
extractDir, std::filesystem::directory_options::skip_permission_denied, ec)) {
|
||
if (ec) break;
|
||
if (!entry.is_regular_file(ec)) continue;
|
||
if (entry.path().filename() != "texture_version.txt") continue;
|
||
|
||
const auto parent = entry.path().parent_path();
|
||
if (parent.filename() == "texture2d") {
|
||
return parent;
|
||
}
|
||
if (fallback.empty()) {
|
||
fallback = parent;
|
||
}
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
void saveProgramConfig() {
|
||
nlohmann::json config;
|
||
config["enableConsole"] = g_enable_console;
|
||
config["useRemoteAssets"] = g_useRemoteAssets;
|
||
config["transRemoteZipUrl"] = g_remoteResourceUrl;
|
||
config["useAPIAssets"] = g_useAPIAssets;
|
||
config["useAPIAssetsURL"] = g_useAPIAssetsURL;
|
||
config["useAPITextureAssets"] = g_useAPITextureAssets;
|
||
config["useAPITextureAssetsURL"] = g_useAPITextureAssetsURL;
|
||
config["delTextureRemoteAfterUpdate"] = g_delTextureRemoteAfterUpdate;
|
||
|
||
std::ofstream out(ProgramConfigJson);
|
||
if (!out) {
|
||
GakumasLocal::Log::ErrorFmt("SaveProgramConfig error: Cannot open file: %s", ProgramConfigJson.c_str());
|
||
return;
|
||
}
|
||
out << config.dump(4);
|
||
GakumasLocal::Log::Info("SaveProgramConfig success");
|
||
}
|
||
|
||
web::http::http_response send_get(std::string url, int timeout) {
|
||
web::http::client::http_client_config cfg;
|
||
cfg.set_timeout(utility::seconds(30));
|
||
web::http::client::http_client client(utility::conversions::to_utf16string(url), cfg);
|
||
return client.request(web::http::methods::GET).get();
|
||
}
|
||
|
||
bool DownloadFile(const std::string& url, const std::string& outputPath) {
|
||
using namespace utility;
|
||
using namespace web;
|
||
using namespace web::http;
|
||
using namespace web::http::client;
|
||
using namespace concurrency::streams;
|
||
|
||
try {
|
||
// 打开输出文件流(同步方式)
|
||
auto outTask = fstream::open_ostream(conversions::to_string_t(outputPath));
|
||
outTask.wait();
|
||
auto fileStream = outTask.get();
|
||
|
||
// 创建 HTTP 客户端,注意:如果 url 包含完整路径,cpprestsdk 会自动解析
|
||
http_client client(conversions::to_string_t(url));
|
||
|
||
downloading = true;
|
||
downloadProgress = 0.0f;
|
||
|
||
// 发起 GET 请求
|
||
auto responseTask = client.request(methods::GET);
|
||
responseTask.wait();
|
||
http_response response = responseTask.get();
|
||
if (response.status_code() != status_codes::OK) {
|
||
downloading = false;
|
||
GakumasLocal::Log::ErrorFmt("DownloadFile error: %d", response.status_code());
|
||
return false;
|
||
}
|
||
|
||
// 获取响应头中的文件大小(如果存在)
|
||
uint64_t contentLength = 0;
|
||
if (response.headers().has(L"Content-Length"))
|
||
contentLength = std::stoull(conversions::to_utf8string(response.headers().find(L"Content-Length")->second));
|
||
|
||
// 读取响应体,逐块写入文件,同时更新进度
|
||
auto inStream = response.body();
|
||
const size_t bufferSize = 8192;
|
||
// std::vector<unsigned char> buffer(bufferSize);
|
||
size_t totalDownloaded = 0;
|
||
|
||
while (true) {
|
||
auto readTask = inStream.read(fileStream.streambuf(), bufferSize);
|
||
readTask.wait();
|
||
size_t bytesRead = readTask.get();
|
||
if (bytesRead == 0)
|
||
break;
|
||
totalDownloaded += bytesRead;
|
||
if (contentLength > 0)
|
||
downloadProgress = static_cast<float>(totalDownloaded) / static_cast<float>(contentLength);
|
||
}
|
||
fileStream.close().wait();
|
||
downloading = false;
|
||
return true;
|
||
}
|
||
catch (const std::exception& e) {
|
||
downloading = false;
|
||
GakumasLocal::Log::ErrorFmt("DownloadFile error: %s", e.what());
|
||
return false;
|
||
}
|
||
}
|
||
|
||
std::string GetCurrentResourceVersion(bool useCache) {
|
||
if (useCache) {
|
||
if (!resourceVersionCache.empty()) {
|
||
return resourceVersionCache;
|
||
}
|
||
}
|
||
|
||
auto resourceVersionFile = gakumasLocalPath / "version.txt";
|
||
resourceVersionCache = readTrimmedFile(resourceVersionFile);
|
||
return resourceVersionCache;
|
||
}
|
||
|
||
std::string GetCurrentTextureVersion(bool useCache) {
|
||
if (useCache) {
|
||
if (!textureVersionCache.empty()) {
|
||
return textureVersionCache;
|
||
}
|
||
}
|
||
|
||
auto textureVersionFile = getTextureResourceRoot() / "texture_version.txt";
|
||
textureVersionCache = readTrimmedFile(textureVersionFile);
|
||
return textureVersionCache;
|
||
}
|
||
|
||
bool unzipFileFromURL(std::string downloadUrl, const std::string& unzipPath, const std::string& targetDir = "") {
|
||
std::string tempZipFile = (gakumasLocalPath / "temp_download.zip").string();
|
||
if (std::filesystem::exists(tempZipFile)) {
|
||
std::filesystem::remove(tempZipFile);
|
||
}
|
||
if (!DownloadFile(downloadUrl, tempZipFile)) {
|
||
GakumasLocal::Log::Error("Download zip file failed.");
|
||
return false;
|
||
}
|
||
if (!UnzipFile(tempZipFile, unzipPath, targetDir)) {
|
||
GakumasLocal::Log::Error("Unzip file failed.");
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
bool installTextureZipFromURL(const std::string& downloadUrl, const std::string& expectedVersion) {
|
||
auto textureRoot = getTextureResourceRoot();
|
||
auto tempZipFile = gakumasLocalPath / "temp_texture_download.zip";
|
||
auto extractDir = gakumasLocalPath / "texture2d.extract";
|
||
auto installDir = gakumasLocalPath / "texture2d.tmp";
|
||
|
||
std::error_code ec;
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
std::filesystem::remove_all(extractDir, ec);
|
||
std::filesystem::remove_all(installDir, ec);
|
||
std::filesystem::create_directories(gakumasLocalPath, ec);
|
||
|
||
if (!DownloadFile(downloadUrl, tempZipFile.string())) {
|
||
GakumasLocal::Log::Error("Download texture zip file failed.");
|
||
return false;
|
||
}
|
||
|
||
if (!UnzipFile(tempZipFile.string(), extractDir.string())) {
|
||
GakumasLocal::Log::Error("Unzip texture file failed.");
|
||
if (g_delTextureRemoteAfterUpdate) {
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
auto sourceDir = findTextureZipSourceDir(extractDir);
|
||
if (sourceDir.empty()) {
|
||
GakumasLocal::Log::Error("Texture zip validation failed: texture_version.txt not found.");
|
||
std::filesystem::remove_all(extractDir, ec);
|
||
if (g_delTextureRemoteAfterUpdate) {
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
auto versionFile = sourceDir / "texture_version.txt";
|
||
const auto packageVersion = readTrimmedFile(versionFile);
|
||
if (packageVersion.empty() || packageVersion == "Unknown") {
|
||
GakumasLocal::Log::Error("Texture zip validation failed: texture_version.txt is empty or not found.");
|
||
std::filesystem::remove_all(extractDir, ec);
|
||
if (g_delTextureRemoteAfterUpdate) {
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
}
|
||
return false;
|
||
}
|
||
if (!expectedVersion.empty() && packageVersion != expectedVersion) {
|
||
GakumasLocal::Log::ErrorFmt(
|
||
"Texture zip validation failed: texture_version.txt (%s) differs from release tag (%s).",
|
||
packageVersion.c_str(), expectedVersion.c_str());
|
||
std::filesystem::remove_all(extractDir, ec);
|
||
if (g_delTextureRemoteAfterUpdate) {
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
std::filesystem::copy(sourceDir, installDir,
|
||
std::filesystem::copy_options::recursive | std::filesystem::copy_options::overwrite_existing, ec);
|
||
if (ec) {
|
||
GakumasLocal::Log::ErrorFmt("Copy texture resource failed: %s", ec.message().c_str());
|
||
std::filesystem::remove_all(extractDir, ec);
|
||
if (g_delTextureRemoteAfterUpdate) {
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
std::filesystem::remove_all(textureRoot, ec);
|
||
ec.clear();
|
||
std::filesystem::rename(installDir, textureRoot, ec);
|
||
if (ec) {
|
||
GakumasLocal::Log::ErrorFmt("Install texture resource failed: %s", ec.message().c_str());
|
||
std::filesystem::remove_all(installDir, ec);
|
||
std::filesystem::remove_all(extractDir, ec);
|
||
if (g_delTextureRemoteAfterUpdate) {
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
}
|
||
return false;
|
||
}
|
||
|
||
std::filesystem::remove_all(extractDir, ec);
|
||
if (g_delTextureRemoteAfterUpdate) {
|
||
std::filesystem::remove(tempZipFile, ec);
|
||
}
|
||
|
||
textureVersionCache.clear();
|
||
const auto installedVersion = GetCurrentTextureVersion(false);
|
||
GakumasLocal::Log::InfoFmt("Texture zip installed into %s, version=%s",
|
||
textureRoot.string().c_str(), installedVersion.c_str());
|
||
return true;
|
||
}
|
||
|
||
void CheckUpdateFromAPI(bool isManual) {
|
||
std::thread([isManual]() {
|
||
try {
|
||
if (!g_useAPIAssets) {
|
||
return;
|
||
}
|
||
UpdateJobGuard job("resource API update");
|
||
if (!job) return;
|
||
|
||
GakumasLocal::Log::Info("Checking update from API...");
|
||
|
||
auto response = send_get(g_useAPIAssetsURL, 30);
|
||
if (response.status_code() != 200) {
|
||
GakumasLocal::Log::ErrorFmt("Failed to check update from API: %d\n", response.status_code());
|
||
return;
|
||
}
|
||
|
||
auto data = nlohmann::json::parse(response.extract_utf8string().get());
|
||
|
||
std::string remoteVersion = data["tag_name"];
|
||
const auto localVersion = GetCurrentResourceVersion(false);
|
||
|
||
if (localVersion == remoteVersion) {
|
||
if (isManual) {
|
||
auto check = MessageBoxW(NULL, utility::conversions::to_string_t(GkmsGUII18n::ts("local_file_already_latest")).c_str(),
|
||
L"Check Update", MB_OKCANCEL);
|
||
if (check != IDOK) {
|
||
return;
|
||
}
|
||
}
|
||
else {
|
||
return;
|
||
}
|
||
}
|
||
|
||
std::string description = data["body"];
|
||
|
||
auto check = MessageBoxW(NULL, std::format(L"{} -> {}\n\n{}", utility::conversions::to_string_t(localVersion),
|
||
utility::conversions::to_string_t(remoteVersion), utility::conversions::to_string_t(description)).c_str(),
|
||
L"Resource Update", MB_OKCANCEL);
|
||
if (check != IDOK) {
|
||
return;
|
||
}
|
||
|
||
|
||
if (!data.contains("assets") || !data["assets"].is_array()) {
|
||
GakumasLocal::Log::Error("API response doesn't contain assets array.");
|
||
return;
|
||
}
|
||
for (const auto& asset : data["assets"]) {
|
||
if (!asset.contains("name") || !asset.contains("browser_download_url"))
|
||
continue;
|
||
std::string name = asset["name"];
|
||
if (name.ends_with(".zip")) {
|
||
std::string downloadUrl = asset["browser_download_url"];
|
||
|
||
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string())) {
|
||
g_reload_all_data();
|
||
GakumasLocal::Log::Info("Update completed.");
|
||
}
|
||
// 仅解压一个文件
|
||
return;
|
||
}
|
||
}
|
||
GakumasLocal::Log::Error("No .zip file found.");
|
||
|
||
}
|
||
catch (std::exception& e) {
|
||
GakumasLocal::Log::ErrorFmt("Exception occurred in CheckUpdateFromAPI: %s\n", e.what());
|
||
}
|
||
}).detach();
|
||
}
|
||
|
||
void CheckTextureUpdateFromAPI(bool isManual) {
|
||
std::thread([isManual]() {
|
||
try {
|
||
if (!g_useAPITextureAssets) {
|
||
return;
|
||
}
|
||
UpdateJobGuard job("texture API update");
|
||
if (!job) return;
|
||
|
||
GakumasLocal::Log::Info("Checking texture update from API...");
|
||
|
||
auto response = send_get(g_useAPITextureAssetsURL, 30);
|
||
if (response.status_code() != 200) {
|
||
GakumasLocal::Log::ErrorFmt("Failed to check texture update from API: %d\n", response.status_code());
|
||
return;
|
||
}
|
||
|
||
auto data = nlohmann::json::parse(response.extract_utf8string().get());
|
||
if (!data.contains("tag_name") || !data["tag_name"].is_string()) {
|
||
GakumasLocal::Log::Error("Texture API response doesn't contain tag_name.");
|
||
return;
|
||
}
|
||
|
||
std::string remoteVersion = data["tag_name"];
|
||
const auto localVersion = GetCurrentTextureVersion(false);
|
||
|
||
if (localVersion == remoteVersion) {
|
||
if (isManual) {
|
||
auto check = MessageBoxW(NULL, utility::conversions::to_string_t(GkmsGUII18n::ts("local_file_already_latest")).c_str(),
|
||
L"Texture Resource Update", MB_OKCANCEL);
|
||
if (check != IDOK) {
|
||
return;
|
||
}
|
||
}
|
||
else {
|
||
return;
|
||
}
|
||
}
|
||
|
||
std::string description = "";
|
||
if (data.contains("body") && data["body"].is_string()) {
|
||
description = data["body"];
|
||
}
|
||
|
||
auto check = MessageBoxW(NULL, std::format(L"{} -> {}\n\n{}", utility::conversions::to_string_t(localVersion),
|
||
utility::conversions::to_string_t(remoteVersion), utility::conversions::to_string_t(description)).c_str(),
|
||
L"Texture Resource Update", MB_OKCANCEL);
|
||
if (check != IDOK) {
|
||
return;
|
||
}
|
||
|
||
if (!data.contains("assets") || !data["assets"].is_array()) {
|
||
GakumasLocal::Log::Error("Texture API response doesn't contain assets array.");
|
||
return;
|
||
}
|
||
|
||
std::string downloadUrl = "";
|
||
for (const auto& asset : data["assets"]) {
|
||
if (!asset.contains("name") || !asset.contains("browser_download_url") || !asset["name"].is_string() || !asset["browser_download_url"].is_string()) {
|
||
continue;
|
||
}
|
||
std::string name = asset["name"];
|
||
std::string lowerName = name;
|
||
std::transform(lowerName.begin(), lowerName.end(), lowerName.begin(), [](unsigned char ch) {
|
||
return static_cast<char>(std::tolower(ch));
|
||
});
|
||
if (lowerName.ends_with(".zip")) {
|
||
downloadUrl = asset["browser_download_url"];
|
||
if (lowerName == "texture2d.zip") {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (downloadUrl.empty()) {
|
||
GakumasLocal::Log::Error("No texture .zip file found.");
|
||
return;
|
||
}
|
||
|
||
if (installTextureZipFromURL(downloadUrl, remoteVersion)) {
|
||
g_reload_all_data();
|
||
GakumasLocal::Log::Info("Texture update completed.");
|
||
}
|
||
}
|
||
catch (std::exception& e) {
|
||
GakumasLocal::Log::ErrorFmt("Exception occurred in CheckTextureUpdateFromAPI: %s\n", e.what());
|
||
}
|
||
}).detach();
|
||
}
|
||
|
||
void checkUpdateFromURL(const std::string& downloadUrl) {
|
||
std::thread([downloadUrl]() {
|
||
UpdateJobGuard job("remote zip update");
|
||
if (!job) return;
|
||
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string(), "local-files")) {
|
||
g_reload_all_data();
|
||
GakumasLocal::Log::Info("Update completed.");
|
||
}
|
||
}).detach();
|
||
}
|
||
}
|