Files
SCOPENET-MC/launcher/src-tauri/src/updater.rs
T
SCOPEDD 7809d47d39
CI / Real Minecraft installs (manual) (push) Skipped
CI / Rust, web and container (push) Canceled after 0s
CI / Windows launcher (push) Canceled after 0s
Server integrations / paper (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.20.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 1.21.1 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.1.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.2 (push) Canceled after 0s
Server integrations / Fabric and Forge / 26.3 (push) Canceled after 0s
Migrate CI to Gitea and improve live map diagnostics
2026-10-01 00:07:35 -04:00

94 lines
3.2 KiB
Rust

//! Self-update via Gitea 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<Asset>,
}
#[derive(Deserialize)]
struct Asset {
name: String,
browser_download_url: String,
size: u64,
}
fn parse(v: &str) -> Vec<u64> {
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<Option<UpdateInfo>> {
let Some(repo) = build::REPO.filter(|r| !r.is_empty()) else { return Ok(None) };
let repo = reqwest::Url::parse(repo).context("invalid release repository URL")?;
if repo.scheme() != "https" {
anyhow::bail!("release repository must use HTTPS");
}
let api = format!("{}/api/v1/repos{}/releases/latest", repo.origin().ascii_serialization(), repo.path());
let release: Release = http.get(api).header("Accept", "application/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<()> {
let expected = reqwest::Url::parse(build::REPO.unwrap_or_default()).context("invalid release repository URL")?;
let actual = reqwest::Url::parse(url).context("invalid update URL")?;
if actual.scheme() != "https" || actual.origin() != expected.origin() {
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"));
}
}