下载二进制文件

This commit is contained in:
zqm
2026-04-08 11:29:57 +08:00
parent 07c91826d0
commit 0b931d57c9
3 changed files with 286 additions and 3 deletions

View File

@@ -6,6 +6,7 @@ version = 4
name = "Updater"
version = "0.1.0"
dependencies = [
"base64",
"chrono",
"cube_lib",
"dirs",
@@ -57,6 +58,12 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bitflags"
version = "2.11.0"

View File

@@ -10,6 +10,7 @@ dirs = "5.0"
tokio = { version = "1.37", features = ["full"] }
windows = { version = "0.56", features = ["Win32_System_Console"] }
chrono = "0.4"
base64 = "0.22"
# Local CubeLib for WebSocket
cube_lib = { path = "../../../Rust/CubeLib" }

View File

@@ -1,11 +1,255 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::fs::{self, File};
use std::io::Write as IoWrite;
use std::path::PathBuf;
use std::pin::Pin;
use std::process::Command;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use cube_lib::websocket::{WebSocketClient, WebSocketConfig};
// ===================== 文件下载状态 =====================
struct DownloadState {
filename: String,
offset: u64,
temp_path: Option<PathBuf>,
file: Option<File>,
}
impl Default for DownloadState {
fn default() -> Self {
Self {
filename: String::new(),
offset: 0,
temp_path: None,
file: None,
}
}
}
static DOWNLOAD_STATE: std::sync::Mutex<DownloadState> = std::sync::Mutex::new(DownloadState {
filename: String::new(),
offset: 0,
temp_path: None,
file: None,
});
// ===================== 版本比较与下载 =====================
/// 获取本地文件版本号(使用 PowerShell 获取 PE 文件版本信息)
fn get_local_file_version(filename: &str) -> String {
let exe_path = std::env::current_exe().expect("Failed to get executable path");
let base_dir = exe_path.parent().unwrap_or(std::path::Path::new(""));
let file_path = base_dir.join(filename);
if !file_path.exists() {
return "0.0.0".to_string();
}
#[cfg(windows)]
{
let path_str = file_path.to_string_lossy().to_string();
let ps_script = format!(
"(Get-Item -LiteralPath '{}' -ErrorAction SilentlyContinue).VersionInfo.FileVersion",
path_str.replace("'", "''")
);
let output = Command::new("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", &ps_script])
.output();
if let Ok(output) = output {
let stdout = String::from_utf8_lossy(&output.stdout);
let version = stdout.trim();
if !version.is_empty() && version != "0" && !version.contains("没有版本") {
return version.to_string();
}
}
// 备用:使用文件修改时间作为"版本"
if let Ok(metadata) = fs::metadata(&file_path) {
use std::time::UNIX_EPOCH;
if let Ok(modified) = metadata.modified() {
let duration = modified.duration_since(UNIX_EPOCH).unwrap_or_default();
return format!("{}.{}", duration.as_secs(), duration.subsec_nanos() / 1_000_000);
}
}
"0.0.0".to_string()
}
#[cfg(not(windows))]
{
"0.0.0".to_string()
}
}
/// 比较两个版本号a < b 返回 true
fn version_less_than(a: &str, b: &str) -> bool {
let parse_v = |v: &str| -> Vec<u32> {
v.split(|c: char| !c.is_ascii_digit() && c != '.')
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse().ok())
.collect()
};
let a_parts: Vec<u32> = parse_v(a);
let b_parts: Vec<u32> = parse_v(b);
let max_len = a_parts.len().max(b_parts.len());
for i in 0..max_len {
let av = if i < a_parts.len() { a_parts[i] } else { 0 };
let bv = if i < b_parts.len() { b_parts[i] } else { 0 };
if av < bv {
return true;
}
if av > bv {
return false;
}
}
false
}
/// 发送文件下载请求(断点续传)
fn request_download(sender: &cube_lib::websocket::MessageSender, filename: &str, offset: u64, debug: bool) {
let msg_str = format!(
r#"{{"Type":"DownloadFile","Data":{{"filename":"{}","offset":{}}}}}"#,
filename, offset
);
if debug {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
println!("{} 发送消息:{}", ts, msg_str);
}
sender.send(msg_str);
}
/// 处理收到的文件块
fn handle_file_chunk(data: &serde_json::Map<std::string::String, serde_json::Value>, debug: bool) {
let filename = data.get("filename").and_then(|v| v.as_str()).unwrap_or("");
let offset = data.get("offset").and_then(|v| v.as_u64()).unwrap_or(0);
let chunk_data = data.get("data").and_then(|v| v.as_str()).unwrap_or("");
let is_last = data.get("is_last").and_then(|v| v.as_bool()).unwrap_or(false);
let mut state = DOWNLOAD_STATE.lock().unwrap();
// 如果是新文件或 offset=0重置状态
if state.filename != filename || offset == 0 {
// 关闭旧文件
if let Some(f) = state.file.take() {
drop(f);
}
// 删除旧临时文件
if let Some(ref tp) = state.temp_path {
let _ = fs::remove_file(tp);
}
// 获取可执行文件目录
let exe_path = std::env::current_exe().expect("Failed to get executable path");
let base_dir = exe_path.parent().unwrap_or(std::path::Path::new(""));
let temp_path = base_dir.join(format!("{}.tmp", filename));
// 创建临时文件(截断)
match File::create(&temp_path) {
Ok(file) => {
state.filename = filename.to_string();
state.offset = 0;
state.temp_path = Some(temp_path);
state.file = Some(file);
}
Err(_) => {
if debug {
eprintln!("[下载] 无法创建临时文件: {}", filename);
}
return;
}
}
}
// 解码并追加数据
let current_offset = state.offset;
if offset == current_offset {
if let Some(ref mut file) = state.file {
match BASE64.decode(chunk_data) {
Ok(decoded) => {
match file.write_all(&decoded) {
Ok(_) => {
let _ = file.flush();
state.offset += decoded.len() as u64;
}
Err(e) => {
if debug {
eprintln!("[下载] 写入失败: {}", e);
}
}
}
}
Err(e) => {
if debug {
eprintln!("[下载] Base64 解码失败: {}", e);
}
}
}
}
} else {
if debug {
eprintln!("[下载] 偏移不匹配: 期望 {}, 收到 {}", current_offset, offset);
}
}
// 最后一块:重命名临时文件为正式文件
if is_last {
let temp_path = state.temp_path.clone();
let final_offset = state.offset;
// 关闭文件句柄
if let Some(f) = state.file.take() {
drop(f);
}
if let Some(ref temp) = temp_path {
let exe_path = std::env::current_exe().expect("Failed to get executable path");
let base_dir = exe_path.parent().unwrap_or(std::path::Path::new(""));
let final_path = base_dir.join(filename);
// 原子重命名
if let Err(e) = fs::rename(temp, &final_path) {
if debug {
eprintln!("[下载] 重命名失败: {}", e);
}
// 尝试直接覆盖写入
let _ = fs::copy(temp, &final_path);
let _ = fs::remove_file(temp);
}
if debug {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
println!("{} [下载] {} 下载完成,共 {} 字节", ts, filename, final_offset);
}
}
// 重置状态
state.filename = String::new();
state.offset = 0;
state.temp_path = None;
state.file = None;
}
}
/// 处理下载完成消息
fn handle_download_complete(data: &serde_json::Map<std::string::String, serde_json::Value>, debug: bool) {
let filename = data.get("filename").and_then(|v| v.as_str()).unwrap_or("");
let size = data.get("size").and_then(|v| v.as_u64()).unwrap_or(0);
if debug {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
println!("{} [下载] {} 下载完成,最终大小: {} 字节", ts, filename, size);
}
let mut state = DOWNLOAD_STATE.lock().unwrap();
state.filename = String::new();
state.offset = 0;
state.temp_path = None;
state.file = None;
}
/// Updater 自身配置AppData/Updater/config.json
/// 只负责 Updater 自己的行为参数,连接地址从公共 config.json 加载
#[derive(Debug, Serialize, Deserialize)]
@@ -193,11 +437,42 @@ async fn run_updater(debug_mode: bool) {
// 处理 FileVer 响应
if msg_type == "FileVer" {
if let Some(file_versions) = data.get("Data").and_then(|d| d.get("file_versions")).and_then(|v| v.as_object()) {
for _ in file_versions {
// 静默记录版本,不输出日志
for (filename, server_ver) in file_versions {
let server_version = server_ver.as_str().unwrap_or("0.0.0");
let local_version = get_local_file_version(filename);
if debug_msg {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
println!("{} [版本] {}: 服务端={}, 本地={}", ts, filename, server_version, local_version);
}
// 比较版本:如果本地不存在或比服务端旧,则下载
let need_update = local_version == "0.0.0" || version_less_than(&local_version, server_version);
if need_update {
if debug_msg {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S%.3f");
println!("{} [升级] {} 需要更新,开始下载...", ts, filename);
}
// 从 offset 0 开始下载(断点续传逻辑在 handle_file_chunk 中处理)
request_download(&sender, filename, 0, debug_msg);
}
}
}
}
// 处理文件块
if msg_type == "FileChunk" {
if let Some(data_obj) = data.get("Data").and_then(|v| v.as_object()) {
handle_file_chunk(data_obj, debug_msg);
}
}
// 处理下载完成
if msg_type == "DownloadComplete" {
if let Some(data_obj) = data.get("Data").and_then(|v| v.as_object()) {
handle_download_complete(data_obj, debug_msg);
}
}
});
// 设置断开连接回调