This commit is contained in:
chinosk
2025-03-18 09:28:58 +00:00
parent 45338b40cd
commit 3fe2d1775b
39 changed files with 56771 additions and 68 deletions

View File

@@ -0,0 +1,224 @@
#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 <format>
#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 = "";
namespace GkmsResourceUpdate {
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;
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";
std::ifstream file(resourceVersionFile);
if (!file) {
// GakumasLocal::Log::ErrorFmt("Can't open file: %s", resourceVersionFile.string().c_str());
return "Unknown";
}
std::stringstream buffer;
buffer << file.rdbuf();
std::string content = buffer.str();
// 去除首尾空格和换行符
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());
resourceVersionCache = content;
return content;
}
bool unzipFileFromURL(std::string downloadUrl, const std::string& unzipPath) {
std::string tempZipFile = (gakumasLocalPath / "temp_download.zip").string();
if (!DownloadFile(downloadUrl, tempZipFile)) {
GakumasLocal::Log::Error("Download zip file failed.");
return false;
}
if (!UnzipFile(tempZipFile, unzipPath)) {
GakumasLocal::Log::Error("Unzip file failed.");
return false;
}
return true;
}
void CheckUpdateFromAPI(bool isManual) {
std::thread([&isManual]() {
try {
if (!g_useAPIAssets) {
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 = MessageBoxA(NULL, GkmsGUII18n::ts("local_file_already_latest"), "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 checkUpdateFromURL(std::string downloadUrl) {
std::thread([&downloadUrl]() {
if (unzipFileFromURL(downloadUrl, gakumasLocalPath.string())) {
g_reload_all_data();
GakumasLocal::Log::Info("Update completed.");
}
}).detach();
}
}

View File

@@ -0,0 +1,10 @@
#pragma once
#include <string>
namespace GkmsResourceUpdate {
void saveProgramConfig();
std::string GetCurrentResourceVersion(bool useCache);
void CheckUpdateFromAPI(bool isManual);
void checkUpdateFromURL(std::string downloadUrl);
}

View File

@@ -0,0 +1,149 @@
#pragma once
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <vector>
#include <errno.h>
#include <sys/stat.h>
#ifdef _WIN32
#include <direct.h>
#endif
#include "minizip/unzip.h"
#include "GakumasLocalify/Log.h"
// 辅助函数:递归创建目录
static bool CreateDirectoryRecursively(const std::string& dir) {
if (dir.empty())
return false;
// 尝试创建目录
#ifdef _WIN32
int ret = _mkdir(dir.c_str());
#else
int ret = mkdir(dir.c_str(), 0755);
#endif
if (ret == 0 || errno == EEXIST)
return true;
// 如果创建失败,尝试先创建父目录
size_t pos = dir.find_last_of("/\\");
if (pos != std::string::npos) {
std::string parentDir = dir.substr(0, pos);
if (!CreateDirectoryRecursively(parentDir))
return false;
#ifdef _WIN32
ret = _mkdir(dir.c_str());
#else
ret = mkdir(dir.c_str(), 0755);
#endif
return (ret == 0 || errno == EEXIST);
}
return false;
}
// 解压缩函数
static bool UnzipFile(const std::string& zipPath, const std::string& destinationFolder) {
// 打开zip文件
unzFile zipfile = unzOpen(zipPath.c_str());
if (zipfile == nullptr) {
GakumasLocal::Log::ErrorFmt( "Can't open zip file: %s", zipPath.c_str());
return false;
}
int ret = unzGoToFirstFile(zipfile);
if (ret != UNZ_OK) {
GakumasLocal::Log::ErrorFmt("Can't read first file %s", zipPath.c_str());
unzClose(zipfile);
return false;
}
// 遍历zip内的每个文件
do {
char filename[512] = { 0 };
unz_file_info fileInfo;
ret = unzGetCurrentFileInfo(zipfile, &fileInfo,
filename, sizeof(filename),
nullptr, 0, nullptr, 0);
if (ret != UNZ_OK) {
GakumasLocal::Log::ErrorFmt("Read ZIP File Info Error");
unzClose(zipfile);
return false;
}
std::string filePath = filename;
std::string fullPath = destinationFolder;
// 保证目标目录以路径分隔符结尾
if (fullPath.back() != '/' && fullPath.back() != '\\') {
fullPath += "/";
}
fullPath += filePath;
// 判断是否为目录(目录条目通常以'/'结尾)
if (!filePath.empty() && (filePath.back() == '/' || filePath.back() == '\\')) {
// 创建目录
if (!CreateDirectoryRecursively(fullPath)) {
GakumasLocal::Log::ErrorFmt("Create Dir Failed: %s", fullPath.c_str());
unzClose(zipfile);
return false;
}
}
else {
// 对于文件,先确保其上级目录存在
size_t pos = fullPath.find_last_of("/\\");
if (pos != std::string::npos) {
std::string directory = fullPath.substr(0, pos);
if (!CreateDirectoryRecursively(directory)) {
GakumasLocal::Log::ErrorFmt("Create Dir Failed: %s", directory.c_str());
unzClose(zipfile);
return false;
}
}
// 打开zip中文件
ret = unzOpenCurrentFile(zipfile);
if (ret != UNZ_OK) {
GakumasLocal::Log::ErrorFmt("Open file in zip failed: %s", filePath.c_str());
unzClose(zipfile);
return false;
}
// 在目标路径上创建新文件
FILE* outFile = fopen(fullPath.c_str(), "wb");
if (outFile == nullptr) {
GakumasLocal::Log::ErrorFmt("Can't create output file: %s", fullPath.c_str());
unzCloseCurrentFile(zipfile);
unzClose(zipfile);
return false;
}
// 读取数据并写入文件
const int bufferSize = 8192;
std::vector<char> buffer(bufferSize);
int bytesRead = 0;
do {
bytesRead = unzReadCurrentFile(zipfile, buffer.data(), bufferSize);
if (bytesRead < 0) {
GakumasLocal::Log::ErrorFmt("Read File Error: %s", filePath.c_str());
fclose(outFile);
unzCloseCurrentFile(zipfile);
unzClose(zipfile);
return false;
}
if (bytesRead > 0) {
fwrite(buffer.data(), 1, bytesRead, outFile);
}
} while (bytesRead > 0);
fclose(outFile);
unzCloseCurrentFile(zipfile);
}
ret = unzGoToNextFile(zipfile);
} while (ret == UNZ_OK);
unzClose(zipfile);
return true;
}