//! Auto-updater — checks the Gitea releases endpoint for a newer version //! than the one currently running. Downloads are opt-in (user clicks //! "Install Update"), not automatic. //! //! Port of the v1 `lib/updater.js` semver-compare + Gitea polling flow. use semver::Version; use serde::{Deserialize, Serialize}; use std::time::Duration; const REPO: &str = "Administrator/Multi-Hoster-Upload-2"; const BASE: &str = "https://git.24-music.de/api/v1/repos"; #[derive(Serialize, Default, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct UpdateCheck { pub available: bool, pub current_version: String, pub latest_version: Option, pub download_url: Option, pub release_notes: Option, } #[derive(Deserialize)] struct GiteaRelease { tag_name: String, body: Option, assets: Option>, } #[derive(Deserialize)] struct GiteaAsset { name: String, browser_download_url: String, } pub async fn download_and_launch(app: tauri::AppHandle) -> Result<(), String> { let info = check().await; if !info.available { return Err("Kein Update verfügbar".into()); } let url = info.download_url.ok_or("Kein Download-URL im Release")?; let client = reqwest::Client::builder() .timeout(Duration::from_secs(5 * 60)) .user_agent("multi-hoster-upload/2.0") .build().map_err(|e| e.to_string())?; let resp = client.get(&url).send().await.map_err(|e| e.to_string())?; if !resp.status().is_success() { return Err(format!("Download HTTP {}", resp.status().as_u16())); } let bytes = resp.bytes().await.map_err(|e| e.to_string())?; let tmp = std::env::temp_dir(); let file_name = url.rsplit('/').next().unwrap_or("Multi-Hoster-Upload-Setup.exe"); let target = tmp.join(format!("multi-hoster-update-{}-{file_name}", chrono::Utc::now().timestamp())); std::fs::write(&target, &bytes).map_err(|e| e.to_string())?; // Notify UI and launch the installer detached. We then exit so the NSIS // installer can replace the running exe. let _ = tauri::Emitter::emit(&app, "app:update-installing", &serde_json::json!({ "path": target.display().to_string() })); #[cfg(target_os = "windows")] { use std::os::windows::process::CommandExt; const DETACHED_PROCESS: u32 = 0x00000008; const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200; std::process::Command::new(&target) .creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP) .spawn() .map_err(|e| e.to_string())?; } #[cfg(not(target_os = "windows"))] { std::process::Command::new(&target).spawn().map_err(|e| e.to_string())?; } tokio::time::sleep(Duration::from_millis(500)).await; app.exit(0); Ok(()) } pub async fn check() -> UpdateCheck { let current = env!("CARGO_PKG_VERSION").to_string(); let mut out = UpdateCheck { available: false, current_version: current.clone(), ..Default::default() }; let Ok(client) = reqwest::Client::builder() .timeout(Duration::from_secs(15)) .user_agent("multi-hoster-upload/2.0") .build() else { return out }; let url = format!("{BASE}/{REPO}/releases?limit=10"); let Ok(resp) = client.get(&url).send().await else { return out }; if !resp.status().is_success() { return out; } let Ok(list) = resp.json::>().await else { return out }; let Ok(current_v) = Version::parse(current.trim_start_matches('v')) else { return out }; for r in list { let raw = r.tag_name.trim_start_matches('v'); let Ok(v) = Version::parse(raw) else { continue }; if v > current_v { out.available = true; out.latest_version = Some(r.tag_name.clone()); out.release_notes = r.body.clone(); // Prefer MSI > NSIS Setup > fallback. let assets = r.assets.unwrap_or_default(); let url = assets.iter().find(|a| a.name.ends_with(".msi")) .or_else(|| assets.iter().find(|a| a.name.to_lowercase().contains("setup.exe"))) .or_else(|| assets.iter().find(|a| a.name.ends_with(".exe"))); out.download_url = url.map(|a| a.browser_download_url.clone()); break; } } out }