Multi-Hoster-Upload-2/src-tauri/src/updater.rs
Claude 2958dca282 Add system tray, shutdown-after-finish scheduler, repoint updater
- src/tray.rs: system tray with show/hide/quit menu, left-click
    toggles main window visibility (minimize-to-tray parity with v1).
  - src/shutdown.rs: 60s countdown with per-second 'shutdown-countdown'
    event; sleep/shutdown/restart via rundll32/shutdown on Windows,
    cancel-aware.
  - cancel_shutdown + set_shutdown_after_finish commands now drive the
    scheduler so the renderer's existing countdown UI works unchanged.
  - Cargo features + tray-icon + image-png added.
  - Updater pointed at new Gitea repo Administrator/Multi-Hoster-Upload-2.
2026-04-20 18:32:59 +02:00

121 lines
4.3 KiB
Rust

//! Auto-updater — checks the Gitea releases endpoint for a newer version
//! than the one currently running. Downloads are opt-in (user clicks
//! "Install Update"), not automatic.
//!
//! Port of the v1 `lib/updater.js` semver-compare + Gitea polling flow.
use semver::Version;
use serde::{Deserialize, Serialize};
use std::time::Duration;
const REPO: &str = "Administrator/Multi-Hoster-Upload-2";
const BASE: &str = "https://git.24-music.de/api/v1/repos";
#[derive(Serialize, Default, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UpdateCheck {
pub available: bool,
pub current_version: String,
pub latest_version: Option<String>,
pub download_url: Option<String>,
pub release_notes: Option<String>,
}
#[derive(Deserialize)]
struct GiteaRelease {
tag_name: String,
body: Option<String>,
assets: Option<Vec<GiteaAsset>>,
}
#[derive(Deserialize)]
struct GiteaAsset {
name: String,
browser_download_url: String,
}
pub async fn download_and_launch(app: tauri::AppHandle) -> Result<(), String> {
let info = check().await;
if !info.available {
return Err("Kein Update verfügbar".into());
}
let url = info.download_url.ok_or("Kein Download-URL im Release")?;
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(5 * 60))
.user_agent("multi-hoster-upload/2.0")
.build().map_err(|e| e.to_string())?;
let resp = client.get(&url).send().await.map_err(|e| e.to_string())?;
if !resp.status().is_success() {
return Err(format!("Download HTTP {}", resp.status().as_u16()));
}
let bytes = resp.bytes().await.map_err(|e| e.to_string())?;
let tmp = std::env::temp_dir();
let file_name = url.rsplit('/').next().unwrap_or("Multi-Hoster-Upload-Setup.exe");
let target = tmp.join(format!("multi-hoster-update-{}-{file_name}",
chrono::Utc::now().timestamp()));
std::fs::write(&target, &bytes).map_err(|e| e.to_string())?;
// Notify UI and launch the installer detached. We then exit so the NSIS
// installer can replace the running exe.
let _ = tauri::Emitter::emit(&app, "app:update-installing",
&serde_json::json!({ "path": target.display().to_string() }));
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x00000008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
std::process::Command::new(&target)
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
.spawn()
.map_err(|e| e.to_string())?;
}
#[cfg(not(target_os = "windows"))]
{
std::process::Command::new(&target).spawn().map_err(|e| e.to_string())?;
}
tokio::time::sleep(Duration::from_millis(500)).await;
app.exit(0);
Ok(())
}
pub async fn check() -> UpdateCheck {
let current = env!("CARGO_PKG_VERSION").to_string();
let mut out = UpdateCheck {
available: false,
current_version: current.clone(),
..Default::default()
};
let Ok(client) = reqwest::Client::builder()
.timeout(Duration::from_secs(15))
.user_agent("multi-hoster-upload/2.0")
.build()
else { return out };
let url = format!("{BASE}/{REPO}/releases?limit=10");
let Ok(resp) = client.get(&url).send().await else { return out };
if !resp.status().is_success() { return out; }
let Ok(list) = resp.json::<Vec<GiteaRelease>>().await else { return out };
let Ok(current_v) = Version::parse(current.trim_start_matches('v')) else { return out };
for r in list {
let raw = r.tag_name.trim_start_matches('v');
let Ok(v) = Version::parse(raw) else { continue };
if v > current_v {
out.available = true;
out.latest_version = Some(r.tag_name.clone());
out.release_notes = r.body.clone();
// Prefer MSI > NSIS Setup > fallback.
let assets = r.assets.unwrap_or_default();
let url = assets.iter().find(|a| a.name.ends_with(".msi"))
.or_else(|| assets.iter().find(|a| a.name.to_lowercase().contains("setup.exe")))
.or_else(|| assets.iter().find(|a| a.name.ends_with(".exe")));
out.download_url = url.map(|a| a.browser_download_url.clone());
break;
}
}
out
}