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
94 lines
3.2 KiB
Rust
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"));
|
|
}
|
|
}
|