|
|
@@ -1,8 +1,148 @@
|
|
|
use crate::commands::AppState;
|
|
|
use crate::utils::shell::{run_program, run_shell_hidden, CommandOptions};
|
|
|
+use futures_util::StreamExt;
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
+use std::io::Write;
|
|
|
+use std::path::Path;
|
|
|
use tauri::{Emitter, State};
|
|
|
|
|
|
+/// 下载文件并报告进度(支持断点续传)
|
|
|
+/// progress_callback: (downloaded_bytes, total_bytes, percent)
|
|
|
+async fn download_file_with_progress<F>(
|
|
|
+ url: &str,
|
|
|
+ dest_path: &Path,
|
|
|
+ mut progress_callback: F,
|
|
|
+) -> Result<(), String>
|
|
|
+where
|
|
|
+ F: FnMut(u64, u64, f64),
|
|
|
+{
|
|
|
+ // 临时文件路径(用于断点续传)
|
|
|
+ let temp_path = dest_path.with_extension(
|
|
|
+ dest_path
|
|
|
+ .extension()
|
|
|
+ .map(|e| format!("{}.downloading", e.to_string_lossy()))
|
|
|
+ .unwrap_or_else(|| "downloading".to_string()),
|
|
|
+ );
|
|
|
+
|
|
|
+ // 获取已下载的文件大小
|
|
|
+ let downloaded_size = if temp_path.exists() {
|
|
|
+ std::fs::metadata(&temp_path)
|
|
|
+ .map(|m| m.len())
|
|
|
+ .unwrap_or(0)
|
|
|
+ } else {
|
|
|
+ 0
|
|
|
+ };
|
|
|
+
|
|
|
+ let client = reqwest::Client::new();
|
|
|
+
|
|
|
+ // 构建请求,支持断点续传
|
|
|
+ let mut request = client.get(url);
|
|
|
+ if downloaded_size > 0 {
|
|
|
+ request = request.header("Range", format!("bytes={}-", downloaded_size));
|
|
|
+ log::info!(
|
|
|
+ "断点续传: 从 {:.1}MB 处继续下载",
|
|
|
+ downloaded_size as f64 / 1024.0 / 1024.0
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ let response = request
|
|
|
+ .send()
|
|
|
+ .await
|
|
|
+ .map_err(|e| format!("Failed to send request: {}", e))?;
|
|
|
+
|
|
|
+ let status = response.status();
|
|
|
+
|
|
|
+ // 处理 HTTP 错误状态码
|
|
|
+ if status.is_client_error() || status.is_server_error() {
|
|
|
+ // 416 表示 Range 请求无效(可能文件已完成或服务器不支持)
|
|
|
+ if status.as_u16() == 416 && downloaded_size > 0 {
|
|
|
+ log::info!("文件可能已下载完成,尝试验证...");
|
|
|
+ if temp_path.exists() {
|
|
|
+ if dest_path.exists() {
|
|
|
+ let _ = std::fs::remove_file(dest_path);
|
|
|
+ }
|
|
|
+ std::fs::rename(&temp_path, dest_path)
|
|
|
+ .map_err(|e| format!("Failed to rename file: {}", e))?;
|
|
|
+ return Ok(());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return Err(format!("HTTP {}: 下载失败", status.as_u16()));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 206 表示部分内容(断点续传成功)
|
|
|
+ let is_partial_content = status.as_u16() == 206;
|
|
|
+
|
|
|
+ // 计算总大小
|
|
|
+ let total_size = if is_partial_content {
|
|
|
+ // 从 Content-Range 头获取总大小: bytes 0-999/1000
|
|
|
+ response
|
|
|
+ .headers()
|
|
|
+ .get("content-range")
|
|
|
+ .and_then(|v| v.to_str().ok())
|
|
|
+ .and_then(|s| s.rsplit('/').next())
|
|
|
+ .and_then(|s| s.parse::<u64>().ok())
|
|
|
+ .unwrap_or(0)
|
|
|
+ } else {
|
|
|
+ // 新下载,获取 Content-Length
|
|
|
+ let content_length = response.content_length().unwrap_or(0);
|
|
|
+ // 如果是新下载但存在临时文件,说明服务器不支持断点续传,删除重新下载
|
|
|
+ if downloaded_size > 0 {
|
|
|
+ log::info!("服务器不支持断点续传,重新下载");
|
|
|
+ let _ = std::fs::remove_file(&temp_path);
|
|
|
+ }
|
|
|
+ content_length
|
|
|
+ };
|
|
|
+
|
|
|
+ // 以追加模式打开文件(断点续传)或创建新文件
|
|
|
+ let mut file = std::fs::OpenOptions::new()
|
|
|
+ .write(true)
|
|
|
+ .create(true)
|
|
|
+ .append(is_partial_content)
|
|
|
+ .truncate(!is_partial_content)
|
|
|
+ .open(&temp_path)
|
|
|
+ .map_err(|e| format!("Failed to open file: {}", e))?;
|
|
|
+
|
|
|
+ let mut current_downloaded = if is_partial_content { downloaded_size } else { 0 };
|
|
|
+ let mut stream = response.bytes_stream();
|
|
|
+
|
|
|
+ while let Some(chunk_result) = stream.next().await {
|
|
|
+ let chunk = chunk_result.map_err(|e| format!("Failed to read chunk: {}", e))?;
|
|
|
+ file.write_all(&chunk)
|
|
|
+ .map_err(|e| format!("Failed to write chunk: {}", e))?;
|
|
|
+
|
|
|
+ current_downloaded += chunk.len() as u64;
|
|
|
+
|
|
|
+ if total_size > 0 {
|
|
|
+ let percent = (current_downloaded as f64 / total_size as f64) * 100.0;
|
|
|
+ progress_callback(current_downloaded, total_size, percent);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证文件大小
|
|
|
+ let final_size = std::fs::metadata(&temp_path)
|
|
|
+ .map(|m| m.len())
|
|
|
+ .unwrap_or(0);
|
|
|
+
|
|
|
+ if total_size > 0 && final_size < total_size {
|
|
|
+ // 文件不完整,保留临时文件以便下次续传
|
|
|
+ log::warn!(
|
|
|
+ "下载不完整: {}/{} 字节,可重试继续下载",
|
|
|
+ final_size,
|
|
|
+ total_size
|
|
|
+ );
|
|
|
+ return Err("下载不完整,请重试".to_string());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 重命名临时文件为最终文件
|
|
|
+ if dest_path.exists() {
|
|
|
+ let _ = std::fs::remove_file(dest_path);
|
|
|
+ }
|
|
|
+ std::fs::rename(&temp_path, dest_path)
|
|
|
+ .map_err(|e| format!("Failed to rename file: {}", e))?;
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+}
|
|
|
+
|
|
|
#[derive(Serialize, Deserialize, Clone)]
|
|
|
pub struct InstallOptions {
|
|
|
pub version: Option<String>,
|
|
|
@@ -193,15 +333,23 @@ where
|
|
|
let temp_dir = std::env::temp_dir();
|
|
|
let msi_path = temp_dir.join(format!("node-{}-x64.msi", version));
|
|
|
|
|
|
- let client = reqwest::Client::new();
|
|
|
- let response = client
|
|
|
- .get(&download_url)
|
|
|
- .send()
|
|
|
- .await
|
|
|
- .map_err(|e| e.to_string())?;
|
|
|
-
|
|
|
- let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
|
|
- std::fs::write(&msi_path, bytes).map_err(|e| e.to_string())?;
|
|
|
+ // 使用流式下载并报告进度
|
|
|
+ // 下载进度占 10% - 50%
|
|
|
+ let mut last_logged_percent: i32 = 0;
|
|
|
+ download_file_with_progress(&download_url, &msi_path, |downloaded, total, percent| {
|
|
|
+ // 下载进度映射到 10% - 50%
|
|
|
+ let progress = 10.0 + (percent * 0.4);
|
|
|
+ let downloaded_mb = downloaded as f64 / 1024.0 / 1024.0;
|
|
|
+ let total_mb = total as f64 / 1024.0 / 1024.0;
|
|
|
+ let current_percent = percent as i32;
|
|
|
+ // 每 5% 记录一次日志,但每次都更新进度条
|
|
|
+ let should_log = current_percent - last_logged_percent >= 5;
|
|
|
+ if should_log {
|
|
|
+ last_logged_percent = (current_percent / 5) * 5;
|
|
|
+ }
|
|
|
+ let message = format!("Downloading Node.js... ({:.1}MB / {:.1}MB)", downloaded_mb, total_mb);
|
|
|
+ emit_status(&message, progress, Some("install.downloading.progress"));
|
|
|
+ }).await?;
|
|
|
|
|
|
if is_cancelled() {
|
|
|
let _ = std::fs::remove_file(&msi_path);
|
|
|
@@ -212,7 +360,7 @@ where
|
|
|
});
|
|
|
}
|
|
|
|
|
|
- emit_status("Installing Node.js...", 50.0, Some("install.installing"));
|
|
|
+ emit_status("Installing Node.js...", 55.0, Some("install.installing"));
|
|
|
|
|
|
// 执行安装
|
|
|
let mut args = vec!["/i", msi_path.to_str().unwrap(), "/qn"];
|
|
|
@@ -363,17 +511,25 @@ where
|
|
|
let temp_dir = std::env::temp_dir();
|
|
|
let exe_path = temp_dir.join("VSCodeSetup.exe");
|
|
|
|
|
|
- let client = reqwest::Client::new();
|
|
|
- let response = client
|
|
|
- .get(download_url)
|
|
|
- .send()
|
|
|
- .await
|
|
|
- .map_err(|e| e.to_string())?;
|
|
|
-
|
|
|
- let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
|
|
- std::fs::write(&exe_path, bytes).map_err(|e| e.to_string())?;
|
|
|
+ // 使用流式下载并报告进度
|
|
|
+ // 下载进度占 10% - 60%
|
|
|
+ let mut last_logged_percent: i32 = 0;
|
|
|
+ download_file_with_progress(download_url, &exe_path, |downloaded, total, percent| {
|
|
|
+ // 下载进度映射到 10% - 60%
|
|
|
+ let progress = 10.0 + (percent * 0.5);
|
|
|
+ let downloaded_mb = downloaded as f64 / 1024.0 / 1024.0;
|
|
|
+ let total_mb = total as f64 / 1024.0 / 1024.0;
|
|
|
+ let current_percent = percent as i32;
|
|
|
+ // 每 5% 记录一次日志
|
|
|
+ let should_log = current_percent - last_logged_percent >= 5;
|
|
|
+ if should_log {
|
|
|
+ last_logged_percent = (current_percent / 5) * 5;
|
|
|
+ }
|
|
|
+ let message = format!("Downloading VS Code... ({:.1}MB / {:.1}MB)", downloaded_mb, total_mb);
|
|
|
+ emit_status(&message, progress, Some("install.downloading.progress"));
|
|
|
+ }).await?;
|
|
|
|
|
|
- emit_status("Installing VS Code...", 50.0, Some("install.installing"));
|
|
|
+ emit_status("Installing VS Code...", 65.0, Some("install.installing"));
|
|
|
|
|
|
let mut args = vec!["/VERYSILENT", "/NORESTART", "/MERGETASKS=!runcode"];
|
|
|
|
|
|
@@ -421,7 +577,7 @@ where
|
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
{
|
|
|
- emit_status("Installing VS Code...", 10.0, Some("install.installing"));
|
|
|
+ emit_status("Downloading VS Code...", 10.0, Some("install.downloading"));
|
|
|
|
|
|
// 下载并安装 .deb 包
|
|
|
let download_url = "https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64";
|
|
|
@@ -429,15 +585,22 @@ where
|
|
|
let temp_dir = std::env::temp_dir();
|
|
|
let deb_path = temp_dir.join("code.deb");
|
|
|
|
|
|
- let client = reqwest::Client::new();
|
|
|
- let response = client
|
|
|
- .get(download_url)
|
|
|
- .send()
|
|
|
- .await
|
|
|
- .map_err(|e| e.to_string())?;
|
|
|
+ // 使用流式下载并报告进度
|
|
|
+ let mut last_logged_percent: i32 = 0;
|
|
|
+ download_file_with_progress(download_url, &deb_path, |downloaded, total, percent| {
|
|
|
+ let progress = 10.0 + (percent * 0.5);
|
|
|
+ let downloaded_mb = downloaded as f64 / 1024.0 / 1024.0;
|
|
|
+ let total_mb = total as f64 / 1024.0 / 1024.0;
|
|
|
+ let current_percent = percent as i32;
|
|
|
+ let should_log = current_percent - last_logged_percent >= 5;
|
|
|
+ if should_log {
|
|
|
+ last_logged_percent = (current_percent / 5) * 5;
|
|
|
+ }
|
|
|
+ let message = format!("Downloading VS Code... ({:.1}MB / {:.1}MB)", downloaded_mb, total_mb);
|
|
|
+ emit_status(&message, progress, Some("install.downloading.progress"));
|
|
|
+ }).await?;
|
|
|
|
|
|
- let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
|
|
- std::fs::write(&deb_path, bytes).map_err(|e| e.to_string())?;
|
|
|
+ emit_status("Installing VS Code...", 65.0, Some("install.installing"));
|
|
|
|
|
|
// 使用 shell 执行以获取最新的 PATH
|
|
|
let cmd = format!("sudo dpkg -i {}", deb_path.to_str().unwrap());
|
|
|
@@ -513,16 +676,25 @@ where
|
|
|
let temp_dir = std::env::temp_dir();
|
|
|
let exe_path = temp_dir.join("GitSetup.exe");
|
|
|
|
|
|
- let response = client
|
|
|
- .get(download_url)
|
|
|
- .send()
|
|
|
- .await
|
|
|
- .map_err(|e| e.to_string())?;
|
|
|
-
|
|
|
- let bytes = response.bytes().await.map_err(|e| e.to_string())?;
|
|
|
- std::fs::write(&exe_path, bytes).map_err(|e| e.to_string())?;
|
|
|
+ // 使用流式下载并报告进度
|
|
|
+ // 下载进度占 10% - 60%
|
|
|
+ let mut last_logged_percent: i32 = 0;
|
|
|
+ download_file_with_progress(download_url, &exe_path, |downloaded, total, percent| {
|
|
|
+ // 下载进度映射到 10% - 60%
|
|
|
+ let progress = 10.0 + (percent * 0.5);
|
|
|
+ let downloaded_mb = downloaded as f64 / 1024.0 / 1024.0;
|
|
|
+ let total_mb = total as f64 / 1024.0 / 1024.0;
|
|
|
+ let current_percent = percent as i32;
|
|
|
+ // 每 5% 记录一次日志
|
|
|
+ let should_log = current_percent - last_logged_percent >= 5;
|
|
|
+ if should_log {
|
|
|
+ last_logged_percent = (current_percent / 5) * 5;
|
|
|
+ }
|
|
|
+ let message = format!("Downloading Git... ({:.1}MB / {:.1}MB)", downloaded_mb, total_mb);
|
|
|
+ emit_status(&message, progress, Some("install.downloading.progress"));
|
|
|
+ }).await?;
|
|
|
|
|
|
- emit_status("Installing Git...", 50.0, Some("install.installing"));
|
|
|
+ emit_status("Installing Git...", 65.0, Some("install.installing"));
|
|
|
|
|
|
let mut args = vec!["/VERYSILENT", "/NORESTART"];
|
|
|
|