use futures_util::StreamExt; use serde::Serialize; use std::io::Write; use tauri::{AppHandle, Emitter}; use crate::sidecar::{SidecarManager, DATA_DIR}; const REPO_API: &str = "https://repo.anhonesthost.net/api/v1/repos/MacroPad/voice-to-notes"; #[derive(Serialize, Clone)] struct DownloadProgress { downloaded: u64, total: u64, percent: u8, } #[derive(Serialize)] pub struct UpdateInfo { pub current_version: String, pub latest_version: String, } /// Check if the sidecar binary exists for the current version. #[tauri::command] pub fn check_sidecar() -> bool { let data_dir = match DATA_DIR.get() { Some(d) => d, None => return false, }; let binary_name = if cfg!(target_os = "windows") { "voice-to-notes-sidecar.exe" } else { "voice-to-notes-sidecar" }; let version = env!("CARGO_PKG_VERSION"); let extract_dir = data_dir.join(format!("sidecar-{}", version)); extract_dir.join(binary_name).exists() } /// Determine the current platform name for asset downloads. fn platform_os() -> &'static str { if cfg!(target_os = "windows") { "windows" } else if cfg!(target_os = "macos") { "macos" } else { "linux" } } /// Determine the current architecture name for asset downloads. fn platform_arch() -> &'static str { if cfg!(target_arch = "aarch64") { "aarch64" } else { "x86_64" } } /// Download the sidecar binary for the given variant (cpu or cuda). #[tauri::command] pub async fn download_sidecar(app: AppHandle, variant: String) -> Result<(), String> { let data_dir = DATA_DIR.get().ok_or("App data directory not initialized")?; let version = env!("CARGO_PKG_VERSION"); let os = platform_os(); let arch = platform_arch(); let asset_name = format!("sidecar-{}-{}-{}.zip", os, arch, variant); // Fetch release info from Gitea API to get the download URL let release_url = format!("{}/releases/tags/v{}", REPO_API, version); let client = reqwest::Client::new(); let release_resp = client .get(&release_url) .header("Accept", "application/json") .send() .await .map_err(|e| format!("Failed to fetch release info: {}", e))?; if !release_resp.status().is_success() { return Err(format!( "Failed to fetch release info: HTTP {}", release_resp.status() )); } let release_json = release_resp .json::() .await .map_err(|e| format!("Failed to parse release JSON: {}", e))?; // Find the matching asset let assets = release_json["assets"] .as_array() .ok_or("No assets found in release")?; let download_url = assets .iter() .find(|a| a["name"].as_str() == Some(&asset_name)) .and_then(|a| a["browser_download_url"].as_str()) .ok_or_else(|| format!("Asset '{}' not found in release v{}", asset_name, version))? .to_string(); // Stream download with progress events let response: reqwest::Response = client .get(&download_url) .send() .await .map_err(|e| format!("Failed to start download: {}", e))?; if !response.status().is_success() { return Err(format!("Download failed: HTTP {}", response.status())); } let total: u64 = response.content_length().unwrap_or(0); let mut downloaded: u64 = 0; let mut stream = response.bytes_stream(); let zip_path = data_dir.join("sidecar.zip"); let mut file = std::fs::File::create(&zip_path) .map_err(|e| format!("Failed to create zip file: {}", e))?; while let Some(chunk) = stream.next().await { let chunk: bytes::Bytes = chunk.map_err(|e| format!("Download stream error: {}", e))?; file.write_all(&chunk) .map_err(|e| format!("Failed to write chunk: {}", e))?; downloaded += chunk.len() as u64; let percent = if total > 0 { (downloaded * 100 / total) as u8 } else { 0 }; let _ = app.emit( "sidecar-download-progress", DownloadProgress { downloaded, total, percent, }, ); } // Extract the downloaded zip let extract_dir = data_dir.join(format!("sidecar-{}", version)); SidecarManager::extract_zip(&zip_path, &extract_dir)?; // Make the binary executable on Unix #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let binary_path = extract_dir.join("voice-to-notes-sidecar"); if let Ok(meta) = std::fs::metadata(&binary_path) { let mut perms = meta.permissions(); perms.set_mode(0o755); let _ = std::fs::set_permissions(&binary_path, perms); } } // Clean up the zip file and old sidecar versions let _ = std::fs::remove_file(&zip_path); SidecarManager::cleanup_old_sidecars(data_dir, version); Ok(()) } /// Check if a sidecar update is available. #[tauri::command] pub async fn check_sidecar_update() -> Result, String> { // If sidecar doesn't exist yet, return None (first launch handled separately) if !check_sidecar() { return Ok(None); } let current_version = env!("CARGO_PKG_VERSION").to_string(); // Fetch latest release from Gitea API let latest_url = format!("{}/releases/latest", REPO_API); let client = reqwest::Client::new(); let resp = client .get(&latest_url) .header("Accept", "application/json") .send() .await .map_err(|e| format!("Failed to fetch latest release: {}", e))?; if !resp.status().is_success() { return Err(format!( "Failed to fetch latest release: HTTP {}", resp.status() )); } let release_json = resp .json::() .await .map_err(|e| format!("Failed to parse release JSON: {}", e))?; let latest_tag = release_json["tag_name"] .as_str() .ok_or("No tag_name in release")?; // Strip leading 'v' if present let latest_version = latest_tag.strip_prefix('v').unwrap_or(latest_tag); if latest_version != current_version { Ok(Some(UpdateInfo { current_version, latest_version: latest_version.to_string(), })) } else { Ok(None) } }