#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 #include #include #include #include #include "unzip.hpp" extern std::filesystem::path gakumasLocalPath; extern std::filesystem::path ProgramConfigJson; extern bool downloading; extern float downloadProgress; extern std::function 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 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(totalDownloaded) / static_cast(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(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(); } }