Skip to content

Commit 6ff2ef5

Browse files
wip: 测试自动更新
1 parent 5df4e81 commit 6ff2ef5

13 files changed

Lines changed: 662 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<h3>🎵 Majdata Hub</h3>
77
<p>一个用于管理 MajdataPlay 谱面和皮肤的桌面应用</p>
88

9-
![Version](https://img.shields.io/badge/version-0.5.2-blue.svg)
9+
![Version](https://img.shields.io/badge/version-0.5.3-blue.svg)
1010
![License](https://img.shields.io/badge/license-MIT-green.svg)
1111
![Rust](https://img.shields.io/badge/Rust-Server-orange.svg)
1212
![React](https://img.shields.io/badge/React-19-61dafb.svg)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "majdata-hub",
33
"private": true,
4-
"version": "0.5.2",
4+
"version": "0.5.3",
55
"type": "module",
66
"scripts": {
77
"dev": "concurrently \"pnpm vite:dev\" \"cargo run --manifest-path src-server/Cargo.toml\"",

src-server/Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-server/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "majdata-hub"
3-
version = "0.5.2"
3+
version = "0.5.3"
44
description = "Majdata Hub - localhost server"
55
authors = ["KirisameVanilla"]
66
edition = "2021"
@@ -42,3 +42,4 @@ rust-embed = "8"
4242
mime_guess = "2"
4343
open = "5"
4444
rfd = "0.14"
45+
semver = "1"

src-server/src/commands/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pub mod checksum;
33
pub mod file_system;
44
pub mod network;
5+
pub mod updater;
56
pub mod zip;
67

78
// 重新导出所有命令函数
@@ -16,4 +17,5 @@ pub use network::{
1617
clear_api_cache, download_charts_batch, download_file_to_path, download_skin_zip,
1718
fetch_chart_list, fetch_github_skins, fetch_remote_hashes,
1819
};
20+
pub use updater::{apply_update, check_for_update, cleanup_update_files, download_update, UpdateInfo};
1921
pub use zip::{download_and_extract, extract_zip};

src-server/src/commands/updater.rs

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
use crate::sse::UpdateProgress;
2+
use futures_util::StreamExt;
3+
use serde::{Deserialize, Serialize};
4+
use tokio::io::AsyncWriteExt;
5+
6+
/// GitHub Releases API 响应
7+
#[derive(Debug, Clone, Serialize, Deserialize)]
8+
pub struct GithubRelease {
9+
pub tag_name: String,
10+
pub body: Option<String>,
11+
pub assets: Vec<GithubAsset>,
12+
}
13+
14+
#[derive(Debug, Clone, Serialize, Deserialize)]
15+
pub struct GithubAsset {
16+
pub name: String,
17+
pub browser_download_url: String,
18+
pub size: u64,
19+
}
20+
21+
/// 版本检查响应
22+
#[derive(Debug, Clone, Serialize)]
23+
#[serde(rename_all = "camelCase")]
24+
pub struct UpdateInfo {
25+
pub has_update: bool,
26+
pub current_version: String,
27+
pub latest_version: String,
28+
pub release_notes: Option<String>,
29+
pub download_url_github: Option<String>,
30+
pub download_url_cnb: Option<String>,
31+
pub file_size: u64,
32+
}
33+
34+
const GITHUB_API_URL: &str =
35+
"https://api.github.com/repos/KirisameVanilla/MajdataHub/releases/latest";
36+
const CNB_BASE_URL: &str = "https://cnb.cool/TeamMajdata/MajdataHub-Build/-/git/raw/main/";
37+
38+
/// 检查是否有更新
39+
pub async fn check_for_update(proxy: Option<String>) -> Result<UpdateInfo, String> {
40+
let client = super::network::create_http_client(proxy)?;
41+
42+
let response = client
43+
.get(GITHUB_API_URL)
44+
.header("User-Agent", "MajdataHub-Updater")
45+
.send()
46+
.await
47+
.map_err(|e| format!("检查更新失败: {}", e))?;
48+
49+
if !response.status().is_success() {
50+
return Err(format!("GitHub API 返回错误: {}", response.status()));
51+
}
52+
53+
let release: GithubRelease = response
54+
.json()
55+
.await
56+
.map_err(|e| format!("解析 GitHub 响应失败: {}", e))?;
57+
58+
// 从 tag_name 提取版本号(去掉前缀 v)
59+
let latest_version = release
60+
.tag_name
61+
.strip_prefix('v')
62+
.unwrap_or(&release.tag_name)
63+
.to_string();
64+
65+
// env!("CARGO_PKG_VERSION")
66+
let current_version = env!("CARGO_PKG_VERSION").to_string();
67+
68+
// 用 semver 比较版本
69+
let current = semver::Version::parse(&current_version)
70+
.map_err(|e| format!("当前版本号解析失败: {}", e))?;
71+
let latest = semver::Version::parse(&latest_version)
72+
.map_err(|e| format!("远程版本号解析失败: {}", e))?;
73+
74+
let has_update = latest > current;
75+
76+
// 查找 exe 资产
77+
let asset_name = format!("majdata-hub-v{}.exe", latest_version);
78+
let asset = release.assets.iter().find(|a| a.name == asset_name);
79+
80+
let (download_url_github, file_size) = match asset {
81+
Some(a) => (Some(a.browser_download_url.clone()), a.size),
82+
None => (None, 0),
83+
};
84+
85+
// 构造 CNB URL
86+
let download_url_cnb = if has_update {
87+
Some(format!(
88+
"{}majdata-hub-v{}.exe?download=true",
89+
CNB_BASE_URL, latest_version
90+
))
91+
} else {
92+
None
93+
};
94+
95+
tracing::info!(
96+
"更新检查: 当前={}, 最新={}, 有更新={}",
97+
current_version,
98+
latest_version,
99+
has_update
100+
);
101+
102+
Ok(UpdateInfo {
103+
has_update,
104+
current_version,
105+
latest_version,
106+
release_notes: release.body,
107+
download_url_github,
108+
download_url_cnb,
109+
file_size,
110+
})
111+
}
112+
113+
/// 下载更新到 majdata-hub.new.exe
114+
pub async fn download_update(
115+
download_url: String,
116+
proxy: Option<String>,
117+
) -> Result<String, String> {
118+
let exe_dir = std::env::current_exe()
119+
.map_err(|e| format!("获取 exe 路径失败: {}", e))?
120+
.parent()
121+
.ok_or("无法确定 exe 所在目录")?
122+
.to_path_buf();
123+
124+
let output_path = exe_dir.join("majdata-hub.new.exe");
125+
let tx = crate::sse::get_update_progress_tx();
126+
127+
let _ = tx.send(UpdateProgress {
128+
downloaded: 0,
129+
total: None,
130+
speed: 0.0,
131+
status: "downloading".into(),
132+
error: None,
133+
});
134+
135+
let client = super::network::create_http_client(proxy)?;
136+
let response = client.get(&download_url).send().await.map_err(|e| {
137+
let _ = tx.send(UpdateProgress {
138+
downloaded: 0,
139+
total: None,
140+
speed: 0.0,
141+
status: "failed".into(),
142+
error: Some(format!("{}", e)),
143+
});
144+
format!("下载失败: {}", e)
145+
})?;
146+
147+
if !response.status().is_success() {
148+
let _ = tx.send(UpdateProgress {
149+
downloaded: 0,
150+
total: None,
151+
speed: 0.0,
152+
status: "failed".into(),
153+
error: Some(format!("HTTP {}", response.status())),
154+
});
155+
return Err(format!("下载失败,HTTP 状态码: {}", response.status()));
156+
}
157+
158+
let total_size = response.content_length();
159+
let mut downloaded: u64 = 0;
160+
let mut file = tokio::fs::File::create(&output_path)
161+
.await
162+
.map_err(|e| format!("无法创建文件: {}", e))?;
163+
164+
let mut stream = response.bytes_stream();
165+
let mut last_emit = std::time::Instant::now();
166+
let mut last_downloaded: u64 = 0;
167+
168+
while let Some(chunk) = stream.next().await {
169+
let chunk = chunk.map_err(|e| {
170+
let _ = tx.send(UpdateProgress {
171+
downloaded,
172+
total: total_size,
173+
speed: 0.0,
174+
status: "failed".into(),
175+
error: Some(format!("{}", e)),
176+
});
177+
format!("流读取错误: {}", e)
178+
})?;
179+
file.write_all(&chunk)
180+
.await
181+
.map_err(|e| format!("写入文件错误: {}", e))?;
182+
downloaded += chunk.len() as u64;
183+
184+
let now = std::time::Instant::now();
185+
if now.duration_since(last_emit).as_millis() >= 200 {
186+
let elapsed = now.duration_since(last_emit).as_secs_f64();
187+
let speed = if elapsed > 0.0 {
188+
(downloaded - last_downloaded) as f64 / elapsed
189+
} else {
190+
0.0
191+
};
192+
last_downloaded = downloaded;
193+
last_emit = now;
194+
let _ = tx.send(UpdateProgress {
195+
downloaded,
196+
total: total_size,
197+
speed,
198+
status: "downloading".into(),
199+
error: None,
200+
});
201+
}
202+
}
203+
204+
file.flush()
205+
.await
206+
.map_err(|e| format!("刷新文件缓冲失败: {}", e))?;
207+
drop(file);
208+
209+
let _ = tx.send(UpdateProgress {
210+
downloaded,
211+
total: total_size,
212+
speed: 0.0,
213+
status: "ready".into(),
214+
error: None,
215+
});
216+
217+
tracing::info!("更新下载完成: {} 字节", downloaded);
218+
Ok(output_path.to_string_lossy().to_string())
219+
}
220+
221+
/// 应用更新:创建 bat 脚本替换 exe 并退出
222+
pub fn apply_update() -> Result<(), String> {
223+
let exe_path = std::env::current_exe().map_err(|e| format!("获取 exe 路径失败: {}", e))?;
224+
let exe_dir = exe_path.parent().ok_or("无法确定 exe 所在目录")?;
225+
226+
let new_exe = exe_dir.join("majdata-hub.new.exe");
227+
if !new_exe.exists() {
228+
return Err("更新文件不存在".to_string());
229+
}
230+
231+
let exe_name = exe_path
232+
.file_name()
233+
.ok_or("无法确定 exe 文件名")?
234+
.to_string_lossy()
235+
.to_string();
236+
237+
// 使用 %~dp0 (bat 脚本自身所在目录) 引用文件路径,比硬编码绝对路径更可靠
238+
let script_content = format!(
239+
r#"@echo off
240+
cd /d "%~dp0"
241+
242+
echo ==== BAT START ====
243+
244+
timeout /t 2
245+
246+
:waitloop
247+
echo TRY COPY
248+
copy /Y "%~dp0majdata-hub.new.exe" "%~dp0{exe_name}"
249+
250+
if errorlevel 1 (
251+
echo COPY FAILED: %errorlevel%
252+
pause
253+
timeout /t 1
254+
goto waitloop
255+
)
256+
257+
echo COPY SUCCESS
258+
259+
del "%~dp0majdata-hub.new.exe"
260+
start "" "%~dp0{exe_name}"
261+
262+
echo DONE
263+
"#,
264+
exe_name = exe_name,
265+
);
266+
267+
let script_path = exe_dir.join("apply_update.bat");
268+
std::fs::write(&script_path, &script_content)
269+
.map_err(|e| format!("写入更新脚本失败: {}", e))?;
270+
271+
// 启动 bat 脚本(分离进程,无窗口)
272+
#[cfg(target_os = "windows")]
273+
{
274+
tracing::info!("启动 bat 脚本应用更新: {:?}", script_path);
275+
use std::os::windows::process::CommandExt;
276+
const CREATE_NEW_CONSOLE: u32 = 0x00000010;
277+
std::process::Command::new("cmd.exe")
278+
.args(["/C", "start", "", script_path.to_str().unwrap()])
279+
.creation_flags(CREATE_NEW_CONSOLE)
280+
.spawn()
281+
.map_err(|e| format!("启动更新脚本失败: {}", e))?;
282+
}
283+
284+
tracing::info!("更新已应用,正在退出以完成替换...");
285+
std::process::exit(0);
286+
}
287+
288+
/// 清理上次更新残留文件
289+
pub fn cleanup_update_files() -> Result<(), String> {
290+
let exe_dir = std::env::current_exe()
291+
.map_err(|e| format!("获取 exe 路径失败: {}", e))?
292+
.parent()
293+
.ok_or("无法确定 exe 所在目录")?
294+
.to_path_buf();
295+
296+
let new_exe = exe_dir.join("majdata-hub.new.exe");
297+
if new_exe.exists() {
298+
tracing::info!("清理残留更新文件: {:?}", new_exe);
299+
let _ = std::fs::remove_file(&new_exe);
300+
}
301+
302+
let bat = exe_dir.join("apply_update.bat");
303+
if bat.exists() {
304+
tracing::info!("清理残留更新脚本: {:?}", bat);
305+
let _ = std::fs::remove_file(&bat);
306+
}
307+
308+
Ok(())
309+
}

src-server/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ pub fn run() {
8585

8686
tracing::info!("启动 Majdata Hub 服务器");
8787

88+
// 清理上次更新残留文件
89+
let _ = commands::cleanup_update_files();
90+
8891
let rt = tokio::runtime::Builder::new_multi_thread()
8992
.enable_all()
9093
.build()

0 commit comments

Comments
 (0)