[fix] update setup
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use crate::steam;
|
||||
use crate::tool::updater::{check_update, download_update, install_update, UpdateInfo};
|
||||
use crate::tool::*;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
use crate::vdf::preset;
|
||||
use crate::vdf::preset::VideoConfig;
|
||||
use crate::wrap_err;
|
||||
@@ -8,12 +8,15 @@ use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use reqwest;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
use std::sync::{Arc, OnceLock};
|
||||
use tauri::path::BaseDirectory;
|
||||
use tauri::Manager;
|
||||
use tauri::{Emitter, Manager};
|
||||
use url::Url;
|
||||
use log::{debug, error, info, warn};
|
||||
|
||||
// use tauri_plugin_shell::ShellExt;
|
||||
|
||||
@@ -314,62 +317,345 @@ pub fn read_vprof_report(console_log_path: &str) -> Result<String, String> {
|
||||
|
||||
// 更新相关命令
|
||||
|
||||
/// 检查更新(支持 GitHub Release 和自定义端点)
|
||||
/// 更新信息结构(用于前端)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub version: String,
|
||||
pub notes: Option<String>,
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
// 包装结构,用于存储 update 对象和修改后的 CDN URL
|
||||
struct UpdateWithCdnUrl {
|
||||
update: tauri_plugin_updater::Update,
|
||||
cdn_url: String,
|
||||
file_path: Option<std::path::PathBuf>, // 保存下载的文件路径
|
||||
}
|
||||
|
||||
// 全局存储待安装的更新
|
||||
use std::sync::Mutex;
|
||||
static PENDING_UPDATE: OnceLock<Mutex<Option<UpdateWithCdnUrl>>> = OnceLock::new();
|
||||
|
||||
fn get_pending_update() -> &'static Mutex<Option<UpdateWithCdnUrl>> {
|
||||
PENDING_UPDATE.get_or_init(|| Mutex::new(None))
|
||||
}
|
||||
|
||||
/// 检查更新(使用官方 updater 插件)
|
||||
#[tauri::command]
|
||||
pub async fn check_app_update(
|
||||
app: tauri::AppHandle,
|
||||
endpoint: Option<String>,
|
||||
use_mirror: Option<bool>,
|
||||
include_prerelease: Option<bool>,
|
||||
) -> Result<Option<UpdateInfo>, String> {
|
||||
let current_version = app.package_info().version.to_string();
|
||||
let use_mirror = use_mirror.unwrap_or(false);
|
||||
let include_prerelease = include_prerelease.unwrap_or(false);
|
||||
info!("开始检查更新...");
|
||||
info!("include_prerelease: {:?}", include_prerelease);
|
||||
|
||||
// 构建更新端点 URL
|
||||
// Tauri updater 需要支持 {{target}} 和 {{arch}} 变量的端点
|
||||
let update_url = if let Some(custom_endpoint) = endpoint {
|
||||
info!("使用自定义端点: {}", custom_endpoint);
|
||||
custom_endpoint
|
||||
} else {
|
||||
// 使用默认的 Tauri updater 格式端点
|
||||
// 端点应该支持 {{target}} 和 {{arch}} 变量
|
||||
const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb";
|
||||
let github_repo_str = std::env::var("GITHUB_REPO").ok();
|
||||
let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO);
|
||||
|
||||
// 构建标准的 Tauri updater 端点格式
|
||||
// 端点需要返回包含 platforms 字段的 JSON
|
||||
let url = if include_prerelease.unwrap_or(false) {
|
||||
format!("https://gh-info.okk.cool/repos/{}/releases/latest/pre/tauri", github_repo)
|
||||
} else {
|
||||
format!("https://gh-info.okk.cool/repos/{}/releases/latest/tauri", github_repo)
|
||||
};
|
||||
info!("使用默认端点: {}", url);
|
||||
url
|
||||
};
|
||||
|
||||
// 从环境变量获取 GitHub 仓库信息,如果没有则使用默认值
|
||||
const DEFAULT_GITHUB_REPO: &str = "plsgo/cstb";
|
||||
let github_repo_str = std::env::var("GITHUB_REPO").ok();
|
||||
let github_repo = github_repo_str.as_deref().unwrap_or(DEFAULT_GITHUB_REPO);
|
||||
let result = wrap_err!(
|
||||
check_update(
|
||||
endpoint.as_deref(),
|
||||
¤t_version,
|
||||
use_mirror,
|
||||
Some(github_repo),
|
||||
include_prerelease
|
||||
)
|
||||
.await
|
||||
)?;
|
||||
// 构建 updater
|
||||
let mut builder = app.updater_builder();
|
||||
|
||||
// 设置端点(如果提供了自定义端点,使用自定义端点;否则使用默认端点)
|
||||
let endpoint_url = Url::parse(&update_url)
|
||||
.map_err(|e| {
|
||||
error!("解析更新端点 URL 失败: {} (URL: {})", e, update_url);
|
||||
format!("解析更新端点 URL 失败: {}", e)
|
||||
})?;
|
||||
info!("解析端点 URL 成功: {}", endpoint_url);
|
||||
|
||||
builder = builder.endpoints(vec![endpoint_url])
|
||||
.map_err(|e| {
|
||||
error!("设置更新端点失败: {}", e);
|
||||
format!("设置更新端点失败: {}", e)
|
||||
})?;
|
||||
|
||||
// 设置超时
|
||||
builder = builder.timeout(std::time::Duration::from_secs(30));
|
||||
info!("设置超时为 30 秒");
|
||||
|
||||
// 检查更新
|
||||
let update = match builder.build() {
|
||||
Ok(updater) => {
|
||||
info!("构建 updater 成功,开始检查更新...");
|
||||
updater.check().await
|
||||
},
|
||||
Err(e) => {
|
||||
error!("构建 updater 失败: {}", e);
|
||||
return Err(format!("构建 updater 失败: {}", e));
|
||||
},
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
match update {
|
||||
Ok(Some(update)) => {
|
||||
info!("发现新版本: {}", update.version);
|
||||
info!("原始下载 URL: {}", update.download_url);
|
||||
// Update 类型没有实现 Debug trait,所以不能使用 {:?} 格式化
|
||||
// 如果需要更多信息,可以单独记录各个字段
|
||||
|
||||
// 将下载 URL 替换为 CDN 链接
|
||||
let mut download_url = update.download_url.to_string();
|
||||
let original_url = download_url.clone();
|
||||
// 如果 URL 不是 CDN 链接,则在 CDN 域名后拼接原 URL
|
||||
if !download_url.contains("cdn.upup.cool") {
|
||||
download_url = format!("https://cdn.upup.cool/{}", original_url);
|
||||
info!("将下载 URL 从 {} 替换为 CDN 链接: {}", original_url, download_url);
|
||||
} else {
|
||||
info!("下载 URL 已经是 CDN 链接: {}", download_url);
|
||||
}
|
||||
|
||||
// 存储更新对象和 CDN URL 供后续使用
|
||||
let pending = get_pending_update();
|
||||
*pending.lock().unwrap() = Some(UpdateWithCdnUrl {
|
||||
update: update.clone(),
|
||||
cdn_url: download_url.clone(),
|
||||
file_path: None,
|
||||
});
|
||||
|
||||
// 转换为前端需要的格式
|
||||
let update_info = UpdateInfo {
|
||||
version: update.version.to_string(),
|
||||
notes: update.body.clone(),
|
||||
download_url: download_url.clone(),
|
||||
};
|
||||
|
||||
info!("更新信息准备完成 - 版本: {}, 下载 URL: {}", update_info.version, download_url);
|
||||
Ok(Some(update_info))
|
||||
}
|
||||
Ok(None) => {
|
||||
info!("当前已是最新版本");
|
||||
Ok(None)
|
||||
},
|
||||
Err(e) => {
|
||||
error!("检查更新失败: {}", e);
|
||||
Err(format!("检查更新失败: {}", e))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载更新
|
||||
#[tauri::command]
|
||||
pub async fn download_app_update(
|
||||
app: tauri::AppHandle,
|
||||
download_url: String,
|
||||
) -> Result<String, String> {
|
||||
// 重置取消标志
|
||||
let cancelled = get_download_cancelled();
|
||||
cancelled.store(false, Ordering::Relaxed);
|
||||
|
||||
let path = wrap_err!(download_update(&app, &download_url, cancelled).await)?;
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
) -> Result<(), String> {
|
||||
info!("开始下载更新...");
|
||||
let pending = get_pending_update();
|
||||
|
||||
// 检查是否有待下载的更新
|
||||
let has_update = {
|
||||
let update_guard = pending.lock().unwrap();
|
||||
update_guard.is_some()
|
||||
};
|
||||
|
||||
if !has_update {
|
||||
warn!("没有待下载的更新");
|
||||
return Err("没有待下载的更新".to_string());
|
||||
}
|
||||
|
||||
// 监听下载进度
|
||||
// 克隆 app_handle 用于两个闭包
|
||||
let app_handle_progress = app.clone();
|
||||
let app_handle_complete = app.clone();
|
||||
|
||||
// 在锁内获取 update 和 CDN URL,然后在锁外使用
|
||||
let cloned_data = {
|
||||
let update_guard = pending.lock().unwrap();
|
||||
if let Some(ref update_with_cdn) = *update_guard {
|
||||
info!("准备下载更新 - 版本: {}, 原始 URL: {}, CDN URL: {}",
|
||||
update_with_cdn.update.version,
|
||||
update_with_cdn.update.download_url,
|
||||
update_with_cdn.cdn_url);
|
||||
Some((update_with_cdn.update.clone(), update_with_cdn.cdn_url.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// 现在锁已经释放,可以安全地下载
|
||||
if let Some((update, cdn_url)) = cloned_data {
|
||||
info!("开始使用 CDN URL 下载更新文件: {}", cdn_url);
|
||||
|
||||
// 使用 reqwest 手动下载文件
|
||||
let client = reqwest::Client::new();
|
||||
let mut response = client
|
||||
.get(&cdn_url)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("下载更新失败: {}", e);
|
||||
format!("下载更新失败: {}", e)
|
||||
})?;
|
||||
|
||||
// 获取文件大小(用于进度计算)
|
||||
let content_length = response.content_length();
|
||||
let mut downloaded: u64 = 0;
|
||||
|
||||
// 创建临时文件
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let file_name = Path::new(update.download_url.as_str())
|
||||
.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.unwrap_or("update.exe");
|
||||
let temp_file_path = temp_dir.join(file_name);
|
||||
|
||||
info!("临时文件路径: {:?}", temp_file_path);
|
||||
|
||||
// 创建文件并写入
|
||||
let mut file = std::fs::File::create(&temp_file_path)
|
||||
.map_err(|e| {
|
||||
error!("创建临时文件失败: {}", e);
|
||||
format!("创建临时文件失败: {}", e)
|
||||
})?;
|
||||
|
||||
// 下载并写入文件,同时报告进度
|
||||
while let Some(chunk) = response.chunk().await
|
||||
.map_err(|e| {
|
||||
error!("下载更新失败: {}", e);
|
||||
format!("下载更新失败: {}", e)
|
||||
})? {
|
||||
file.write_all(&chunk)
|
||||
.map_err(|e| {
|
||||
error!("写入文件失败: {}", e);
|
||||
format!("写入文件失败: {}", e)
|
||||
})?;
|
||||
|
||||
downloaded += chunk.len() as u64;
|
||||
|
||||
// 计算并报告进度
|
||||
if let Some(total) = content_length {
|
||||
let progress = (downloaded * 100) / total;
|
||||
debug!("下载进度: {} / {} ({}%)", downloaded, total, progress);
|
||||
let _ = app_handle_progress.emit("update-download-progress", progress);
|
||||
} else {
|
||||
debug!("下载进度: {} 字节", downloaded);
|
||||
let _ = app_handle_progress.emit("update-download-progress", downloaded);
|
||||
}
|
||||
}
|
||||
|
||||
info!("文件下载完成,大小: {} 字节", downloaded);
|
||||
|
||||
// 下载完成
|
||||
let _ = app_handle_complete.emit("update-download-progress", 100u64);
|
||||
|
||||
// 注意:由于我们手动下载了文件,我们需要确保 update 对象知道文件的位置
|
||||
// 但是,update.download() 方法可能还会验证签名等,所以我们需要确保手动下载的文件也能通过验证
|
||||
// 目前,我们仍然使用原始的 update 对象,但文件已经下载到临时目录
|
||||
// 如果 update.install() 需要文件路径,我们可能需要修改它
|
||||
|
||||
// 更新存储的 update 对象,保存文件路径
|
||||
let mut update_guard = pending.lock().unwrap();
|
||||
*update_guard = Some(UpdateWithCdnUrl {
|
||||
update,
|
||||
cdn_url,
|
||||
file_path: Some(temp_file_path.clone()),
|
||||
});
|
||||
info!("更新文件下载完成并已存储,文件路径: {:?}", temp_file_path);
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
warn!("没有待下载的更新(克隆失败)");
|
||||
Err("没有待下载的更新".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消下载
|
||||
#[tauri::command]
|
||||
pub fn cancel_download_update() -> Result<(), String> {
|
||||
let cancelled = get_download_cancelled();
|
||||
cancelled.store(true, Ordering::Relaxed);
|
||||
info!("取消下载更新");
|
||||
// 官方 updater 插件没有直接的取消方法
|
||||
// 可以通过删除待安装的更新来实现
|
||||
let pending = get_pending_update();
|
||||
*pending.lock().unwrap() = None;
|
||||
info!("已清除待下载的更新");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 安装更新
|
||||
#[tauri::command]
|
||||
pub fn install_app_update(installer_path: String) -> Result<(), String> {
|
||||
wrap_err!(install_update(&installer_path))
|
||||
pub fn install_app_update(_app: tauri::AppHandle) -> Result<(), String> {
|
||||
info!("开始安装更新...");
|
||||
let pending = get_pending_update();
|
||||
let mut update_guard = pending.lock().unwrap();
|
||||
|
||||
if let Some(update_with_cdn) = update_guard.take() {
|
||||
let update = update_with_cdn.update;
|
||||
info!("准备安装更新 - 版本: {}", update.version);
|
||||
info!("下载 URL: {}", update.download_url);
|
||||
|
||||
// 使用 tauri updater 的 install API,传递已下载文件的字节内容
|
||||
// 这样可以确保 tauri updater 正确处理应用的关闭和重启逻辑
|
||||
if let Some(ref file_path) = update_with_cdn.file_path {
|
||||
if file_path.exists() {
|
||||
info!("找到下载的安装程序: {:?}", file_path);
|
||||
|
||||
// 读取文件内容
|
||||
let file_bytes = std::fs::read(file_path)
|
||||
.map_err(|e| {
|
||||
error!("读取安装文件失败: {}", e);
|
||||
format!("读取安装文件失败: {}", e)
|
||||
})?;
|
||||
|
||||
info!("读取安装文件成功,大小: {} 字节", file_bytes.len());
|
||||
|
||||
// 使用 tauri updater 的 install 方法,传递文件字节内容
|
||||
// 这样 tauri updater 可以正确处理应用的关闭和重启
|
||||
match update.install(&file_bytes) {
|
||||
Ok(_) => {
|
||||
info!("安装更新成功,应用将退出以完成安装");
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
error!("安装更新失败: {}", e);
|
||||
error!("错误详情: {:?}", e);
|
||||
let error_msg = format!("安装更新失败: {}", e);
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!("下载的安装程序不存在: {:?}", file_path);
|
||||
Err(format!("下载的安装程序不存在: {:?}", file_path))
|
||||
}
|
||||
} else {
|
||||
warn!("没有找到下载的文件路径,尝试使用 update.install() 空参数");
|
||||
// 如果没有文件路径,尝试使用 update.install() 空参数
|
||||
// 这可能会让 updater 自己下载文件
|
||||
match update.install(&[]) {
|
||||
Ok(_) => {
|
||||
info!("安装更新成功,应用将退出以完成安装");
|
||||
Ok(())
|
||||
},
|
||||
Err(e) => {
|
||||
error!("安装更新失败: {}", e);
|
||||
error!("错误详情: {:?}", e);
|
||||
let error_msg = format!("安装更新失败: {}", e);
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("没有待安装的更新");
|
||||
Err("没有待安装的更新".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 PowerShell Get-ComputerInfo 信息(异步版本)
|
||||
@@ -378,8 +664,6 @@ pub fn install_app_update(installer_path: String) -> Result<(), String> {
|
||||
pub async fn get_computer_info() -> Result<serde_json::Value, String> {
|
||||
use tokio::process::Command;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// 异步执行 PowerShell 命令获取计算机信息并转换为 JSON
|
||||
@@ -565,8 +849,6 @@ pub struct MotherboardInfo {
|
||||
pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
|
||||
use tokio::process::Command;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// 执行 PowerShell 命令获取内存信息
|
||||
@@ -657,8 +939,6 @@ pub async fn get_memory_info() -> Result<Vec<MemoryInfo>, String> {
|
||||
pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
|
||||
use tokio::process::Command;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// 执行 PowerShell 命令获取显示器信息
|
||||
@@ -823,8 +1103,6 @@ pub async fn get_monitor_info() -> Result<Vec<MonitorInfo>, String> {
|
||||
pub async fn get_motherboard_info() -> Result<MotherboardInfo, String> {
|
||||
use tokio::process::Command;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// 执行 PowerShell 命令获取主板信息
|
||||
|
||||
@@ -3,19 +3,19 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
use tauri_plugin_cli::CliExt;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use tauri_plugin_cli::CliExt;
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
|
||||
// Window Vibrancy
|
||||
#[cfg(target_os = "windows")]
|
||||
use window_vibrancy::apply_acrylic;
|
||||
#[cfg(target_os = "macos")]
|
||||
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
@@ -48,27 +48,13 @@ fn main() {
|
||||
let app_name = ctx.config().identifier.as_str();
|
||||
let store_dir = config_dir.join(app_name).join("cstb");
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(
|
||||
|app: &tauri::AppHandle, args: Vec<String>, _cwd: String| {
|
||||
// 检查是否是"更新启动"的特殊请求
|
||||
let is_update_launch = args.contains(&"--update-launch".to_string());
|
||||
let mut builder = tauri::Builder::default()
|
||||
.plugin(tauri_plugin_single_instance::init(|app, _, _| {
|
||||
let window = app.get_webview_window("main").expect("no main window");
|
||||
|
||||
if is_update_launch {
|
||||
// 若存在旧实例,强制关闭(避免残留进程阻止新实例)
|
||||
if let Some(old_window) = app.get_webview_window("main") {
|
||||
// 先尝试优雅关闭,再强制退出
|
||||
old_window.close().ok();
|
||||
app.exit(0);
|
||||
}
|
||||
} else {
|
||||
// 常规单实例逻辑:激活现有窗口
|
||||
let window = app.get_webview_window("main").expect("no main window");
|
||||
window.show().expect("can't show main window");
|
||||
window.set_focus().expect("can't focus main window");
|
||||
}
|
||||
},
|
||||
))
|
||||
window.show().expect("no main window, can't show");
|
||||
window.set_focus().expect("no main window, can't set focus")
|
||||
}))
|
||||
.plugin(tauri_plugin_deep_link::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
@@ -87,9 +73,14 @@ fn main() {
|
||||
.plugin(tauri_plugin_system_info::init())
|
||||
.plugin(tauri_plugin_cli::init())
|
||||
// .plugin(tauri_plugin_valtio::init())
|
||||
.plugin(tauri_plugin_valtio::Builder::new().path(&store_dir).build())
|
||||
// .plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.setup(move |app| {
|
||||
.plugin(tauri_plugin_valtio::Builder::new().path(&store_dir).build());
|
||||
|
||||
#[cfg(desktop)]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_updater::Builder::new().build());
|
||||
}
|
||||
|
||||
builder.setup(move |app| {
|
||||
// Get Window
|
||||
let window = app.get_webview_window("main").unwrap();
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
pub mod common;
|
||||
pub mod macros;
|
||||
pub mod powerplan;
|
||||
pub mod updater;
|
||||
// pub mod updater; // 已迁移到官方 tauri-plugin-updater
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
use anyhow::{Context, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use tauri::path::BaseDirectory;
|
||||
use tauri::{Emitter, Manager};
|
||||
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
#[cfg(windows)]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
/// 更新信息结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdateInfo {
|
||||
pub version: String,
|
||||
pub notes: Option<String>,
|
||||
pub download_url: String,
|
||||
}
|
||||
|
||||
/// gh-info API 响应结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct GhInfoApiResponse {
|
||||
repo: String,
|
||||
latest_version: String,
|
||||
changelog: Option<String>,
|
||||
published_at: String,
|
||||
#[serde(default)]
|
||||
prerelease: bool,
|
||||
attachments: serde_json::Value, // 支持两种格式: ["URL1", "URL2"] 或 [["文件名", "URL"], ...]
|
||||
}
|
||||
|
||||
/// 自定义更新服务器 API 响应结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct CustomUpdateApiResponse {
|
||||
version: String,
|
||||
notes: Option<String>,
|
||||
#[serde(rename = "pub_date")]
|
||||
pub_date: Option<String>,
|
||||
download_url: String,
|
||||
signature: Option<String>,
|
||||
platforms: Option<std::collections::HashMap<String, PlatformInfo>>,
|
||||
}
|
||||
|
||||
/// 平台特定信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PlatformInfo {
|
||||
url: String,
|
||||
signature: Option<String>,
|
||||
}
|
||||
|
||||
/// 检查更新(使用自定义 API 端点)
|
||||
pub async fn check_update(
|
||||
endpoint: Option<&str>,
|
||||
current_version: &str,
|
||||
_use_mirror: bool,
|
||||
github_repo: Option<&str>,
|
||||
include_prerelease: bool,
|
||||
) -> Result<Option<UpdateInfo>> {
|
||||
// 确定使用的 API 端点
|
||||
let api_url = if let Some(custom_endpoint) = endpoint {
|
||||
// 如果提供了自定义端点,直接使用
|
||||
custom_endpoint.to_string()
|
||||
} else {
|
||||
// 否则使用默认的 gh-info API
|
||||
let repo = github_repo.unwrap_or("plsgo/cstb");
|
||||
if include_prerelease {
|
||||
format!(
|
||||
"https://gh-info.okk.cool/repos/{}/releases/latest/pre",
|
||||
repo
|
||||
)
|
||||
} else {
|
||||
format!("https://gh-info.okk.cool/repos/{}/releases/latest", repo)
|
||||
}
|
||||
};
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.user_agent("cstb-updater/1.0")
|
||||
.build()?;
|
||||
|
||||
let response = client.get(&api_url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"API 请求失败,HTTP 状态码: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
// 获取响应文本以便尝试不同的解析方式
|
||||
let response_text = response.text().await?;
|
||||
// 关闭更新日志的打印
|
||||
// println!("[更新检查] API 响应: {}", response_text);
|
||||
|
||||
// 尝试解析为自定义更新服务器格式
|
||||
let update_info = if let Ok(custom_resp) =
|
||||
serde_json::from_str::<CustomUpdateApiResponse>(&response_text)
|
||||
{
|
||||
// 提取版本号(去掉 'v' 前缀)
|
||||
let version = custom_resp.version.trim_start_matches('v').to_string();
|
||||
|
||||
// 版本比较
|
||||
let comparison = compare_version(&version, current_version);
|
||||
if comparison > 0 {
|
||||
// 获取下载链接
|
||||
// 优先使用平台特定的链接
|
||||
let download_url = if let Some(ref platforms) = custom_resp.platforms {
|
||||
// 检测当前平台
|
||||
#[cfg(target_os = "windows")]
|
||||
let platform_key = "windows-x86_64";
|
||||
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
|
||||
let platform_key = "darwin-x86_64";
|
||||
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
|
||||
let platform_key = "darwin-aarch64";
|
||||
#[cfg(target_os = "linux")]
|
||||
let platform_key = "linux-x86_64";
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
|
||||
let platform_key = "";
|
||||
|
||||
if !platform_key.is_empty() {
|
||||
platforms
|
||||
.get(platform_key)
|
||||
.map(|p| p.url.clone())
|
||||
.unwrap_or_else(|| custom_resp.download_url.clone())
|
||||
} else {
|
||||
custom_resp.download_url.clone()
|
||||
}
|
||||
} else {
|
||||
custom_resp.download_url.clone()
|
||||
};
|
||||
|
||||
Some(UpdateInfo {
|
||||
version,
|
||||
notes: custom_resp.notes,
|
||||
download_url,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// 尝试解析为 gh-info API 格式
|
||||
let api_resp: GhInfoApiResponse = serde_json::from_str(&response_text)
|
||||
.context("解析更新 API 响应失败,既不是自定义格式也不是 gh-info 格式")?;
|
||||
|
||||
// 提取版本号(去掉 'v' 前缀)
|
||||
let version = api_resp.latest_version.trim_start_matches('v').to_string();
|
||||
|
||||
// 版本比较
|
||||
let comparison = compare_version(&version, current_version);
|
||||
|
||||
if comparison > 0 {
|
||||
// 从 attachments 中获取下载链接
|
||||
// 支持两种格式:
|
||||
// 1. 字符串数组: ["URL1", "URL2", ...]
|
||||
// 2. 嵌套数组: [["文件名", "URL"], ...]
|
||||
let download_url = extract_download_url(&api_resp.attachments)
|
||||
.ok_or_else(|| anyhow::anyhow!("未找到可下载的安装包"))?;
|
||||
|
||||
Some(UpdateInfo {
|
||||
version,
|
||||
notes: api_resp.changelog,
|
||||
download_url,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
Ok(update_info)
|
||||
}
|
||||
|
||||
/// 从 attachments 中提取下载 URL
|
||||
/// 支持两种格式:
|
||||
/// 1. 字符串数组: ["URL1", "URL2", ...] - 优先选择 .exe 或 .msi 文件
|
||||
/// 2. 嵌套数组: [["文件名", "URL"], ...] - 优先选择 .exe 或 .msi 文件
|
||||
fn extract_download_url(attachments: &serde_json::Value) -> Option<String> {
|
||||
// 尝试解析为字符串数组格式: ["URL1", "URL2", ...]
|
||||
if let Ok(urls) = serde_json::from_value::<Vec<String>>(attachments.clone()) {
|
||||
// 优先选择 .exe 或 .msi 文件
|
||||
if let Some(url) = urls
|
||||
.iter()
|
||||
.find(|url| url.ends_with(".exe") || url.ends_with(".msi"))
|
||||
{
|
||||
return Some(url.clone());
|
||||
}
|
||||
// 如果没有找到 .exe 或 .msi,使用第一个 URL
|
||||
return urls.first().cloned();
|
||||
}
|
||||
|
||||
// 尝试解析为嵌套数组格式: [["文件名", "URL"], ...]
|
||||
if let Ok(nested) = serde_json::from_value::<Vec<Vec<String>>>(attachments.clone()) {
|
||||
// 优先选择 .exe 或 .msi 文件
|
||||
if let Some(url) = nested.iter().find_map(|attachment| {
|
||||
if attachment.len() >= 2 {
|
||||
let filename = &attachment[0];
|
||||
let url = &attachment[1];
|
||||
if filename.ends_with(".exe") || filename.ends_with(".msi") {
|
||||
Some(url.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
return Some(url);
|
||||
}
|
||||
// 如果没有找到 .exe 或 .msi,使用第一个附件的 URL
|
||||
if let Some(attachment) = nested.first() {
|
||||
if attachment.len() >= 2 {
|
||||
return Some(attachment[1].clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// 改进的版本比较函数,支持预发布版本(beta.5, beta.6等)
|
||||
fn compare_version(new: &str, current: &str) -> i32 {
|
||||
// 解析版本号:支持格式如 "0.0.6-beta.5", "beta.6", "0.0.6" 等
|
||||
let (new_base, new_pre) = parse_version(new);
|
||||
let (current_base, current_pre) = parse_version(current);
|
||||
|
||||
// 先比较基础版本号(数字部分)
|
||||
let base_comparison = compare_version_parts(&new_base, ¤t_base);
|
||||
|
||||
if base_comparison != 0 {
|
||||
return base_comparison;
|
||||
}
|
||||
|
||||
// 如果基础版本相同(或都为空),比较预发布标识符
|
||||
// 如果基础版本都为空,说明是纯预发布版本(如 beta.5 vs beta.6)
|
||||
let pre_comparison = compare_prerelease(&new_pre, ¤t_pre);
|
||||
|
||||
// 如果基础版本都为空且预发布比较结果为0,说明版本完全相同
|
||||
if new_base.is_empty() && current_base.is_empty() && pre_comparison == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
pre_comparison
|
||||
}
|
||||
|
||||
/// 解析版本号,返回(基础版本号数组,预发布标识符)
|
||||
fn parse_version(version: &str) -> (Vec<u32>, Option<String>) {
|
||||
// 去掉 'v' 前缀
|
||||
let version = version.trim_start_matches('v').trim();
|
||||
|
||||
// 检查是否有预发布标识符(如 -beta.5, -alpha.1 等)
|
||||
let (base_str, pre_str) = if let Some(dash_pos) = version.find('-') {
|
||||
let (base, pre) = version.split_at(dash_pos);
|
||||
(base, Some(pre[1..].to_string())) // 跳过 '-' 字符
|
||||
} else {
|
||||
(version, None)
|
||||
};
|
||||
|
||||
// 解析基础版本号(数字部分)
|
||||
let base_parts: Vec<u32> = base_str.split('.').filter_map(|s| s.parse().ok()).collect();
|
||||
|
||||
// 如果基础版本号为空且没有预发布标识符,可能是纯预发布版本(如 "beta.5")
|
||||
// 这种情况下,整个字符串作为预发布标识符
|
||||
if base_parts.is_empty() && pre_str.is_none() {
|
||||
// 检查是否包含非数字字符(可能是预发布版本)
|
||||
if !version.chars().any(|c| c.is_ascii_digit()) {
|
||||
return (vec![], Some(version.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
(base_parts, pre_str)
|
||||
}
|
||||
|
||||
/// 比较版本号数组(数字部分)
|
||||
fn compare_version_parts(new: &[u32], current: &[u32]) -> i32 {
|
||||
let max_len = new.len().max(current.len());
|
||||
|
||||
for i in 0..max_len {
|
||||
let new_val = new.get(i).copied().unwrap_or(0);
|
||||
let current_val = current.get(i).copied().unwrap_or(0);
|
||||
|
||||
if new_val > current_val {
|
||||
return 1;
|
||||
} else if new_val < current_val {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
0
|
||||
}
|
||||
|
||||
/// 比较预发布标识符
|
||||
/// 规则:
|
||||
/// - 有预发布标识符的版本 < 没有预发布标识符的版本
|
||||
/// - 如果都有预发布标识符,按字典序比较
|
||||
fn compare_prerelease(new: &Option<String>, current: &Option<String>) -> i32 {
|
||||
match (new, current) {
|
||||
// 都没有预发布标识符,版本相同
|
||||
(None, None) => 0,
|
||||
// 新版本有预发布,当前版本没有 -> 新版本更旧(预发布版本 < 正式版本)
|
||||
(Some(_), None) => -1,
|
||||
// 新版本没有预发布,当前版本有 -> 新版本更新
|
||||
(None, Some(_)) => 1,
|
||||
// 都有预发布标识符,按字典序比较
|
||||
(Some(new_pre), Some(current_pre)) => {
|
||||
// 尝试提取数字部分进行比较(如 beta.5 -> 5, beta.6 -> 6)
|
||||
let new_num = extract_number_from_prerelease(new_pre);
|
||||
let current_num = extract_number_from_prerelease(current_pre);
|
||||
|
||||
if let (Some(new_n), Some(current_n)) = (new_num, current_num) {
|
||||
// 如果都能提取数字,比较数字
|
||||
if new_n > current_n {
|
||||
1
|
||||
} else if new_n < current_n {
|
||||
-1
|
||||
} else {
|
||||
// 数字相同,按字符串比较
|
||||
new_pre.cmp(current_pre) as i32
|
||||
}
|
||||
} else {
|
||||
// 无法提取数字,按字符串比较
|
||||
new_pre.cmp(current_pre) as i32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 从预发布标识符中提取数字(如 "beta.5" -> 5, "alpha.1" -> 1)
|
||||
fn extract_number_from_prerelease(pre: &str) -> Option<u32> {
|
||||
// 尝试从最后一部分提取数字
|
||||
if let Some(last_part) = pre.split('.').last() {
|
||||
last_part.parse().ok()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// 下载更新文件(带进度追踪和取消支持)
|
||||
pub async fn download_update(
|
||||
app: &tauri::AppHandle,
|
||||
download_url: &str,
|
||||
cancelled: Arc<AtomicBool>,
|
||||
) -> Result<PathBuf> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(300))
|
||||
.build()?;
|
||||
|
||||
let response = client.get(download_url).send().await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"下载失败,HTTP 状态码: {}",
|
||||
response.status()
|
||||
));
|
||||
}
|
||||
|
||||
// 获取文件总大小
|
||||
let total_size = response.content_length().unwrap_or(0);
|
||||
|
||||
// 获取缓存目录
|
||||
let cache_dir = app
|
||||
.path()
|
||||
.resolve("updates", BaseDirectory::AppCache)
|
||||
.context("无法获取缓存目录")?;
|
||||
|
||||
fs::create_dir_all(&cache_dir)?;
|
||||
|
||||
// 从 URL 中提取文件名
|
||||
let filename = download_url
|
||||
.split('/')
|
||||
.last()
|
||||
.unwrap_or("update")
|
||||
.split('?')
|
||||
.next()
|
||||
.unwrap_or("update");
|
||||
|
||||
let file_path = cache_dir.join(filename);
|
||||
|
||||
// 下载文件
|
||||
let mut file = fs::File::create(&file_path)?;
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut downloaded: u64 = 0;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use std::io::Write;
|
||||
|
||||
while let Some(item) = stream.next().await {
|
||||
// 检查是否取消
|
||||
if cancelled.load(Ordering::Relaxed) {
|
||||
// 删除部分下载的文件
|
||||
let _ = fs::remove_file(&file_path);
|
||||
return Err(anyhow::anyhow!("下载已取消"));
|
||||
}
|
||||
|
||||
let chunk = item?;
|
||||
file.write_all(&chunk)?;
|
||||
downloaded += chunk.len() as u64;
|
||||
|
||||
// 发送进度事件
|
||||
if total_size > 0 {
|
||||
let progress = (downloaded * 100) / total_size;
|
||||
let _ = app.emit("update-download-progress", progress);
|
||||
} else {
|
||||
// 如果无法获取总大小,发送已下载的字节数
|
||||
let _ = app.emit("update-download-progress", downloaded);
|
||||
}
|
||||
}
|
||||
|
||||
file.sync_all()?;
|
||||
|
||||
// 发送完成事件
|
||||
let _ = app.emit("update-download-progress", 100u64);
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
/// 安装更新(Windows)
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn install_update(installer_path: &str) -> Result<()> {
|
||||
// 使用 /S 静默安装
|
||||
let mut cmd = Command::new(installer_path);
|
||||
cmd.args(&["/S", "/appParam=\"--update-launch\""]); // 静默安装
|
||||
cmd.creation_flags(CREATE_NO_WINDOW);
|
||||
let mut child = cmd.spawn()?;
|
||||
// 等待安装程序完成
|
||||
child.wait()?;
|
||||
// 安装完成后,由前端调用 relaunch() 来启动新版本
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 安装更新(macOS)
|
||||
#[cfg(target_os = "macos")]
|
||||
pub fn install_update(installer_path: &str) -> Result<()> {
|
||||
let mut child = Command::new("open").arg(installer_path).spawn()?;
|
||||
// 等待安装程序完成
|
||||
child.wait()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 安装更新(Linux)
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn install_update(installer_path: &str) -> Result<()> {
|
||||
if installer_path.ends_with(".deb") {
|
||||
let mut child = Command::new("sudo")
|
||||
.args(&["dpkg", "-i", installer_path])
|
||||
.spawn()?;
|
||||
child.wait()?;
|
||||
} else if installer_path.ends_with(".AppImage") {
|
||||
let mut child = Command::new("chmod")
|
||||
.args(&["+x", installer_path])
|
||||
.spawn()?;
|
||||
child.wait()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user