feat(desktop): stream download progress and auto-launch installer
This commit is contained in:
@@ -7,7 +7,9 @@ use std::fs;
|
||||
use std::io::Write;
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{Duration, UNIX_EPOCH};
|
||||
use std::process::Command;
|
||||
use std::time::{Duration, Instant, UNIX_EPOCH};
|
||||
use tauri::Emitter;
|
||||
|
||||
struct ApiState {
|
||||
client: reqwest::Client,
|
||||
@@ -21,6 +23,47 @@ struct BridgeResponse {
|
||||
data: Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct NativeDownloadProgressPayload {
|
||||
task_id: String,
|
||||
downloaded_bytes: u64,
|
||||
total_bytes: Option<u64>,
|
||||
progress: Option<f64>,
|
||||
resumed_bytes: u64,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
fn emit_native_download_progress(
|
||||
window: &tauri::WebviewWindow,
|
||||
task_id: &str,
|
||||
downloaded_bytes: u64,
|
||||
total_bytes: Option<u64>,
|
||||
resumed_bytes: u64,
|
||||
done: bool,
|
||||
) {
|
||||
if task_id.trim().is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let progress = total_bytes
|
||||
.filter(|total| *total > 0)
|
||||
.map(|total| (downloaded_bytes as f64 / total as f64) * 100.0);
|
||||
|
||||
let payload = NativeDownloadProgressPayload {
|
||||
task_id: task_id.to_string(),
|
||||
downloaded_bytes,
|
||||
total_bytes,
|
||||
progress,
|
||||
resumed_bytes,
|
||||
done,
|
||||
};
|
||||
|
||||
if let Err(err) = window.emit("native-download-progress", payload) {
|
||||
eprintln!("emit native-download-progress failed: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_base_url(base_url: &str) -> String {
|
||||
let trimmed = base_url.trim();
|
||||
if trimmed.is_empty() {
|
||||
@@ -550,8 +593,10 @@ async fn api_create_direct_link(
|
||||
#[tauri::command]
|
||||
async fn api_native_download(
|
||||
state: tauri::State<'_, ApiState>,
|
||||
window: tauri::WebviewWindow,
|
||||
url: String,
|
||||
file_name: Option<String>,
|
||||
task_id: Option<String>,
|
||||
) -> Result<BridgeResponse, String> {
|
||||
let trimmed_url = url.trim().to_string();
|
||||
if trimmed_url.is_empty() {
|
||||
@@ -595,6 +640,16 @@ async fn api_native_download(
|
||||
let save_path = alloc_download_path(&download_dir, preferred_name);
|
||||
fs::rename(&resume_temp_path, &save_path)
|
||||
.map_err(|err| format!("完成断点下载失败: {}", err))?;
|
||||
if let Some(ref id) = task_id {
|
||||
emit_native_download_progress(
|
||||
&window,
|
||||
id,
|
||||
existing_size,
|
||||
Some(existing_size),
|
||||
existing_size,
|
||||
true,
|
||||
);
|
||||
}
|
||||
let mut data = Map::new();
|
||||
data.insert("success".to_string(), Value::Bool(true));
|
||||
data.insert(
|
||||
@@ -625,6 +680,26 @@ async fn api_native_download(
|
||||
}
|
||||
|
||||
let append_mode = existing_size > 0 && status == reqwest::StatusCode::PARTIAL_CONTENT;
|
||||
let total_bytes = if append_mode {
|
||||
response
|
||||
.content_length()
|
||||
.map(|remaining| remaining.saturating_add(existing_size))
|
||||
} else {
|
||||
response.content_length()
|
||||
};
|
||||
let resumed_bytes = if append_mode { existing_size } else { 0 };
|
||||
|
||||
if let Some(ref id) = task_id {
|
||||
emit_native_download_progress(
|
||||
&window,
|
||||
id,
|
||||
if append_mode { existing_size } else { 0 },
|
||||
total_bytes,
|
||||
resumed_bytes,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
if !append_mode && resume_temp_path.exists() {
|
||||
fs::remove_file(&resume_temp_path)
|
||||
.map_err(|err| format!("重置断点下载文件失败: {}", err))?;
|
||||
@@ -640,6 +715,7 @@ async fn api_native_download(
|
||||
|
||||
let mut downloaded_bytes: u64 = if append_mode { existing_size } else { 0 };
|
||||
let mut stream = response;
|
||||
let mut last_emit = Instant::now();
|
||||
while let Some(chunk) = stream
|
||||
.chunk()
|
||||
.await
|
||||
@@ -649,6 +725,20 @@ async fn api_native_download(
|
||||
.write_all(&chunk)
|
||||
.map_err(|err| format!("写入文件失败: {}", err))?;
|
||||
downloaded_bytes += chunk.len() as u64;
|
||||
|
||||
if let Some(ref id) = task_id {
|
||||
if last_emit.elapsed() >= Duration::from_millis(120) {
|
||||
emit_native_download_progress(
|
||||
&window,
|
||||
id,
|
||||
downloaded_bytes,
|
||||
total_bytes,
|
||||
resumed_bytes,
|
||||
false,
|
||||
);
|
||||
last_emit = Instant::now();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target_file
|
||||
@@ -659,6 +749,17 @@ async fn api_native_download(
|
||||
fs::rename(&resume_temp_path, &save_path)
|
||||
.map_err(|err| format!("保存下载文件失败: {}", err))?;
|
||||
|
||||
if let Some(ref id) = task_id {
|
||||
emit_native_download_progress(
|
||||
&window,
|
||||
id,
|
||||
downloaded_bytes,
|
||||
total_bytes.or(Some(downloaded_bytes)),
|
||||
resumed_bytes,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
let mut data = Map::new();
|
||||
data.insert("success".to_string(), Value::Bool(true));
|
||||
data.insert(
|
||||
@@ -681,6 +782,44 @@ async fn api_native_download(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn api_launch_installer(installer_path: String) -> Result<BridgeResponse, String> {
|
||||
let path_text = installer_path.trim().to_string();
|
||||
if path_text.is_empty() {
|
||||
return Err("安装包路径不能为空".to_string());
|
||||
}
|
||||
|
||||
let installer = PathBuf::from(&path_text);
|
||||
if !installer.exists() {
|
||||
return Err("安装包不存在,请重新下载".to_string());
|
||||
}
|
||||
if !installer.is_file() {
|
||||
return Err("安装包路径无效".to_string());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let spawn_result = Command::new(&installer).spawn();
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
let spawn_result = Command::new("open").arg(&installer).spawn();
|
||||
|
||||
#[cfg(all(not(target_os = "windows"), not(target_os = "macos")))]
|
||||
let spawn_result = Command::new("xdg-open").arg(&installer).spawn();
|
||||
|
||||
spawn_result.map_err(|err| format!("启动安装程序失败: {}", err))?;
|
||||
|
||||
let mut data = Map::new();
|
||||
data.insert("success".to_string(), Value::Bool(true));
|
||||
data.insert("message".to_string(), Value::String("安装程序已启动".to_string()));
|
||||
data.insert("installerPath".to_string(), Value::String(path_text));
|
||||
|
||||
Ok(BridgeResponse {
|
||||
ok: true,
|
||||
status: 200,
|
||||
data: Value::Object(data),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn api_check_client_update(
|
||||
state: tauri::State<'_, ApiState>,
|
||||
@@ -1060,6 +1199,7 @@ pub fn run() {
|
||||
api_delete_share,
|
||||
api_create_direct_link,
|
||||
api_native_download,
|
||||
api_launch_installer,
|
||||
api_check_client_update,
|
||||
api_list_local_files,
|
||||
api_upload_file_resumable,
|
||||
|
||||
Reference in New Issue
Block a user