2026-03-22 07:09:08 -07:00
|
|
|
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()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 07:50:53 -07:00
|
|
|
let release_json = release_resp
|
|
|
|
|
.json::<serde_json::Value>()
|
2026-03-22 07:09:08 -07:00
|
|
|
.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
|
2026-03-22 07:50:53 -07:00
|
|
|
let response: reqwest::Response = client
|
|
|
|
|
.get(&download_url)
|
|
|
|
|
.send()
|
2026-03-22 07:09:08 -07:00
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Failed to start download: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
return Err(format!("Download failed: HTTP {}", response.status()));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 07:50:53 -07:00
|
|
|
let total: u64 = response.content_length().unwrap_or(0);
|
2026-03-22 07:09:08 -07:00
|
|
|
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 {
|
2026-03-22 07:50:53 -07:00
|
|
|
let chunk: bytes::Bytes =
|
|
|
|
|
chunk.map_err(|e| format!("Download stream error: {}", e))?;
|
2026-03-22 07:09:08 -07:00
|
|
|
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()
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-22 07:50:53 -07:00
|
|
|
let release_json = resp
|
|
|
|
|
.json::<serde_json::Value>()
|
2026-03-22 07:09:08 -07:00
|
|
|
.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)
|
|
|
|
|
}
|
|
|
|
|
}
|