Files
voice-to-notes/src-tauri/src/commands/sidecar.rs

212 lines
6.3 KiB
Rust
Raw Normal View History

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: serde_json::Value = 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::get(&download_url)
.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 = 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 = 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<Option<UpdateInfo>, 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: serde_json::Value = 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)
}
}