下载二进制文件
This commit is contained in:
7
Windows/CS/Framework4.0/Updater/Cargo.lock
generated
7
Windows/CS/Framework4.0/Updater/Cargo.lock
generated
@@ -6,6 +6,7 @@ version = 4
|
|||||||
name = "Updater"
|
name = "Updater"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64",
|
||||||
"chrono",
|
"chrono",
|
||||||
"cube_lib",
|
"cube_lib",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -57,6 +58,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.11.0"
|
version = "2.11.0"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ dirs = "5.0"
|
|||||||
tokio = { version = "1.37", features = ["full"] }
|
tokio = { version = "1.37", features = ["full"] }
|
||||||
windows = { version = "0.56", features = ["Win32_System_Console"] }
|
windows = { version = "0.56", features = ["Win32_System_Console"] }
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
# Local CubeLib for WebSocket
|
# Local CubeLib for WebSocket
|
||||||
cube_lib = { path = "../../../Rust/CubeLib" }
|
cube_lib = { path = "../../../Rust/CubeLib" }
|
||||||
|
|||||||
@@ -1,11 +1,255 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fs;
|
use std::fs::{self, File};
|
||||||
|
use std::io::Write as IoWrite;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
use cube_lib::websocket::{WebSocketClient, WebSocketConfig};
|
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 自身配置(AppData/Updater/config.json)
|
||||||
/// 只负责 Updater 自己的行为参数,连接地址从公共 config.json 加载
|
/// 只负责 Updater 自己的行为参数,连接地址从公共 config.json 加载
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -193,11 +437,42 @@ async fn run_updater(debug_mode: bool) {
|
|||||||
// 处理 FileVer 响应
|
// 处理 FileVer 响应
|
||||||
if msg_type == "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()) {
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置断开连接回调
|
// 设置断开连接回调
|
||||||
|
|||||||
Reference in New Issue
Block a user