diff --git a/src-tauri/src/commands/app_commands.rs b/src-tauri/src/commands/app_commands.rs new file mode 100644 index 0000000..ea8215e --- /dev/null +++ b/src-tauri/src/commands/app_commands.rs @@ -0,0 +1,55 @@ +use tauri::AppHandle; + +use crate::domain::models::{AppSettings, EssentialsRepo, LogPayload, SyncEssentialsResult}; +use crate::services::{essentials_service, settings_service, software_state_service}; +use crate::winget::Software; + +#[tauri::command] +pub fn get_settings(app: AppHandle) -> AppSettings { + settings_service::get_settings(&app) +} + +#[tauri::command] +pub fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String> { + settings_service::save_settings(&app, &settings) +} + +#[tauri::command] +pub async fn sync_essentials(app: AppHandle) -> Result { + essentials_service::sync_essentials(&app).await +} + +#[tauri::command] +pub fn get_essentials(app: AppHandle) -> Option { + essentials_service::get_essentials(&app) +} + +#[tauri::command] +pub async fn initialize_app(app: AppHandle) -> Result { + software_state_service::initialize_app(app).await +} + +#[tauri::command] +pub async fn get_installed_software(app: AppHandle) -> Vec { + software_state_service::get_installed_software(app).await +} + +#[tauri::command] +pub async fn get_updates(app: AppHandle) -> Vec { + software_state_service::get_updates(app).await +} + +#[tauri::command] +pub async fn get_software_info(app: AppHandle, id: String) -> Option { + software_state_service::get_software_info(app, id).await +} + +#[tauri::command] +pub async fn get_software_icon(app: AppHandle, id: String, name: String) -> Option { + software_state_service::get_software_icon(app, id, name).await +} + +#[tauri::command] +pub fn get_logs_history() -> Vec { + vec![] +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 0000000..9b6cdb5 --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1 @@ +pub mod app_commands; diff --git a/src-tauri/src/domain/mod.rs b/src-tauri/src/domain/mod.rs new file mode 100644 index 0000000..c446ac8 --- /dev/null +++ b/src-tauri/src/domain/mod.rs @@ -0,0 +1 @@ +pub mod models; diff --git a/src-tauri/src/domain/models.rs b/src-tauri/src/domain/models.rs new file mode 100644 index 0000000..6198b79 --- /dev/null +++ b/src-tauri/src/domain/models.rs @@ -0,0 +1,65 @@ +use serde::{Deserialize, Serialize}; + +use crate::winget::{PostInstallStep, Software}; + +#[derive(Clone, Serialize, Deserialize)] +pub struct AppSettings { + pub repo_url: String, +} + +impl Default for AppSettings { + fn default() -> Self { + Self { + repo_url: "https://karlblue.github.io/winget-repo".to_string(), + } + } +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct EssentialsRepo { + pub version: String, + pub essentials: Vec, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct InstallTask { + pub id: String, + pub version: Option, + #[serde(default)] + pub use_manifest: bool, + pub manifest_url: Option, + #[serde(default = "default_true")] + pub enable_post_install: bool, +} + +fn default_true() -> bool { + true +} + +#[derive(Clone, Serialize)] +pub struct LogPayload { + pub id: String, + pub timestamp: String, + pub command: String, + pub output: String, + pub status: String, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct SyncEssentialsResult { + pub status: String, + pub message: String, +} + +#[derive(Clone, Serialize)] +pub struct InstallProgress { + pub id: String, + pub status: String, + pub progress: f32, +} + +#[derive(Clone)] +pub struct ResolvedPostInstall { + pub software: Software, + pub steps: Vec, +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 86b7008..7928b9d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,651 +1,33 @@ +use tauri::Manager; + +pub mod commands; +pub mod domain; +pub mod providers; +pub mod services; +pub mod storage; +pub mod tasks; pub mod winget; -use std::fs; -use std::io::Read; -use std::process::{Command, Stdio}; -use std::os::windows::process::CommandExt; -use std::io::{BufRead, BufReader}; -use std::path::PathBuf; -use std::thread; -use tokio::sync::mpsc; -use tauri::{AppHandle, Manager, State, Emitter}; -use serde::{Serialize, Deserialize}; -use winget::{Software, list_installed_software, list_updates, ensure_winget_dependencies, PostInstallStep, get_cached_or_extract_icon}; -use regex::Regex; -use winreg::RegKey; -use winreg::enums::*; - -#[derive(Clone, Serialize, Deserialize)] -pub struct AppSettings { - pub repo_url: String, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct EssentialsRepo { - pub version: String, - pub essentials: Vec, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct InstallTask { - pub id: String, - pub version: Option, - #[serde(default)] - pub use_manifest: bool, - pub manifest_url: Option, - #[serde(default = "default_true")] - pub enable_post_install: bool, -} - -fn default_true() -> bool { true } - -impl Default for AppSettings { - fn default() -> Self { - Self { - repo_url: "https://karlblue.github.io/winget-repo".to_string(), - } - } -} - -struct AppState { - install_tx: mpsc::Sender, -} - -#[derive(Clone, Serialize, Deserialize)] -pub struct SyncEssentialsResult { - pub status: String, - pub message: String, -} - -#[derive(Clone, Serialize)] -pub struct LogPayload { - pub id: String, - pub timestamp: String, - pub command: String, - pub output: String, - pub status: String, -} - -pub fn emit_log(handle: &AppHandle, id: &str, command: &str, output: &str, status: &str) { - let now = chrono::Local::now().format("%H:%M:%S").to_string(); - let _ = handle.emit("log-event", LogPayload { - id: id.to_string(), - timestamp: now, - command: command.to_string(), - output: output.to_string(), - status: status.to_string(), - }); -} - -fn get_settings_path(app: &AppHandle) -> PathBuf { - let app_data_dir = app.path().app_data_dir().unwrap_or_default(); - if !app_data_dir.exists() { - let _ = fs::create_dir_all(&app_data_dir); - } - app_data_dir.join("settings.json") -} - -fn get_essentials_path(app: &AppHandle) -> PathBuf { - let app_data_dir = app.path().app_data_dir().unwrap_or_default(); - if !app_data_dir.exists() { - let _ = fs::create_dir_all(&app_data_dir); - } - app_data_dir.join("setup-essentials.json") -} - -#[tauri::command] -fn get_settings(app: AppHandle) -> AppSettings { - let path = get_settings_path(&app); - if !path.exists() { - let default_settings = AppSettings::default(); - let _ = fs::write(&path, serde_json::to_string_pretty(&default_settings).unwrap()); - return default_settings; - } - let content = fs::read_to_string(path).unwrap_or_default(); - serde_json::from_str(&content).unwrap_or_default() -} - -#[tauri::command] -fn save_settings(app: AppHandle, settings: AppSettings) -> Result<(), String> { - let path = get_settings_path(&app); - let content = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?; - fs::write(path, content).map_err(|e| e.to_string()) -} - -#[tauri::command] -async fn sync_essentials(app: AppHandle) -> Result { - let settings = get_settings(app.clone()); - let url = format!("{}/setup-essentials.json", settings.repo_url.trim_end_matches('/')); - let cache_path = get_essentials_path(&app); - - emit_log(&app, "sync-essentials", "Syncing Essentials", &format!("Downloading from {}...", url), "info"); - - let client = reqwest::Client::builder() - .timeout(std::time::Duration::from_secs(10)) - .build() - .map_err(|e| e.to_string())?; - - match client.get(&url).send().await { - Ok(response) => { - if response.status().is_success() { - let content = response.text().await.map_err(|e| e.to_string())?; - let validation: Result = serde_json::from_str(&content); - if validation.is_ok() { - fs::write(cache_path, content).map_err(|e| e.to_string())?; - emit_log(&app, "sync-essentials", "Result", "Essentials list updated successfully.", "success"); - Ok(SyncEssentialsResult { - status: "updated".to_string(), - message: "清单同步成功".to_string(), - }) - } else { - emit_log(&app, "sync-essentials", "Error", "Invalid JSON format from repository. Expected { version, essentials }.", "error"); - Err("Invalid JSON format".to_string()) - } - } else { - let err_msg = format!("HTTP Error: {}", response.status()); - emit_log(&app, "sync-essentials", "Error", &err_msg, "error"); - Err(err_msg) - } - } - Err(e) => { - if cache_path.exists() { - emit_log(&app, "sync-essentials", "Skipped", &format!("Network issue: {}. Using local cache.", e), "info"); - Ok(SyncEssentialsResult { - status: "cache_used".to_string(), - message: "网络不可用,已继续使用本地缓存".to_string(), - }) - } else { - let err_msg = format!("Network issue: {}", e); - emit_log(&app, "sync-essentials", "Error", &err_msg, "error"); - Err(err_msg) - } - } - } -} - -#[tauri::command] -async fn initialize_app(app: AppHandle) -> Result { - let app_clone = app.clone(); - tokio::task::spawn_blocking(move || { - ensure_winget_dependencies(&app_clone).map(|_| true) - }).await.unwrap_or(Err("Initialization Task Panicked".to_string())) -} - -#[tauri::command] -fn get_essentials(app: AppHandle) -> Option { - let file_path = get_essentials_path(&app); - if !file_path.exists() { - return None; - } - - let content = fs::read_to_string(file_path).unwrap_or_default(); - serde_json::from_str(&content).ok() -} - -#[tauri::command] -async fn get_installed_software(app: AppHandle) -> Vec { - tokio::task::spawn_blocking(move || list_installed_software(&app)).await.unwrap_or_default() -} - -#[tauri::command] -async fn get_updates(app: AppHandle) -> Vec { - tokio::task::spawn_blocking(move || list_updates(&app)).await.unwrap_or_default() -} - -#[tauri::command] -async fn get_software_icon(app: AppHandle, id: String, name: String) -> Option { - tokio::task::spawn_blocking(move || get_cached_or_extract_icon(&app, &id, &name)).await.unwrap_or(None) -} - -#[tauri::command] -async fn install_software( - task: InstallTask, - state: State<'_, AppState> -) -> Result<(), String> { - state.install_tx.send(task).await.map_err(|e| e.to_string()) -} - -#[tauri::command] -async fn get_software_info(app: AppHandle, id: String) -> Option { - tokio::task::spawn_blocking(move || winget::get_software_info(&app, &id)).await.unwrap_or(None) -} - -#[tauri::command] -fn get_logs_history() -> Vec { - vec![] -} - -fn spawn_install_stream_reader( - reader: R, - handle: AppHandle, - log_id: String, - task_id: String, - stream_name: &'static str, - perc_re: Regex, - size_re: Regex, -) -> thread::JoinHandle<()> { - thread::spawn(move || { - let reader = BufReader::new(reader); - for line_res in reader.split(b'\r') { - if let Ok(line_bytes) = line_res { - let line_str = String::from_utf8_lossy(&line_bytes).to_string(); - let clean_line = line_str.trim(); - if clean_line.is_empty() { - continue; - } - - if stream_name == "stdout" { - let mut is_progress = false; - if let Some(caps) = perc_re.captures(clean_line) { - if let Ok(p_val) = caps[1].parse::() { - let _ = handle.emit("install-status", InstallProgress { - id: task_id.clone(), - status: "installing".to_string(), - progress: p_val / 100.0, - }); - is_progress = true; - } - } else if let Some(caps) = size_re.captures(clean_line) { - let current = caps[1].parse::().unwrap_or(0.0); - let total = caps[2].parse::().unwrap_or(1.0); - if total > 0.0 { - let _ = handle.emit("install-status", InstallProgress { - id: task_id.clone(), - status: "installing".to_string(), - progress: (current / total).min(1.0), - }); - is_progress = true; - } - } - - if !is_progress && clean_line.chars().count() > 1 { - emit_log(&handle, &log_id, "", clean_line, "info"); - } - } else { - emit_log(&handle, &log_id, stream_name, clean_line, "error"); - } - } - } - }) -} - -fn expand_win_path(path: &str) -> PathBuf { - let mut expanded = path.to_string(); - let env_vars = [ - "AppData", "LocalAppData", "ProgramData", "SystemRoot", "SystemDrive", "TEMP", "USERPROFILE", "HOMEDRIVE", "HOMEPATH" - ]; - - for var in env_vars { - let re = Regex::new(&format!(r"(?i)%{}%", var)).unwrap(); - if re.is_match(&expanded) { - if let Ok(val) = std::env::var(var) { - expanded = re.replace_all(&expanded, val.as_str()).to_string(); - } - } - } - PathBuf::from(expanded) -} - -async fn execute_post_install(handle: &AppHandle, log_id: &str, steps: Vec) -> Result<(), String> { - let steps_len = steps.len(); - for (i, step) in steps.into_iter().enumerate() { - let step_prefix = format!("Step {}/{}: ", i + 1, steps_len); - - let delay = match &step { - PostInstallStep::RegistryBatch { delay_ms, .. } => *delay_ms, - PostInstallStep::FileCopy { delay_ms, .. } => *delay_ms, - PostInstallStep::FileDelete { delay_ms, .. } => *delay_ms, - PostInstallStep::Command { delay_ms, .. } => *delay_ms, - }; - - match step { - PostInstallStep::RegistryBatch { root, base_path, values, .. } => { - emit_log(handle, log_id, "Registry Update", &format!("{}Applying batch registry settings to {}...", step_prefix, base_path), "info"); - let hive = match root.as_str() { - "HKCU" => RegKey::predef(HKEY_CURRENT_USER), - "HKLM" => RegKey::predef(HKEY_LOCAL_MACHINE), - _ => { - emit_log(handle, log_id, "Registry Error", &format!("Unknown root hive: {}", root), "error"); - continue; - } - }; - - match hive.create_subkey(&base_path) { - Ok((key, _)) => { - for (name, val) in values { - let res = match val.v_type.as_str() { - "String" => key.set_value(&name, &val.data.as_str().unwrap_or_default()), - "Dword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0) as u32)), - "Qword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0))), - "MultiString" => { - let strings: Vec = val.data.as_array() - .map(|a| a.iter().filter_map(|v| v.as_str().map(|s| s.to_string())).collect()) - .unwrap_or_default(); - key.set_value(&name, &strings) - }, - "ExpandString" => { - key.set_value(&name, &val.data.as_str().unwrap_or_default()) - }, - "Delete" => { - key.delete_value(&name) - }, - _ => Err(std::io::Error::new(std::io::ErrorKind::Other, "Unsupported type")), - }; - if let Err(e) = res { - emit_log(handle, log_id, "Registry Error", &format!("Failed to apply {}: {}", name, e), "error"); - } - } - }, - Err(e) => { - emit_log(handle, log_id, "Registry Error", &format!("Failed to create/open key {}: {}", base_path, e), "error"); - } - } - }, - PostInstallStep::FileCopy { src, dest, .. } => { - let dest_path = expand_win_path(&dest); - let src_is_url = src.starts_with("http://") || src.starts_with("https://"); - - if src_is_url { - emit_log(handle, log_id, "File Download", &format!("{}Downloading {:?} to {:?}...", step_prefix, src, dest_path), "info"); - let client = reqwest::Client::new(); - match client.get(&src).timeout(std::time::Duration::from_secs(60)).send().await { - Ok(resp) if resp.status().is_success() => { - if let Ok(bytes) = resp.bytes().await { - if let Some(parent) = dest_path.parent() { - let _ = fs::create_dir_all(parent); - } - if let Err(e) = fs::write(&dest_path, bytes) { - emit_log(handle, log_id, "File Error", &format!("Failed to write to {:?}: {}", dest_path, e), "error"); - } else { - emit_log(handle, log_id, "Success", "File downloaded and saved successfully.", "success"); - } - } - }, - Ok(resp) => emit_log(handle, log_id, "Download Error", &format!("HTTP Status: {}", resp.status()), "error"), - Err(e) => emit_log(handle, log_id, "Download Error", &e.to_string(), "error"), - } - } else { - let src_path = expand_win_path(&src); - emit_log(handle, log_id, "File Copy", &format!("{}Copying {:?} to {:?}...", step_prefix, src_path, dest_path), "info"); - if let Some(parent) = dest_path.parent() { - let _ = fs::create_dir_all(parent); - } - if let Err(e) = fs::copy(&src_path, &dest_path) { - emit_log(handle, log_id, "File Error", &format!("Failed to copy file: {}", e), "error"); - } else { - emit_log(handle, log_id, "Success", "File copied successfully.", "success"); - } - } - }, - PostInstallStep::FileDelete { path, .. } => { - let full_path = expand_win_path(&path); - emit_log(handle, log_id, "File Delete", &format!("{}Deleting {:?}...", step_prefix, full_path), "info"); - if full_path.exists() { - if let Err(e) = fs::remove_file(&full_path) { - emit_log(handle, log_id, "File Error", &format!("Failed to delete file: {}", e), "error"); - } else { - emit_log(handle, log_id, "Success", "File deleted successfully.", "success"); - } - } else { - emit_log(handle, log_id, "File Info", "File does not exist, skipping.", "info"); - } - }, - PostInstallStep::Command { run, .. } => { - emit_log(handle, log_id, "Command Execution", &format!("{}Executing: {}", step_prefix, run), "info"); - let output = Command::new("cmd") - .arg("/C") - .raw_arg(&run) - .creation_flags(0x08000000) - .output(); - - match output { - Ok(out) => { - if !out.status.success() { - let err = String::from_utf8_lossy(&out.stderr); - emit_log(handle, log_id, "Command Failed", &err, "error"); - } else { - emit_log(handle, log_id, "Success", "Command executed successfully.", "success"); - } - }, - Err(e) => { - emit_log(handle, log_id, "Execution Error", &e.to_string(), "error"); - } - } - } - } - - if let Some(ms) = delay { - if ms > 0 { - emit_log(handle, log_id, "Post-Install", &format!("Waiting for {}ms...", ms), "info"); - tokio::time::sleep(std::time::Duration::from_millis(ms)).await; - } - } - } - Ok(()) -} - -#[derive(Clone, Serialize)] -struct InstallProgress { - id: String, - status: String, - progress: f32, -} - pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .setup(move |app| { - let handle = app.handle().clone(); - let (tx, mut rx) = mpsc::channel::(100); - app.manage(AppState { install_tx: tx }); - - tauri::async_runtime::spawn(async move { - let perc_re = Regex::new(r"(\d+)\s*%").unwrap(); - let size_re = Regex::new(r"([\d\.]+)\s*[a-zA-Z]+\s*/\s*([\d\.]+)\s*[a-zA-Z]+").unwrap(); - - while let Some(task) = rx.recv().await { - let task_id = task.id.clone(); - let task_version = task.version.clone(); - let use_manifest = task.use_manifest; - let manifest_url = task.manifest_url.clone(); - let enable_post_install_flag = task.enable_post_install; - - let log_id = format!("install-{}", task_id); - - let _ = handle.emit("install-status", InstallProgress { - id: task_id.clone(), - status: "installing".to_string(), - progress: 0.0, - }); - - let mut args = vec!["install".to_string()]; - let display_cmd: String; - let mut temp_manifest_path: Option = None; - - if use_manifest && manifest_url.is_some() { - let url = manifest_url.unwrap(); - display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url); - emit_log(&handle, &log_id, &display_cmd, "Downloading remote manifest...", "info"); - - let client = reqwest::Client::new(); - match client.get(&url).timeout(std::time::Duration::from_secs(15)).send().await { - Ok(resp) if resp.status().is_success() => { - if let Ok(content) = resp.text().await { - let temp_dir = std::env::temp_dir(); - let file_name = format!("winget_tmp_{}.yaml", chrono::Local::now().timestamp_millis()); - let local_path = temp_dir.join(file_name); - if fs::write(&local_path, content).is_ok() { - args.push("--manifest".to_string()); - args.push(local_path.to_string_lossy().to_string()); - temp_manifest_path = Some(local_path); - } - } - } - _ => {} - } - - if temp_manifest_path.is_none() { - emit_log(&handle, &log_id, "Error", "Failed to download or save manifest.", "error"); - let _ = handle.emit("install-status", InstallProgress { - id: task_id.clone(), - status: "error".to_string(), - progress: 0.0, - }); - continue; - } - } else { - args.push("--id".to_string()); - args.push(task_id.clone()); - args.push("-e".to_string()); - - if let Some(v) = &task_version { - if !v.is_empty() { - args.push("--version".to_string()); - args.push(v.clone()); - } - } - - display_cmd = match &task_version { - Some(v) if !v.is_empty() => format!("Winget Install: {} (v{})", task_id, v), - _ => format!("Winget Install: {}", task_id), - }; - } - - args.extend([ - "--silent".to_string(), - "--accept-package-agreements".to_string(), - "--accept-source-agreements".to_string(), - "--disable-interactivity".to_string(), - ]); - - let full_command = format!("winget {}", args.join(" ")); - emit_log(&handle, &log_id, &display_cmd, &format!("Executing: {}\n---", full_command), "info"); - - let child = Command::new("winget") - .args(&args) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .creation_flags(0x08000000) - .spawn(); - - let status_result = match child { - Ok(mut child_proc) => { - let stdout_handle = child_proc.stdout.take().map(|stdout| { - spawn_install_stream_reader( - stdout, - handle.clone(), - log_id.clone(), - task_id.clone(), - "stdout", - perc_re.clone(), - size_re.clone(), - ) - }); - let stderr_handle = child_proc.stderr.take().map(|stderr| { - spawn_install_stream_reader( - stderr, - handle.clone(), - log_id.clone(), - task_id.clone(), - "stderr", - perc_re.clone(), - size_re.clone(), - ) - }); - - let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false); - if let Some(join_handle) = stdout_handle { - let _ = join_handle.join(); - } - if let Some(join_handle) = stderr_handle { - let _ = join_handle.join(); - } - let status_result = if exit_status { "success" } else { "error" }; - - if status_result == "success" && enable_post_install_flag { - let essentials = get_essentials(handle.clone()); - let software_info = essentials.and_then(|repo| { - repo.essentials.into_iter().find(|s| s.id == task_id) - }); - - if let Some(sw) = software_info { - let mut final_steps = None; - if let Some(steps) = sw.post_install { - if !steps.is_empty() { - final_steps = Some(steps); - } - } else if let Some(url) = sw.post_install_url { - emit_log(&handle, &log_id, "Post-Install", "Local config not found, fetching remote config...", "info"); - let client = reqwest::Client::new(); - if let Ok(resp) = client.get(&url).timeout(std::time::Duration::from_secs(10)).send().await { - if resp.status().is_success() { - if let Ok(text) = resp.text().await { - match serde_json::from_str::>(&text) { - Ok(steps) => { - emit_log(&handle, &log_id, "Post-Install", &format!("Successfully fetched remote config with {} steps.", steps.len()), "info"); - final_steps = Some(steps); - }, - Err(e) => { - emit_log(&handle, &log_id, "Post-Install Error", &format!("JSON Parse Error: {}. Raw Content: {}", e, text), "error"); - } - } - } - } else { - emit_log(&handle, &log_id, "Post-Install Error", &format!("Remote config HTTP Error: {}", resp.status()), "error"); - } - } - } - - if let Some(steps) = final_steps { - let _ = handle.emit("install-status", InstallProgress { - id: task_id.clone(), - status: "configuring".to_string(), - progress: 1.0, - }); - emit_log(&handle, &log_id, "Post-Install", "Starting post-installation configuration...", "info"); - if let Err(e) = execute_post_install(&handle, &log_id, steps).await { - emit_log(&handle, &log_id, "Post-Install Error", &e, "error"); - } else { - emit_log(&handle, &log_id, "Post-Install", "Post-installation configuration completed.", "success"); - } - } - } - } - status_result - }, - Err(e) => { - emit_log(&handle, &log_id, "Fatal Error", &e.to_string(), "error"); - "error" - } - }; - - let _ = handle.emit("install-status", InstallProgress { - id: task_id.clone(), - status: status_result.to_string(), - progress: 1.0, - }); - emit_log(&handle, &log_id, "Result", &format!("Execution finished: {}", status_result), if status_result == "success" { "success" } else { "error" }); - - if let Some(path) = temp_manifest_path { - let _ = fs::remove_file(path); - } - } - }); - + let install_state = tasks::install_queue::create_install_state(app.handle().clone()); + app.manage(install_state); Ok(()) }) .invoke_handler(tauri::generate_handler![ - initialize_app, - get_settings, - save_settings, - sync_essentials, - get_essentials, - get_installed_software, - get_updates, - get_software_icon, - get_software_info, - install_software, - get_logs_history + commands::app_commands::initialize_app, + commands::app_commands::get_settings, + commands::app_commands::save_settings, + commands::app_commands::sync_essentials, + commands::app_commands::get_essentials, + commands::app_commands::get_installed_software, + commands::app_commands::get_updates, + commands::app_commands::get_software_icon, + commands::app_commands::get_software_info, + tasks::install_queue::install_software, + commands::app_commands::get_logs_history ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src-tauri/src/providers/mod.rs b/src-tauri/src/providers/mod.rs new file mode 100644 index 0000000..bac952f --- /dev/null +++ b/src-tauri/src/providers/mod.rs @@ -0,0 +1 @@ +pub mod winget_client; diff --git a/src-tauri/src/providers/winget_client.rs b/src-tauri/src/providers/winget_client.rs new file mode 100644 index 0000000..fb3091b --- /dev/null +++ b/src-tauri/src/providers/winget_client.rs @@ -0,0 +1,23 @@ +use tauri::AppHandle; + +use crate::winget::{self, Software}; + +pub fn ensure_environment_ready(handle: &AppHandle) -> Result<(), String> { + winget::ensure_winget_dependencies(handle) +} + +pub fn list_installed_packages(handle: &AppHandle) -> Vec { + winget::list_installed_software(handle) +} + +pub fn list_upgrade_candidates(handle: &AppHandle) -> Vec { + winget::list_updates(handle) +} + +pub fn get_package_by_id(handle: &AppHandle, id: &str) -> Option { + winget::get_software_info(handle, id) +} + +pub fn resolve_icon(handle: &AppHandle, id: &str, name: &str) -> Option { + winget::get_cached_or_extract_icon(handle, id, name) +} diff --git a/src-tauri/src/services/essentials_service.rs b/src-tauri/src/services/essentials_service.rs new file mode 100644 index 0000000..4ffb57f --- /dev/null +++ b/src-tauri/src/services/essentials_service.rs @@ -0,0 +1,85 @@ +use reqwest::Client; +use tauri::AppHandle; + +use crate::domain::models::{EssentialsRepo, SyncEssentialsResult}; +use crate::services::log_service::emit_log; +use crate::services::settings_service; +use crate::storage::{essentials_store, paths}; + +pub async fn sync_essentials(app: &AppHandle) -> Result { + let settings = settings_service::get_settings(app); + let url = format!("{}/setup-essentials.json", settings.repo_url.trim_end_matches('/')); + let cache_path = paths::get_essentials_path(app); + + emit_log( + app, + "sync-essentials", + "Syncing Essentials", + &format!("Downloading from {}...", url), + "info", + ); + + let client = Client::builder() + .timeout(std::time::Duration::from_secs(10)) + .build() + .map_err(|e| e.to_string())?; + + match client.get(&url).send().await { + Ok(response) => { + if response.status().is_success() { + let content = response.text().await.map_err(|e| e.to_string())?; + let validation: Result = serde_json::from_str(&content); + if validation.is_ok() { + essentials_store::save_essentials(app, &content)?; + emit_log( + app, + "sync-essentials", + "Result", + "Essentials list updated successfully.", + "success", + ); + Ok(SyncEssentialsResult { + status: "updated".to_string(), + message: "清单同步成功".to_string(), + }) + } else { + emit_log( + app, + "sync-essentials", + "Error", + "Invalid JSON format from repository. Expected { version, essentials }.", + "error", + ); + Err("Invalid JSON format".to_string()) + } + } else { + let err_msg = format!("HTTP Error: {}", response.status()); + emit_log(app, "sync-essentials", "Error", &err_msg, "error"); + Err(err_msg) + } + } + Err(e) => { + if cache_path.exists() { + emit_log( + app, + "sync-essentials", + "Skipped", + &format!("Network issue: {}. Using local cache.", e), + "info", + ); + Ok(SyncEssentialsResult { + status: "cache_used".to_string(), + message: "网络不可用,已继续使用本地缓存".to_string(), + }) + } else { + let err_msg = format!("Network issue: {}", e); + emit_log(app, "sync-essentials", "Error", &err_msg, "error"); + Err(err_msg) + } + } + } +} + +pub fn get_essentials(app: &AppHandle) -> Option { + essentials_store::load_essentials(app) +} diff --git a/src-tauri/src/services/log_service.rs b/src-tauri/src/services/log_service.rs new file mode 100644 index 0000000..a616c00 --- /dev/null +++ b/src-tauri/src/services/log_service.rs @@ -0,0 +1,17 @@ +use tauri::{AppHandle, Emitter}; + +use crate::domain::models::LogPayload; + +pub fn emit_log(handle: &AppHandle, id: &str, command: &str, output: &str, status: &str) { + let now = chrono::Local::now().format("%H:%M:%S").to_string(); + let _ = handle.emit( + "log-event", + LogPayload { + id: id.to_string(), + timestamp: now, + command: command.to_string(), + output: output.to_string(), + status: status.to_string(), + }, + ); +} diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs new file mode 100644 index 0000000..be67adb --- /dev/null +++ b/src-tauri/src/services/mod.rs @@ -0,0 +1,4 @@ +pub mod essentials_service; +pub mod log_service; +pub mod settings_service; +pub mod software_state_service; diff --git a/src-tauri/src/services/settings_service.rs b/src-tauri/src/services/settings_service.rs new file mode 100644 index 0000000..849b0b6 --- /dev/null +++ b/src-tauri/src/services/settings_service.rs @@ -0,0 +1,12 @@ +use tauri::AppHandle; + +use crate::domain::models::AppSettings; +use crate::storage::settings_store; + +pub fn get_settings(app: &AppHandle) -> AppSettings { + settings_store::get_settings(app) +} + +pub fn save_settings(app: &AppHandle, settings: &AppSettings) -> Result<(), String> { + settings_store::save_settings(app, settings) +} diff --git a/src-tauri/src/services/software_state_service.rs b/src-tauri/src/services/software_state_service.rs new file mode 100644 index 0000000..9cda0dd --- /dev/null +++ b/src-tauri/src/services/software_state_service.rs @@ -0,0 +1,35 @@ +use tauri::AppHandle; + +use crate::providers::winget_client; +use crate::winget::Software; + +pub async fn initialize_app(app: AppHandle) -> Result { + let app_clone = app.clone(); + tokio::task::spawn_blocking(move || winget_client::ensure_environment_ready(&app_clone).map(|_| true)) + .await + .unwrap_or(Err("Initialization Task Panicked".to_string())) +} + +pub async fn get_installed_software(app: AppHandle) -> Vec { + tokio::task::spawn_blocking(move || winget_client::list_installed_packages(&app)) + .await + .unwrap_or_default() +} + +pub async fn get_updates(app: AppHandle) -> Vec { + tokio::task::spawn_blocking(move || winget_client::list_upgrade_candidates(&app)) + .await + .unwrap_or_default() +} + +pub async fn get_software_info(app: AppHandle, id: String) -> Option { + tokio::task::spawn_blocking(move || winget_client::get_package_by_id(&app, &id)) + .await + .unwrap_or(None) +} + +pub async fn get_software_icon(app: AppHandle, id: String, name: String) -> Option { + tokio::task::spawn_blocking(move || winget_client::resolve_icon(&app, &id, &name)) + .await + .unwrap_or(None) +} diff --git a/src-tauri/src/storage/essentials_store.rs b/src-tauri/src/storage/essentials_store.rs new file mode 100644 index 0000000..d5b8ebc --- /dev/null +++ b/src-tauri/src/storage/essentials_store.rs @@ -0,0 +1,21 @@ +use std::fs; + +use tauri::AppHandle; + +use crate::domain::models::EssentialsRepo; +use crate::storage::paths::get_essentials_path; + +pub fn load_essentials(app: &AppHandle) -> Option { + let file_path = get_essentials_path(app); + if !file_path.exists() { + return None; + } + + let content = fs::read_to_string(file_path).unwrap_or_default(); + serde_json::from_str(&content).ok() +} + +pub fn save_essentials(app: &AppHandle, content: &str) -> Result<(), String> { + let path = get_essentials_path(app); + fs::write(path, content).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/storage/mod.rs b/src-tauri/src/storage/mod.rs new file mode 100644 index 0000000..38a5fef --- /dev/null +++ b/src-tauri/src/storage/mod.rs @@ -0,0 +1,3 @@ +pub mod essentials_store; +pub mod paths; +pub mod settings_store; diff --git a/src-tauri/src/storage/paths.rs b/src-tauri/src/storage/paths.rs new file mode 100644 index 0000000..3e38917 --- /dev/null +++ b/src-tauri/src/storage/paths.rs @@ -0,0 +1,20 @@ +use std::fs; +use std::path::PathBuf; + +use tauri::{AppHandle, Manager}; + +pub fn get_app_data_dir(app: &AppHandle) -> PathBuf { + let app_data_dir = app.path().app_data_dir().unwrap_or_default(); + if !app_data_dir.exists() { + let _ = fs::create_dir_all(&app_data_dir); + } + app_data_dir +} + +pub fn get_settings_path(app: &AppHandle) -> PathBuf { + get_app_data_dir(app).join("settings.json") +} + +pub fn get_essentials_path(app: &AppHandle) -> PathBuf { + get_app_data_dir(app).join("setup-essentials.json") +} diff --git a/src-tauri/src/storage/settings_store.rs b/src-tauri/src/storage/settings_store.rs new file mode 100644 index 0000000..ad0a18d --- /dev/null +++ b/src-tauri/src/storage/settings_store.rs @@ -0,0 +1,24 @@ +use std::fs; + +use tauri::AppHandle; + +use crate::domain::models::AppSettings; +use crate::storage::paths::get_settings_path; + +pub fn get_settings(app: &AppHandle) -> AppSettings { + let path = get_settings_path(app); + if !path.exists() { + let default_settings = AppSettings::default(); + let _ = fs::write(&path, serde_json::to_string_pretty(&default_settings).unwrap()); + return default_settings; + } + + let content = fs::read_to_string(path).unwrap_or_default(); + serde_json::from_str(&content).unwrap_or_default() +} + +pub fn save_settings(app: &AppHandle, settings: &AppSettings) -> Result<(), String> { + let path = get_settings_path(app); + let content = serde_json::to_string_pretty(settings).map_err(|e| e.to_string())?; + fs::write(path, content).map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/tasks/install_queue.rs b/src-tauri/src/tasks/install_queue.rs new file mode 100644 index 0000000..b14a458 --- /dev/null +++ b/src-tauri/src/tasks/install_queue.rs @@ -0,0 +1,641 @@ +use std::fs; +use std::io::{BufRead, BufReader, Read}; +use std::os::windows::process::CommandExt; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::thread; + +use regex::Regex; +use tauri::{AppHandle, Emitter, State}; +use tokio::sync::mpsc; +use winreg::enums::*; +use winreg::RegKey; + +use crate::domain::models::{InstallProgress, InstallTask}; +use crate::services::essentials_service; +use crate::services::log_service::emit_log; +use crate::winget::PostInstallStep; + +pub struct AppState { + pub install_tx: mpsc::Sender, +} + +pub fn create_install_state(handle: AppHandle) -> AppState { + let (tx, mut rx) = mpsc::channel::(100); + + tauri::async_runtime::spawn(async move { + let perc_re = Regex::new(r"(\d+)\s*%").unwrap(); + let size_re = Regex::new(r"([\d\.]+)\s*[a-zA-Z]+\s*/\s*([\d\.]+)\s*[a-zA-Z]+").unwrap(); + + while let Some(task) = rx.recv().await { + let task_id = task.id.clone(); + let task_version = task.version.clone(); + let use_manifest = task.use_manifest; + let manifest_url = task.manifest_url.clone(); + let enable_post_install_flag = task.enable_post_install; + + let log_id = format!("install-{}", task_id); + + let _ = handle.emit( + "install-status", + InstallProgress { + id: task_id.clone(), + status: "installing".to_string(), + progress: 0.0, + }, + ); + + let mut args = vec!["install".to_string()]; + let display_cmd: String; + let mut temp_manifest_path: Option = None; + + if use_manifest && manifest_url.is_some() { + let url = manifest_url.unwrap(); + display_cmd = format!("Winget Install (Manifest): {} from {}", task_id, url); + emit_log( + &handle, + &log_id, + &display_cmd, + "Downloading remote manifest...", + "info", + ); + + let client = reqwest::Client::new(); + match client + .get(&url) + .timeout(std::time::Duration::from_secs(15)) + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + if let Ok(content) = resp.text().await { + let temp_dir = std::env::temp_dir(); + let file_name = + format!("winget_tmp_{}.yaml", chrono::Local::now().timestamp_millis()); + let local_path = temp_dir.join(file_name); + if fs::write(&local_path, content).is_ok() { + args.push("--manifest".to_string()); + args.push(local_path.to_string_lossy().to_string()); + temp_manifest_path = Some(local_path); + } + } + } + _ => {} + } + + if temp_manifest_path.is_none() { + emit_log( + &handle, + &log_id, + "Error", + "Failed to download or save manifest.", + "error", + ); + let _ = handle.emit( + "install-status", + InstallProgress { + id: task_id.clone(), + status: "error".to_string(), + progress: 0.0, + }, + ); + continue; + } + } else { + args.push("--id".to_string()); + args.push(task_id.clone()); + args.push("-e".to_string()); + + if let Some(v) = &task_version { + if !v.is_empty() { + args.push("--version".to_string()); + args.push(v.clone()); + } + } + + display_cmd = match &task_version { + Some(v) if !v.is_empty() => format!("Winget Install: {} (v{})", task_id, v), + _ => format!("Winget Install: {}", task_id), + }; + } + + args.extend([ + "--silent".to_string(), + "--accept-package-agreements".to_string(), + "--accept-source-agreements".to_string(), + "--disable-interactivity".to_string(), + ]); + + let full_command = format!("winget {}", args.join(" ")); + emit_log( + &handle, + &log_id, + &display_cmd, + &format!("Executing: {}\n---", full_command), + "info", + ); + + let child = Command::new("winget") + .args(&args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .creation_flags(0x08000000) + .spawn(); + + let status_result = match child { + Ok(mut child_proc) => { + let stdout_handle = child_proc.stdout.take().map(|stdout| { + spawn_install_stream_reader( + stdout, + handle.clone(), + log_id.clone(), + task_id.clone(), + "stdout", + perc_re.clone(), + size_re.clone(), + ) + }); + let stderr_handle = child_proc.stderr.take().map(|stderr| { + spawn_install_stream_reader( + stderr, + handle.clone(), + log_id.clone(), + task_id.clone(), + "stderr", + perc_re.clone(), + size_re.clone(), + ) + }); + + let exit_status = child_proc.wait().map(|s| s.success()).unwrap_or(false); + if let Some(join_handle) = stdout_handle { + let _ = join_handle.join(); + } + if let Some(join_handle) = stderr_handle { + let _ = join_handle.join(); + } + let status_result = if exit_status { "success" } else { "error" }; + + if status_result == "success" && enable_post_install_flag { + let software_info = essentials_service::get_essentials(&handle) + .and_then(|repo| repo.essentials.into_iter().find(|s| s.id == task_id)); + + if let Some(sw) = software_info { + let mut final_steps = None; + if let Some(steps) = sw.post_install { + if !steps.is_empty() { + final_steps = Some(steps); + } + } else if let Some(url) = sw.post_install_url { + emit_log( + &handle, + &log_id, + "Post-Install", + "Local config not found, fetching remote config...", + "info", + ); + let client = reqwest::Client::new(); + if let Ok(resp) = client + .get(&url) + .timeout(std::time::Duration::from_secs(10)) + .send() + .await + { + if resp.status().is_success() { + if let Ok(text) = resp.text().await { + match serde_json::from_str::>(&text) { + Ok(steps) => { + emit_log( + &handle, + &log_id, + "Post-Install", + &format!( + "Successfully fetched remote config with {} steps.", + steps.len() + ), + "info", + ); + final_steps = Some(steps); + } + Err(e) => { + emit_log( + &handle, + &log_id, + "Post-Install Error", + &format!("JSON Parse Error: {}. Raw Content: {}", e, text), + "error", + ); + } + } + } + } else { + emit_log( + &handle, + &log_id, + "Post-Install Error", + &format!("Remote config HTTP Error: {}", resp.status()), + "error", + ); + } + } + } + + if let Some(steps) = final_steps { + let _ = handle.emit( + "install-status", + InstallProgress { + id: task_id.clone(), + status: "configuring".to_string(), + progress: 1.0, + }, + ); + emit_log( + &handle, + &log_id, + "Post-Install", + "Starting post-installation configuration...", + "info", + ); + if let Err(e) = execute_post_install(&handle, &log_id, steps).await { + emit_log(&handle, &log_id, "Post-Install Error", &e, "error"); + } else { + emit_log( + &handle, + &log_id, + "Post-Install", + "Post-installation configuration completed.", + "success", + ); + } + } + } + } + status_result + } + Err(e) => { + emit_log(&handle, &log_id, "Fatal Error", &e.to_string(), "error"); + "error" + } + }; + + let _ = handle.emit( + "install-status", + InstallProgress { + id: task_id.clone(), + status: status_result.to_string(), + progress: 1.0, + }, + ); + emit_log( + &handle, + &log_id, + "Result", + &format!("Execution finished: {}", status_result), + if status_result == "success" { + "success" + } else { + "error" + }, + ); + + if let Some(path) = temp_manifest_path { + let _ = fs::remove_file(path); + } + } + }); + + AppState { install_tx: tx } +} + +#[tauri::command] +pub async fn install_software( + task: InstallTask, + state: State<'_, AppState>, +) -> Result<(), String> { + state.install_tx.send(task).await.map_err(|e| e.to_string()) +} + +fn spawn_install_stream_reader( + reader: R, + handle: AppHandle, + log_id: String, + task_id: String, + stream_name: &'static str, + perc_re: Regex, + size_re: Regex, +) -> thread::JoinHandle<()> { + thread::spawn(move || { + let reader = BufReader::new(reader); + for line_res in reader.split(b'\r') { + if let Ok(line_bytes) = line_res { + let line_str = String::from_utf8_lossy(&line_bytes).to_string(); + let clean_line = line_str.trim(); + if clean_line.is_empty() { + continue; + } + + if stream_name == "stdout" { + let mut is_progress = false; + if let Some(caps) = perc_re.captures(clean_line) { + if let Ok(p_val) = caps[1].parse::() { + let _ = handle.emit( + "install-status", + InstallProgress { + id: task_id.clone(), + status: "installing".to_string(), + progress: p_val / 100.0, + }, + ); + is_progress = true; + } + } else if let Some(caps) = size_re.captures(clean_line) { + let current = caps[1].parse::().unwrap_or(0.0); + let total = caps[2].parse::().unwrap_or(1.0); + if total > 0.0 { + let _ = handle.emit( + "install-status", + InstallProgress { + id: task_id.clone(), + status: "installing".to_string(), + progress: (current / total).min(1.0), + }, + ); + is_progress = true; + } + } + + if !is_progress && clean_line.chars().count() > 1 { + emit_log(&handle, &log_id, "", clean_line, "info"); + } + } else { + emit_log(&handle, &log_id, stream_name, clean_line, "error"); + } + } + } + }) +} + +fn expand_win_path(path: &str) -> PathBuf { + let mut expanded = path.to_string(); + let env_vars = [ + "AppData", + "LocalAppData", + "ProgramData", + "SystemRoot", + "SystemDrive", + "TEMP", + "USERPROFILE", + "HOMEDRIVE", + "HOMEPATH", + ]; + + for var in env_vars { + let re = Regex::new(&format!(r"(?i)%{}%", var)).unwrap(); + if re.is_match(&expanded) { + if let Ok(val) = std::env::var(var) { + expanded = re.replace_all(&expanded, val.as_str()).to_string(); + } + } + } + PathBuf::from(expanded) +} + +async fn execute_post_install( + handle: &AppHandle, + log_id: &str, + steps: Vec, +) -> Result<(), String> { + let steps_len = steps.len(); + for (i, step) in steps.into_iter().enumerate() { + let step_prefix = format!("Step {}/{}: ", i + 1, steps_len); + + let delay = match &step { + PostInstallStep::RegistryBatch { delay_ms, .. } => *delay_ms, + PostInstallStep::FileCopy { delay_ms, .. } => *delay_ms, + PostInstallStep::FileDelete { delay_ms, .. } => *delay_ms, + PostInstallStep::Command { delay_ms, .. } => *delay_ms, + }; + + match step { + PostInstallStep::RegistryBatch { + root, + base_path, + values, + .. + } => { + emit_log( + handle, + log_id, + "Registry Update", + &format!("{}Applying batch registry settings to {}...", step_prefix, base_path), + "info", + ); + let hive = match root.as_str() { + "HKCU" => RegKey::predef(HKEY_CURRENT_USER), + "HKLM" => RegKey::predef(HKEY_LOCAL_MACHINE), + _ => { + emit_log( + handle, + log_id, + "Registry Error", + &format!("Unknown root hive: {}", root), + "error", + ); + continue; + } + }; + + match hive.create_subkey(&base_path) { + Ok((key, _)) => { + for (name, val) in values { + let res = match val.v_type.as_str() { + "String" => key.set_value(&name, &val.data.as_str().unwrap_or_default()), + "Dword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0) as u32)), + "Qword" => key.set_value(&name, &(val.data.as_u64().unwrap_or(0))), + "MultiString" => { + let strings: Vec = val + .data + .as_array() + .map(|a| { + a.iter() + .filter_map(|v| v.as_str().map(|s| s.to_string())) + .collect() + }) + .unwrap_or_default(); + key.set_value(&name, &strings) + } + "ExpandString" => { + key.set_value(&name, &val.data.as_str().unwrap_or_default()) + } + "Delete" => key.delete_value(&name), + _ => Err(std::io::Error::other("Unsupported type")), + }; + if let Err(e) = res { + emit_log( + handle, + log_id, + "Registry Error", + &format!("Failed to apply {}: {}", name, e), + "error", + ); + } + } + } + Err(e) => { + emit_log( + handle, + log_id, + "Registry Error", + &format!("Failed to create/open key {}: {}", base_path, e), + "error", + ); + } + } + } + PostInstallStep::FileCopy { src, dest, .. } => { + let dest_path = expand_win_path(&dest); + let src_is_url = src.starts_with("http://") || src.starts_with("https://"); + + if src_is_url { + emit_log( + handle, + log_id, + "File Download", + &format!("{}Downloading {:?} to {:?}...", step_prefix, src, dest_path), + "info", + ); + let client = reqwest::Client::new(); + match client + .get(&src) + .timeout(std::time::Duration::from_secs(60)) + .send() + .await + { + Ok(resp) if resp.status().is_success() => { + if let Ok(bytes) = resp.bytes().await { + if let Some(parent) = dest_path.parent() { + let _ = fs::create_dir_all(parent); + } + if let Err(e) = fs::write(&dest_path, bytes) { + emit_log( + handle, + log_id, + "File Error", + &format!("Failed to write to {:?}: {}", dest_path, e), + "error", + ); + } else { + emit_log( + handle, + log_id, + "Success", + "File downloaded and saved successfully.", + "success", + ); + } + } + } + Ok(resp) => emit_log( + handle, + log_id, + "Download Error", + &format!("HTTP Status: {}", resp.status()), + "error", + ), + Err(e) => emit_log(handle, log_id, "Download Error", &e.to_string(), "error"), + } + } else { + let src_path = expand_win_path(&src); + emit_log( + handle, + log_id, + "File Copy", + &format!("{}Copying {:?} to {:?}...", step_prefix, src_path, dest_path), + "info", + ); + if let Some(parent) = dest_path.parent() { + let _ = fs::create_dir_all(parent); + } + if let Err(e) = fs::copy(&src_path, &dest_path) { + emit_log( + handle, + log_id, + "File Error", + &format!("Failed to copy file: {}", e), + "error", + ); + } else { + emit_log(handle, log_id, "Success", "File copied successfully.", "success"); + } + } + } + PostInstallStep::FileDelete { path, .. } => { + let full_path = expand_win_path(&path); + emit_log( + handle, + log_id, + "File Delete", + &format!("{}Deleting {:?}...", step_prefix, full_path), + "info", + ); + if full_path.exists() { + if let Err(e) = fs::remove_file(&full_path) { + emit_log( + handle, + log_id, + "File Error", + &format!("Failed to delete file: {}", e), + "error", + ); + } else { + emit_log(handle, log_id, "Success", "File deleted successfully.", "success"); + } + } else { + emit_log(handle, log_id, "File Info", "File does not exist, skipping.", "info"); + } + } + PostInstallStep::Command { run, .. } => { + emit_log( + handle, + log_id, + "Command Execution", + &format!("{}Executing: {}", step_prefix, run), + "info", + ); + let output = Command::new("cmd") + .arg("/C") + .raw_arg(&run) + .creation_flags(0x08000000) + .output(); + + match output { + Ok(out) => { + if !out.status.success() { + let err = String::from_utf8_lossy(&out.stderr); + emit_log(handle, log_id, "Command Failed", &err, "error"); + } else { + emit_log(handle, log_id, "Success", "Command executed successfully.", "success"); + } + } + Err(e) => { + emit_log(handle, log_id, "Execution Error", &e.to_string(), "error"); + } + } + } + } + + if let Some(ms) = delay { + if ms > 0 { + emit_log( + handle, + log_id, + "Post-Install", + &format!("Waiting for {}ms...", ms), + "info", + ); + tokio::time::sleep(std::time::Duration::from_millis(ms)).await; + } + } + } + Ok(()) +} diff --git a/src-tauri/src/tasks/mod.rs b/src-tauri/src/tasks/mod.rs new file mode 100644 index 0000000..0e4775f --- /dev/null +++ b/src-tauri/src/tasks/mod.rs @@ -0,0 +1 @@ +pub mod install_queue; diff --git a/src-tauri/src/winget.rs b/src-tauri/src/winget.rs index d29db5d..b00093d 100644 --- a/src-tauri/src/winget.rs +++ b/src-tauri/src/winget.rs @@ -6,7 +6,7 @@ use std::os::windows::process::CommandExt; use std::collections::HashMap; use std::path::PathBuf; use tauri::{AppHandle, Manager}; -use crate::emit_log; +use crate::services::log_service::emit_log; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RegistryValue {