黄中银 2 недель назад
Родитель
Сommit
b24e571b7f
1 измененных файлов с 210 добавлено и 38 удалено
  1. 210 38
      src-tauri/src/commands/install.rs

+ 210 - 38
src-tauri/src/commands/install.rs

@@ -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"];