|
|
@@ -8,13 +8,50 @@ use std::io::Write;
|
|
|
use std::path::Path;
|
|
|
use tauri::{Emitter, State};
|
|
|
|
|
|
+/// 安装错误,包含 i18nKey 用于前端翻译
|
|
|
+#[derive(Debug, Clone)]
|
|
|
+pub struct InstallError {
|
|
|
+ pub i18n_key: String,
|
|
|
+ pub detail: Option<String>,
|
|
|
+}
|
|
|
+
|
|
|
+impl InstallError {
|
|
|
+ pub fn new(i18n_key: &str) -> Self {
|
|
|
+ Self {
|
|
|
+ i18n_key: i18n_key.to_string(),
|
|
|
+ detail: None,
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn with_detail(i18n_key: &str, detail: &str) -> Self {
|
|
|
+ Self {
|
|
|
+ i18n_key: i18n_key.to_string(),
|
|
|
+ detail: Some(detail.to_string()),
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /// 转换为前端可用的格式
|
|
|
+ pub fn to_message(&self) -> String {
|
|
|
+ match &self.detail {
|
|
|
+ Some(detail) => format!("{}|{}", self.i18n_key, detail),
|
|
|
+ None => self.i18n_key.clone(),
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl std::fmt::Display for InstallError {
|
|
|
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
+ write!(f, "{}", self.to_message())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/// 下载文件并报告进度(支持断点续传)
|
|
|
/// 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>
|
|
|
+) -> Result<(), InstallError>
|
|
|
where
|
|
|
F: FnMut(u64, u64, f64),
|
|
|
{
|
|
|
@@ -55,7 +92,7 @@ where
|
|
|
.await
|
|
|
.map_err(|e| {
|
|
|
log::error!("[HTTP] 下载请求失败: {} - {}", url, e);
|
|
|
- format!("Failed to send request: {}", e)
|
|
|
+ InstallError::with_detail("error.network.requestFailed", &e.to_string())
|
|
|
})?;
|
|
|
|
|
|
let status = response.status();
|
|
|
@@ -70,11 +107,11 @@ where
|
|
|
let _ = std::fs::remove_file(dest_path);
|
|
|
}
|
|
|
std::fs::rename(&temp_path, dest_path)
|
|
|
- .map_err(|e| format!("Failed to rename file: {}", e))?;
|
|
|
+ .map_err(|e| InstallError::with_detail("error.file.renameFailed", &e.to_string()))?;
|
|
|
return Ok(());
|
|
|
}
|
|
|
}
|
|
|
- return Err(format!("HTTP {}: 下载失败", status.as_u16()));
|
|
|
+ return Err(InstallError::with_detail("error.network.httpError", &format!("HTTP {}", status.as_u16())));
|
|
|
}
|
|
|
|
|
|
// 206 表示部分内容(断点续传成功)
|
|
|
@@ -108,15 +145,15 @@ where
|
|
|
.append(is_partial_content)
|
|
|
.truncate(!is_partial_content)
|
|
|
.open(&temp_path)
|
|
|
- .map_err(|e| format!("Failed to open file: {}", e))?;
|
|
|
+ .map_err(|e| InstallError::with_detail("error.file.openFailed", &e.to_string()))?;
|
|
|
|
|
|
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))?;
|
|
|
+ let chunk = chunk_result.map_err(|e| InstallError::with_detail("error.network.readFailed", &e.to_string()))?;
|
|
|
file.write_all(&chunk)
|
|
|
- .map_err(|e| format!("Failed to write chunk: {}", e))?;
|
|
|
+ .map_err(|e| InstallError::with_detail("error.file.writeFailed", &e.to_string()))?;
|
|
|
|
|
|
current_downloaded += chunk.len() as u64;
|
|
|
|
|
|
@@ -138,7 +175,7 @@ where
|
|
|
final_size,
|
|
|
total_size
|
|
|
);
|
|
|
- return Err("下载不完整,请重试".to_string());
|
|
|
+ return Err(InstallError::new("error.download.incomplete"));
|
|
|
}
|
|
|
|
|
|
// 重命名临时文件为最终文件
|
|
|
@@ -146,7 +183,7 @@ where
|
|
|
let _ = std::fs::remove_file(dest_path);
|
|
|
}
|
|
|
std::fs::rename(&temp_path, dest_path)
|
|
|
- .map_err(|e| format!("Failed to rename file: {}", e))?;
|
|
|
+ .map_err(|e| InstallError::with_detail("error.file.renameFailed", &e.to_string()))?;
|
|
|
|
|
|
Ok(())
|
|
|
}
|
|
|
@@ -206,14 +243,20 @@ pub async fn install_software(
|
|
|
|
|
|
// 发送安装状态事件
|
|
|
let emit_status = |message: &str, progress: f64, i18n_key: Option<&str>, skip_log: bool, i18n_params: Option<serde_json::Value>| {
|
|
|
- let _ = app.emit("install-status", serde_json::json!({
|
|
|
+ let mut json = serde_json::json!({
|
|
|
"software": software,
|
|
|
"message": message,
|
|
|
"progress": progress,
|
|
|
- "i18nKey": i18n_key,
|
|
|
"skipLog": skip_log,
|
|
|
- "i18nParams": i18n_params,
|
|
|
- }));
|
|
|
+ });
|
|
|
+ // 显式设置 i18nKey 和 i18nParams,避免 Option 序列化问题
|
|
|
+ if let Some(key) = i18n_key {
|
|
|
+ json["i18nKey"] = serde_json::Value::String(key.to_string());
|
|
|
+ }
|
|
|
+ if let Some(params) = i18n_params {
|
|
|
+ json["i18nParams"] = params;
|
|
|
+ }
|
|
|
+ let _ = app.emit("install-status", json);
|
|
|
};
|
|
|
|
|
|
// 检查是否取消
|
|
|
@@ -381,7 +424,7 @@ where
|
|
|
"downloaded": format!("{:.1}MB", downloaded_mb),
|
|
|
"total": format!("{:.1}MB", total_mb)
|
|
|
})));
|
|
|
- }).await?;
|
|
|
+ }).await.map_err(|e| e.to_message())?;
|
|
|
|
|
|
if is_cancelled() {
|
|
|
let _ = std::fs::remove_file(&msi_path);
|
|
|
@@ -587,7 +630,7 @@ where
|
|
|
"downloaded": format!("{:.1}MB", downloaded_mb),
|
|
|
"total": format!("{:.1}MB", total_mb)
|
|
|
})));
|
|
|
- }).await?;
|
|
|
+ }).await.map_err(|e| e.to_message())?;
|
|
|
|
|
|
emit_status("正在安装 VS Code...", 65.0, Some("install.installing"), false, Some(serde_json::json!({
|
|
|
"software": "VS Code"
|
|
|
@@ -680,7 +723,7 @@ where
|
|
|
"downloaded": format!("{:.1}MB", downloaded_mb),
|
|
|
"total": format!("{:.1}MB", total_mb)
|
|
|
})));
|
|
|
- }).await?;
|
|
|
+ }).await.map_err(|e| e.to_message())?;
|
|
|
|
|
|
emit_status("正在安装 VS Code...", 65.0, Some("install.installing"), false, Some(serde_json::json!({
|
|
|
"software": "VS Code"
|
|
|
@@ -823,7 +866,7 @@ where
|
|
|
"downloaded": format!("{:.1}MB", downloaded_mb),
|
|
|
"total": format!("{:.1}MB", total_mb)
|
|
|
})));
|
|
|
- }).await?;
|
|
|
+ }).await.map_err(|e| e.to_message())?;
|
|
|
|
|
|
emit_status("正在安装 Git...", 65.0, Some("install.installing"), false, Some(serde_json::json!({
|
|
|
"software": "Git"
|
|
|
@@ -1137,6 +1180,10 @@ where
|
|
|
.map(|(name, _)| *name)
|
|
|
.collect();
|
|
|
|
|
|
- Err(format!("Failed to install: {}", failed.join(", ")))
|
|
|
+ Ok(InstallResult {
|
|
|
+ success: false,
|
|
|
+ message: format!("Failed to install: {}", failed.join(", ")),
|
|
|
+ i18n_key: Some("install.failedToInstall".to_string()),
|
|
|
+ })
|
|
|
}
|
|
|
}
|