Previously only the main sidecar binary got chmod 755. Now all files in the extraction directory get execute permissions — covers ffmpeg, ffprobe, and any other bundled binaries. Applied in three places: - sidecar/mod.rs: after local extraction - commands/sidecar.rs: after download extraction - commands/media.rs: removed single-file fix (now handled globally) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
259 lines
7.9 KiB
Rust
259 lines
7.9 KiB
Rust
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,
|
|
}
|
|
|
|
/// Read the locally installed sidecar version from `sidecar-version.txt`.
|
|
/// Returns `None` if the file doesn't exist or can't be read.
|
|
fn read_local_sidecar_version() -> Option<String> {
|
|
let data_dir = DATA_DIR.get()?;
|
|
let version_file = data_dir.join("sidecar-version.txt");
|
|
std::fs::read_to_string(version_file)
|
|
.ok()
|
|
.map(|v| v.trim().to_string())
|
|
.filter(|v| !v.is_empty())
|
|
}
|
|
|
|
/// Write the sidecar version to `sidecar-version.txt` after a successful download.
|
|
fn write_local_sidecar_version(version: &str) -> Result<(), String> {
|
|
let data_dir = DATA_DIR.get().ok_or("App data directory not initialized")?;
|
|
let version_file = data_dir.join("sidecar-version.txt");
|
|
std::fs::write(&version_file, version)
|
|
.map_err(|e| format!("Failed to write sidecar version file: {}", e))
|
|
}
|
|
|
|
/// Fetch releases from the Gitea API and find the latest sidecar release
|
|
/// (one whose tag_name starts with "sidecar-v").
|
|
async fn fetch_latest_sidecar_release(
|
|
client: &reqwest::Client,
|
|
) -> Result<serde_json::Value, String> {
|
|
let releases_url = format!("{}/releases?limit=20", REPO_API);
|
|
let resp = client
|
|
.get(&releases_url)
|
|
.header("Accept", "application/json")
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("Failed to fetch releases: {}", e))?;
|
|
|
|
if !resp.status().is_success() {
|
|
return Err(format!("Failed to fetch releases: HTTP {}", resp.status()));
|
|
}
|
|
|
|
let releases = resp
|
|
.json::<Vec<serde_json::Value>>()
|
|
.await
|
|
.map_err(|e| format!("Failed to parse releases JSON: {}", e))?;
|
|
|
|
releases
|
|
.into_iter()
|
|
.find(|r| {
|
|
r["tag_name"]
|
|
.as_str()
|
|
.map_or(false, |t| t.starts_with("sidecar-v"))
|
|
})
|
|
.ok_or_else(|| "No sidecar release found".to_string())
|
|
}
|
|
|
|
/// Extract the version string from a sidecar tag name (e.g. "sidecar-v1.0.1" -> "1.0.1").
|
|
fn version_from_sidecar_tag(tag: &str) -> &str {
|
|
tag.strip_prefix("sidecar-v").unwrap_or(tag)
|
|
}
|
|
|
|
/// Check if the sidecar binary exists for the currently installed version.
|
|
#[tauri::command]
|
|
pub fn check_sidecar() -> bool {
|
|
let data_dir = match DATA_DIR.get() {
|
|
Some(d) => d,
|
|
None => return false,
|
|
};
|
|
|
|
let version = match read_local_sidecar_version() {
|
|
Some(v) => v,
|
|
None => return false,
|
|
};
|
|
|
|
let binary_name = if cfg!(target_os = "windows") {
|
|
"voice-to-notes-sidecar.exe"
|
|
} else {
|
|
"voice-to-notes-sidecar"
|
|
};
|
|
|
|
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 os = platform_os();
|
|
let arch = platform_arch();
|
|
let asset_name = format!("sidecar-{}-{}-{}.zip", os, arch, variant);
|
|
|
|
// Fetch the latest sidecar release from Gitea API
|
|
let client = reqwest::Client::new();
|
|
let sidecar_release = fetch_latest_sidecar_release(&client).await?;
|
|
|
|
let tag = sidecar_release["tag_name"]
|
|
.as_str()
|
|
.ok_or("No tag_name in sidecar release")?;
|
|
let sidecar_version = version_from_sidecar_tag(tag).to_string();
|
|
|
|
// Find the matching asset
|
|
let assets = sidecar_release["assets"]
|
|
.as_array()
|
|
.ok_or("No assets found in sidecar 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 sidecar release {}",
|
|
asset_name, tag
|
|
)
|
|
})?
|
|
.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-{}", sidecar_version));
|
|
SidecarManager::extract_zip(&zip_path, &extract_dir)?;
|
|
|
|
// Make all binaries executable on Unix (sidecar, ffmpeg, ffprobe, etc.)
|
|
#[cfg(unix)]
|
|
{
|
|
use std::os::unix::fs::PermissionsExt;
|
|
if let Ok(entries) = std::fs::read_dir(&extract_dir) {
|
|
for entry in entries.flatten() {
|
|
let path = entry.path();
|
|
if path.is_file() {
|
|
if let Ok(meta) = std::fs::metadata(&path) {
|
|
let mut perms = meta.permissions();
|
|
perms.set_mode(0o755);
|
|
let _ = std::fs::set_permissions(&path, perms);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write the sidecar version file
|
|
write_local_sidecar_version(&sidecar_version)?;
|
|
|
|
// Clean up the zip file and old sidecar versions
|
|
let _ = std::fs::remove_file(&zip_path);
|
|
SidecarManager::cleanup_old_sidecars(data_dir, &sidecar_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 = match read_local_sidecar_version() {
|
|
Some(v) => v,
|
|
None => return Ok(None),
|
|
};
|
|
|
|
// Fetch latest sidecar release from Gitea API
|
|
let client = reqwest::Client::new();
|
|
let sidecar_release = fetch_latest_sidecar_release(&client).await?;
|
|
|
|
let latest_tag = sidecar_release["tag_name"]
|
|
.as_str()
|
|
.ok_or("No tag_name in sidecar release")?;
|
|
let latest_version = version_from_sidecar_tag(latest_tag);
|
|
|
|
if latest_version != current_version {
|
|
Ok(Some(UpdateInfo {
|
|
current_version,
|
|
latest_version: latest_version.to_string(),
|
|
}))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|