- Accounts: panel login/sign-up, Microsoft device-code sign-in, offline play; tokens encrypted with a key kept in the OS credential store - Home: instance rail, live server status, news feed, social links, Play button with download progress, speed and ETA - Full settings: accounts, memory/resolution/after-launch behaviour, Java and GC presets, Minecraft keybind editor applied to options.txt, theme overrides, storage manager, self-update via GitHub releases - Game console, crash dialog with hints, per-instance overrides - Live admin branding (colours, fonts, backgrounds, custom CSS) - Online install tests for vanilla/Fabric/Quilt/Forge/NeoForge (CI) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011ARcGWxLx21FwXJ3yfGriS
94 lines
2.9 KiB
Rust
94 lines
2.9 KiB
Rust
//! 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<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 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"));
|
|
}
|
|
}
|