This commit is contained in:
107
src/updater.rs
Normal file
107
src/updater.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::Write;
|
||||
|
||||
const GITEA_BASE: &str = "https://gitea.whitlocktech.com";
|
||||
const REPO_OWNER: &str = "whitlocktech";
|
||||
const REPO_NAME: &str = "tickr";
|
||||
const ASSET_NAME: &str = "tickr.exe";
|
||||
|
||||
/// Check whether a newer release exists on Gitea.
|
||||
/// Returns `Some((tag_name, download_url))` when the remote version is strictly
|
||||
/// greater than the running binary, `None` when already up-to-date.
|
||||
pub async fn check_for_update() -> Result<Option<(String, String)>> {
|
||||
let url = format!(
|
||||
"{GITEA_BASE}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/releases/latest"
|
||||
);
|
||||
|
||||
let resp = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.header("User-Agent", format!("tickr/{}", crate::VERSION))
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to reach Gitea")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
// 404 = no releases yet; treat as up-to-date rather than an error
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await.context("Invalid JSON from Gitea")?;
|
||||
|
||||
let tag = body["tag_name"]
|
||||
.as_str()
|
||||
.context("Missing tag_name in release")?
|
||||
.to_string();
|
||||
|
||||
let download_url = body["assets"]
|
||||
.as_array()
|
||||
.and_then(|assets| {
|
||||
assets
|
||||
.iter()
|
||||
.find(|a| a["name"].as_str() == Some(ASSET_NAME))
|
||||
.and_then(|a| a["browser_download_url"].as_str())
|
||||
})
|
||||
.context("Asset not found in release")?
|
||||
.to_string();
|
||||
|
||||
// Strip leading 'v' and compare semver
|
||||
let remote_str = tag.trim_start_matches('v');
|
||||
let remote_ver = semver::Version::parse(remote_str)
|
||||
.with_context(|| format!("Bad remote version: {remote_str}"))?;
|
||||
let local_ver = semver::Version::parse(crate::VERSION)
|
||||
.with_context(|| format!("Bad local version: {}", crate::VERSION))?;
|
||||
|
||||
if remote_ver > local_ver {
|
||||
Ok(Some((tag, download_url)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Download the new binary and schedule a swap on next launch.
|
||||
///
|
||||
/// The download is streamed in chunks to avoid loading the whole binary into
|
||||
/// memory. A small bat script is written alongside it; the bat waits 2 seconds
|
||||
/// for the current process to exit, moves the new exe into place, relaunches
|
||||
/// it, then deletes itself.
|
||||
pub async fn download_and_swap(download_url: &str) -> Result<()> {
|
||||
let exe_path = std::env::current_exe().context("Cannot resolve current exe path")?;
|
||||
let exe_dir = exe_path.parent().context("Cannot resolve exe directory")?;
|
||||
|
||||
let update_path = exe_dir.join("tickr_update.exe");
|
||||
let bat_path = exe_dir.join("tickr_swap.bat");
|
||||
|
||||
// ── Stream download ───────────────────────────────────────────────────
|
||||
let mut resp = reqwest::Client::new()
|
||||
.get(download_url)
|
||||
.header("User-Agent", format!("tickr/{}", crate::VERSION))
|
||||
.send()
|
||||
.await
|
||||
.context("Download request failed")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("Download returned HTTP {}", resp.status());
|
||||
}
|
||||
|
||||
let mut file = std::fs::File::create(&update_path)
|
||||
.with_context(|| format!("Cannot create {}", update_path.display()))?;
|
||||
|
||||
while let Some(chunk) = resp.chunk().await.context("Download interrupted")? {
|
||||
file.write_all(&chunk).context("Write error during download")?;
|
||||
}
|
||||
drop(file);
|
||||
|
||||
// ── Write swap bat ────────────────────────────────────────────────────
|
||||
let bat = "@echo off\r\ntimeout /t 2 /nobreak >nul\r\nmove /y tickr_update.exe tickr.exe\r\nstart \"\" tickr.exe\r\ndel \"%~f0\"\r\n";
|
||||
std::fs::write(&bat_path, bat)
|
||||
.with_context(|| format!("Cannot write {}", bat_path.display()))?;
|
||||
|
||||
// ── Launch bat detached and exit ──────────────────────────────────────
|
||||
std::process::Command::new("cmd")
|
||||
.args(["/c", "tickr_swap.bat"])
|
||||
.current_dir(exe_dir)
|
||||
.spawn()
|
||||
.context("Failed to launch swap bat")?;
|
||||
|
||||
std::process::exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user