// filepath: src-tauri/src/binary_manager.rs use std::fs; use std::path::PathBuf; use tauri::AppHandle; use anyhow::{Result, anyhow}; #[cfg(target_family = "unix")] use std::os::unix::fs::PermissionsExt; #[cfg(target_os = "windows")] use std::os::windows::process::CommandExt; use zip::ZipArchive; use std::io::Cursor; use crate::storage::{self}; const YT_DLP_REPO_URL: &str = "https://github.com/yt-dlp/yt-dlp/releases/latest/download"; // Bellard's QuickJS binary releases URL const QJS_REPO_URL: &str = "https://bellard.org/quickjs/binary_releases"; pub fn get_ytdlp_binary_name() -> &'static str { if cfg!(target_os = "windows") { "yt-dlp.exe" } else if cfg!(target_os = "macos") { "yt-dlp_macos" } else { "yt-dlp" } } // Target name on disk (for yt-dlp usage) pub fn get_qjs_binary_name() -> &'static str { if cfg!(target_os = "windows") { "quickjs.exe" } else { "quickjs" } } // Source name inside the zip archive (standard QuickJS naming) fn get_qjs_source_name_in_zip() -> &'static str { if cfg!(target_os = "windows") { "qjs.exe" } else { "qjs" } } // Get base directory for all binaries pub fn get_bin_dir(app: &AppHandle) -> Result { let app_data = storage::get_app_data_dir(app)?; let bin_dir = app_data.join("bin"); if !bin_dir.exists() { fs::create_dir_all(&bin_dir)?; } Ok(bin_dir) } pub fn get_ytdlp_path(app: &AppHandle) -> Result { Ok(get_bin_dir(app)?.join(get_ytdlp_binary_name())) } pub fn get_qjs_path(app: &AppHandle) -> Result { Ok(get_bin_dir(app)?.join(get_qjs_binary_name())) } pub fn check_binaries(app: &AppHandle) -> bool { let ytdlp = get_ytdlp_path(app).map(|p| p.exists()).unwrap_or(false); let qjs = get_qjs_path(app).map(|p| p.exists()).unwrap_or(false); ytdlp && qjs } // --- yt-dlp Logic --- pub async fn download_ytdlp(app: &AppHandle) -> Result { let binary_name = get_ytdlp_binary_name(); let url = format!("{}/{}", YT_DLP_REPO_URL, binary_name); let response = reqwest::get(&url).await?; if !response.status().is_success() { return Err(anyhow!("下载 yt-dlp 失败:状态 {}", response.status())); } let bytes = response.bytes().await?; let path = get_ytdlp_path(app)?; if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } fs::write(&path, bytes)?; #[cfg(target_family = "unix")] { let mut perms = fs::metadata(&path)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&path, perms)?; } Ok(path) } pub async fn update_ytdlp(app: &AppHandle) -> Result { let path = get_ytdlp_path(app)?; if !path.exists() { download_ytdlp(app).await?; return Ok("yt-dlp 已全新下载".to_string()); } // Use built-in update for yt-dlp let mut cmd = std::process::Command::new(&path); cmd.arg("-U"); #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); let output = cmd.output()?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr).to_string(); return Err(anyhow!("yt-dlp 更新失败:{}", stderr)); } // Update settings timestamp let mut settings = storage::load_settings(app)?; settings.last_updated = Some(chrono::Utc::now()); storage::save_settings(app, &settings)?; Ok("yt-dlp 已更新".to_string()) } pub fn get_ytdlp_version(app: &AppHandle) -> Result { let path = get_ytdlp_path(app)?; if !path.exists() { return Ok("未安装".to_string()); } let mut cmd = std::process::Command::new(&path); cmd.arg("--version"); #[cfg(target_os = "windows")] cmd.creation_flags(0x08000000); let output = cmd.output()?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) } else { Ok("未知".to_string()) } } // --- QuickJS Logic --- #[derive(serde::Deserialize)] struct LatestInfo { version: String, } pub async fn download_qjs(app: &AppHandle) -> Result { // 1. Fetch LATEST.json to get version info (though filenames seem predictable-ish, version helps) // Actually, looking at the file list, Bellard uses date-based versions. // Format: quickjs-win-x86_64-YYYY-MM-DD.zip // We need to find the correct filename dynamically or parse LATEST.json if it gave filenames. // The LATEST.json content was {"version":"2024-01-13"} (example from prompt context). // So we can construct the filename: quickjs-{platform}-{version}.zip let latest_url = format!("{}/LATEST.json", QJS_REPO_URL); let latest_resp = reqwest::get(&latest_url).await?; if !latest_resp.status().is_success() { return Err(anyhow!("获取 QuickJS 版本信息失败")); } let latest_info: LatestInfo = latest_resp.json().await?; let version = latest_info.version; // Construct filename based on OS/Arch let filename = if cfg!(target_os = "windows") { format!("quickjs-win-x86_64-{}.zip", version) } else if cfg!(target_os = "macos") { // NOTE: Cosmo builds are universal/portable for Linux/Mac usually? // Based on prompt instruction: "macos download quickjs-cosmo marked file" // Bellard lists: quickjs-cosmo-YYYY-MM-DD.zip format!("quickjs-cosmo-{}.zip", version) } else { return Err(anyhow!("不支持当前操作系统的 QuickJS 自动下载")); }; let download_url = format!("{}/{}", QJS_REPO_URL, filename); let response = reqwest::get(&download_url).await?; if !response.status().is_success() { return Err(anyhow!("下载 QuickJS 失败:状态 {}", response.status())); } let bytes = response.bytes().await?; let cursor = Cursor::new(bytes); let mut archive = ZipArchive::new(cursor)?; let bin_dir = get_bin_dir(app)?; // Extract logic: The zip from Bellard usually contains a folder or just binaries. // We need to find the `qjs` binary. // Windows zip usually has `qjs.exe` // Cosmo zip usually has `qjs`? Let's search for it. let source_name = get_qjs_source_name_in_zip(); let target_name = get_qjs_binary_name(); // quickjs.exe or quickjs let mut found_exe = false; for i in 0..archive.len() { let mut file = archive.by_index(i)?; // Filenames in zip might be like "quickjs-win-x86_64-2024-01-13/qjs.exe" let name = file.name().to_string(); // Skip directories if file.is_dir() { continue; } let filename_only = name.split('/').last().unwrap_or(""); if filename_only == source_name { let mut out_file = fs::File::create(bin_dir.join(target_name))?; std::io::copy(&mut file, &mut out_file)?; found_exe = true; } else if filename_only.ends_with(".dll") { // Extract DLLs (needed for Windows MinGW builds, e.g. libwinpthread-1.dll) let mut out_file = fs::File::create(bin_dir.join(filename_only))?; std::io::copy(&mut file, &mut out_file)?; } } if !found_exe { return Err(anyhow!("在下载的压缩包中找不到 {}", source_name)); } let final_path = get_qjs_path(app)?; #[cfg(target_family = "unix")] { let mut perms = fs::metadata(&final_path)?.permissions(); perms.set_mode(0o755); fs::set_permissions(&final_path, perms)?; } Ok(final_path) } pub async fn update_qjs(app: &AppHandle) -> Result { // QuickJS doesn't have self-update, so we just re-download download_qjs(app).await?; Ok("QuickJS 已更新/安装".to_string()) } pub fn get_qjs_version(app: &AppHandle) -> Result { let path = get_qjs_path(app)?; if !path.exists() { return Ok("未安装".to_string()); } Ok("已安装".to_string()) } pub async fn ensure_binaries(app: &AppHandle) -> Result<()> { let ytdlp = get_ytdlp_path(app)?; if !ytdlp.exists() { download_ytdlp(app).await?; } let qjs = get_qjs_path(app)?; if !qjs.exists() { download_qjs(app).await?; } Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_binary_names() { if cfg!(target_os = "windows") { assert_eq!(get_ytdlp_binary_name(), "yt-dlp.exe"); assert_eq!(get_qjs_binary_name(), "quickjs.exe"); assert_eq!(get_qjs_source_name_in_zip(), "qjs.exe"); } else if cfg!(target_os = "macos") { assert_eq!(get_ytdlp_binary_name(), "yt-dlp_macos"); assert_eq!(get_qjs_binary_name(), "quickjs"); assert_eq!(get_qjs_source_name_in_zip(), "qjs"); } } }