//! Self-update via GitHub Releases: find a newer `*-setup.exe`, download //! it and hand over to the installer. use crate::state::build; use anyhow::{anyhow, Context, Result}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize)] pub struct UpdateInfo { pub version: String, pub notes: String, pub url: String, pub size: u64, } #[derive(Deserialize)] struct Release { tag_name: String, #[serde(default)] body: String, #[serde(default)] draft: bool, #[serde(default)] prerelease: bool, assets: Vec, } #[derive(Deserialize)] struct Asset { name: String, browser_download_url: String, size: u64, } fn parse(v: &str) -> Vec { v.trim_start_matches('v').split(['.', '-', '+']).map_while(|p| p.parse().ok()).collect() } pub fn is_newer(candidate: &str, current: &str) -> bool { parse(candidate) > parse(current) } pub async fn check(http: &reqwest::Client) -> Result> { let Some(repo) = build::REPO.filter(|r| !r.is_empty()) else { return Ok(None) }; let release: Release = http .get(format!("https://api.github.com/repos/{repo}/releases/latest")) .header("Accept", "application/vnd.github+json") .send() .await? .error_for_status()? .json() .await?; if release.draft || release.prerelease || !is_newer(&release.tag_name, env!("CARGO_PKG_VERSION")) { return Ok(None); } let asset = release .assets .iter() .find(|a| a.name.to_lowercase().ends_with("-setup.exe")) .ok_or_else(|| anyhow!("release {} has no installer", release.tag_name))?; Ok(Some(UpdateInfo { version: release.tag_name.trim_start_matches('v').to_string(), notes: release.body, url: asset.browser_download_url.clone(), size: asset.size, })) } /// Download the installer and start it. The caller exits the app afterwards. pub async fn download_and_run(http: &reqwest::Client, url: &str) -> Result<()> { if !url.starts_with("https://github.com/") && !url.starts_with("https://objects.githubusercontent.com/") { anyhow::bail!("refusing to download an update from an unexpected host"); } let dest = std::env::temp_dir().join("scopenet-launcher-setup.exe"); let dl = scopenet_core::http::Download::new(url, dest.clone(), None, None); scopenet_core::http::download_one(http, &dl, &|_| {}).await.context("downloading update")?; // /P = passive (progress only), /R = relaunch when done (Tauri NSIS). std::process::Command::new(&dest).args(["/P", "/R"]).spawn().context("starting the installer")?; Ok(()) } #[cfg(test)] mod tests { use super::*; #[test] fn versions() { assert!(is_newer("v0.2.0", "0.1.9")); assert!(is_newer("1.0.0", "0.9.12")); assert!(!is_newer("v0.1.0", "0.1.0")); assert!(!is_newer("0.1.0-beta", "0.1.0")); } }