feat(desktop): Update in der App — Herunterladen, Installieren und Neustart per Klick im Infobereich (tauri-plugin-updater)
- tauri-plugin-updater 2.11 + semver eingebunden; pubkey, installMode passive und createUpdaterArtifacts in tauri.conf.json (kein endpoints, keine dangerous*-Schalter) - Versionspruefung laeuft ueber updater_builder().check() gegen /api-proxy/desktop/update mit eigenem version_comparator (is_update_newer: hoehere Basis oder anderer Beta-Stempel beta.g<sha7>; gleiche Basis Live → kein Update); 15 s Pruefung, 600 s Download-Timeout - Tray-Eintrag "Update installieren" laedt mit Fortschritt im Menuetext, prueft die minisign-Signatur, installiert (Windows NSIS passiv, Linux AppImage an Ort und Stelle) und startet neu; Fehler → Benachrichtigung und Rueckfall auf die Einstellungsseite im Browser - http-Server im Release: Menuetext "Update nur über https möglich", gesperrt - 15 neue Tests, 2 umgestellt (33 gesamt); gen/schemas vom Bau nachgezogen Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
use semver::Version;
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use tauri::{
|
||||
menu::{CheckMenuItemBuilder, MenuBuilder, MenuItem, MenuItemBuilder},
|
||||
@@ -8,24 +10,32 @@ use tauri_plugin_autostart::{MacosLauncher, ManagerExt};
|
||||
use tauri_plugin_notification::NotificationExt;
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tauri_plugin_store::StoreExt;
|
||||
use tauri_plugin_updater::{Update, UpdaterExt};
|
||||
|
||||
/// Standardtext des Update-Eintrags im Tray-Menü, bevor eine neuere Version
|
||||
/// gefunden wurde. `spawn_version_check` setzt den Eintrag nach einem
|
||||
/// Serverwechsel hierauf zurück, damit kein Hinweis des alten Servers stehen
|
||||
/// bleibt.
|
||||
const UPDATE_ITEM_DEFAULT: &str = "Update herunterladen";
|
||||
/// gefunden wurde. Der Eintrag installiert das Update seit quick-260917-kgc
|
||||
/// direkt in der App (Download, Signaturpruefung, Installation, Neustart);
|
||||
/// den Browser oeffnet er nur noch als Rueckfall. `spawn_version_check`
|
||||
/// setzt den Eintrag nach einem Serverwechsel hierauf zurück, damit kein
|
||||
/// Hinweis des alten Servers stehen bleibt.
|
||||
const UPDATE_ITEM_DEFAULT: &str = "Update installieren";
|
||||
|
||||
/// Menuetext, wenn die gespeicherte Server-Adresse mit `http://` beginnt:
|
||||
/// das Updater-Plugin verweigert im Release-Bau unverschluesselte Endpunkte
|
||||
/// (`Error::InsecureTransportProtocol`). Der Eintrag bleibt gesperrt, der
|
||||
/// Text erklaert dauerhaft, warum -- keine Benachrichtigung bei jedem Start.
|
||||
const UPDATE_ITEM_INSECURE: &str = "Update nur über https möglich";
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct VersionResponse {
|
||||
version: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct DesktopLatest {
|
||||
version: String,
|
||||
channel: String,
|
||||
commit: String,
|
||||
}
|
||||
/// Gefundenes Update samt dem Menuetext, der zu ihm gehoert. Wird von
|
||||
/// `spawn_version_check` befuellt und vom Tray-Klick "update" per `take()`
|
||||
/// entnommen -- so kann ein Doppelklick keinen zweiten Download starten.
|
||||
/// `Update` ist Clone + Send + Sync, `app.manage` verlangt das.
|
||||
struct PendingUpdate(Mutex<Option<(Update, String)>>);
|
||||
|
||||
/// Baut die Adresse eines API-Pfads aus der gespeicherten Server-Adresse.
|
||||
/// Die API ist vom Client nur ueber den Web-Ursprung erreichbar
|
||||
@@ -41,10 +51,10 @@ fn api_url(server: &str, path: &str) -> String {
|
||||
/// NUR in die Navigation, niemals in den gespeicherten Server-Wert
|
||||
/// (`server_url` im Store bleibt die reine Adresse); daraus liest die
|
||||
/// Next.js-Middleware (apps/web/src/middleware.ts) das Cookie
|
||||
/// `tessera_desktop`, an dem der Web-Client den Desktop-Client erkennt. Die
|
||||
/// Tray-URL "Update herunterladen" bekommt den Parameter bewusst NICHT --
|
||||
/// sie oeffnet im System-Browser, dort muessen die Download-Links sichtbar
|
||||
/// bleiben.
|
||||
/// `tessera_desktop`, an dem der Web-Client den Desktop-Client erkennt. Der
|
||||
/// Browser-Rueckfall des Tray-Eintrags "Update installieren"
|
||||
/// (`open_download_page`) bekommt den Parameter bewusst NICHT -- er oeffnet
|
||||
/// im System-Browser, dort muessen die Download-Links sichtbar bleiben.
|
||||
fn with_desktop_marker(url: &tauri::Url) -> tauri::Url {
|
||||
let mut marked = url.clone();
|
||||
marked.query_pairs_mut().append_pair("desktop", "1");
|
||||
@@ -54,29 +64,105 @@ fn with_desktop_marker(url: &tauri::Url) -> tauri::Url {
|
||||
/// Liefert (Menuetext, Benachrichtigungstext) fuer den Update-Hinweis. Der
|
||||
/// Beta-Kanal (main) vergibt jedem Commit dieselbe X.Y.Z-Version (D-07,
|
||||
/// desktop-collect.sh) -- hat sich nur der Commit geaendert, waere der reine
|
||||
/// Versionstext ("Version 1.1.0 herunterladen") verwirrend, weil der Client
|
||||
/// bereits genau diese Version hat. Der zweite Zweig nennt darum den
|
||||
/// Commit-Stempel statt der unveraenderten Version.
|
||||
/// Versionstext ("Auf Version 1.1.0 aktualisieren") verwirrend, weil der
|
||||
/// Client bereits genau diese Version hat. Der zweite Zweig nennt darum den
|
||||
/// Commit-Stempel statt der unveraenderten Version. Die Texte sprechen vom
|
||||
/// Aktualisieren, nicht vom Herunterladen: der Klick installiert das Update
|
||||
/// in der App (quick-260917-kgc).
|
||||
fn update_labels(version_changed: bool, version: &str, commit: &str) -> (String, String) {
|
||||
if version_changed {
|
||||
(
|
||||
format!("Version {} herunterladen", version),
|
||||
format!("Auf Version {} aktualisieren", version),
|
||||
format!(
|
||||
"Neue Version {} verfügbar – Download über das Symbol im Infobereich.",
|
||||
"Neue Version {} verfügbar – Aktualisieren über das Symbol im Infobereich.",
|
||||
version
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
"Neuen Beta-Stand herunterladen".to_string(),
|
||||
format!("Auf Beta-Stand {} aktualisieren", commit),
|
||||
format!(
|
||||
"Neuer Beta-Stand {} verfügbar – Download über das Symbol im Infobereich.",
|
||||
"Neuer Beta-Stand {} verfügbar – Aktualisieren über das Symbol im Infobereich.",
|
||||
commit
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Liest den Beta-Stempel `beta.g<sha7>` aus dem Prerelease-Teil einer
|
||||
/// Server-Version (`1.2.0-beta.gabc1234` → `abc1234`). Das `g` ist Pflicht:
|
||||
/// ein rein numerischer SHA mit fuehrender Null (`0123456`) waere kein
|
||||
/// gueltiger SemVer-Identifier, `check()` endete dann mit Err (Recherche
|
||||
/// Pitfall 2). Live-Versionen ohne Prerelease und fremde Formate (`rc.1`)
|
||||
/// liefern None.
|
||||
fn beta_commit(release: &Version) -> Option<&str> {
|
||||
release
|
||||
.pre
|
||||
.as_str()
|
||||
.strip_prefix("beta.g")
|
||||
.filter(|sha| !sha.is_empty())
|
||||
}
|
||||
|
||||
/// Vergleichsregel des Updaters -- ersetzt den Standardvergleich des Plugins
|
||||
/// vollstaendig (updater.rs Z. 576-579; SemVer allein saehe
|
||||
/// `1.2.0-beta.gXXXX < 1.2.0` und ein Beta-Client bekaeme nie einen neueren
|
||||
/// Beta-Bau). Regel:
|
||||
/// - Basisversion X.Y.Z des Servers groesser → Update; kleiner → keins.
|
||||
/// - Gleiche Basis mit Beta-Stempel `beta.g<sha7>` → Update genau dann,
|
||||
/// wenn der Client einen Commit-Stempel kennt und er sich unterscheidet.
|
||||
/// Der Beta-Kanal (main) vergibt jedem Commit dieselbe X.Y.Z-Version
|
||||
/// (D-07); ohne den Commit-Vergleich saehe ein Beta-Client zwischen zwei
|
||||
/// Freigabe-Tags nie einen neueren Bau (WR-02, Code-Review Phase 18).
|
||||
/// - Gleiche Basis ohne Prerelease (Live-Freigabe) → KEIN Update. Das ist die
|
||||
/// bisherige Regel: ein Live-Client 1.2.0 bietet sich nie selbst 1.2.0 an
|
||||
/// (keine Update-Schleife). Der Fall "Beta-Client wird auf einen Live-Server
|
||||
/// derselben Basis umgestellt" ist client-seitig nicht entscheidbar (der
|
||||
/// Client kennt nur CARGO_PKG_VERSION und APP_COMMIT, nicht den Tag-Commit
|
||||
/// der Freigabe) und bekommt das Update mit der naechsten Freigabe.
|
||||
/// - Leerer Client-Commit (Quell-Tarball ohne git) → nur Basisvergleich.
|
||||
fn is_update_newer(current: &Version, current_commit: &str, release: &Version) -> bool {
|
||||
let base = |v: &Version| (v.major, v.minor, v.patch);
|
||||
if base(release) > base(current) {
|
||||
return true;
|
||||
}
|
||||
if base(release) < base(current) {
|
||||
return false;
|
||||
}
|
||||
match beta_commit(release) {
|
||||
Some(sha) => !current_commit.is_empty() && sha != current_commit,
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Baut die Endpunkt-Adresse fuer das Updater-Plugin aus der gespeicherten
|
||||
/// Server-Adresse. Die Platzhalter `{{target}}`, `{{arch}}` und
|
||||
/// `{{current_version}}` stehen als Rohtext im Wert; `query_pairs_mut`
|
||||
/// kodiert sie zu `%7B%7B…%7D%7D`, und das Plugin ersetzt beide Schreibweisen
|
||||
/// (updater.rs Z. 476-486). `base` ist der Origin, aus dem die API die
|
||||
/// absolute Download-Adresse bildet -- das Plugin verlangt eine `url::Url`,
|
||||
/// relative Pfade scheitern. `api_url` bleibt die einzige Stelle mit dem
|
||||
/// `/api-proxy`-Praefix.
|
||||
fn update_endpoint(server: &str) -> Option<tauri::Url> {
|
||||
let mut url = tauri::Url::parse(&api_url(server, "/desktop/update")).ok()?;
|
||||
url.query_pairs_mut()
|
||||
.append_pair("target", "{{target}}")
|
||||
.append_pair("arch", "{{arch}}")
|
||||
.append_pair("current", "{{current_version}}")
|
||||
.append_pair("base", server.trim_end_matches('/'));
|
||||
Some(url)
|
||||
}
|
||||
|
||||
/// Leitet (Menuetext, Benachrichtigungstext) aus Client- und Server-Version
|
||||
/// ab: eine neue Basis X.Y.Z wird als Version genannt (auch bei Beta), bei
|
||||
/// gleicher Basis der Beta-Stempel.
|
||||
fn release_labels(current: &Version, release: &Version) -> (String, String) {
|
||||
let base = |v: &Version| (v.major, v.minor, v.patch);
|
||||
let version_changed = base(release) != base(current);
|
||||
let version = format!("{}.{}.{}", release.major, release.minor, release.patch);
|
||||
let commit = beta_commit(release).unwrap_or("");
|
||||
update_labels(version_changed, &version, commit)
|
||||
}
|
||||
|
||||
/// Parst eine vom Nutzer eingegebene Server-Adresse und erlaubt nur http/https
|
||||
/// -- dieselben Fehlertexte wie bisher inline in `check_server`. `check_server`,
|
||||
/// `save_server_url` und `open_server` rufen ausschliesslich diesen Helfer;
|
||||
@@ -174,43 +260,159 @@ fn apply_server(app: &AppHandle, url: Option<&str>) {
|
||||
let _ = app.state::<TrayItems>().connected.set_text(line);
|
||||
}
|
||||
|
||||
/// Fragt den Update-Endpunkt ueber das Updater-Plugin ab. Der eigene
|
||||
/// `version_comparator` (`is_update_newer`) ersetzt den Standardvergleich
|
||||
/// des Plugins. Die 15 s gelten fuer die Pruefung; das Plugin uebernimmt
|
||||
/// denselben Wert als GESAMT-Timeout des Downloads (updater.rs Z. 698-700),
|
||||
/// darum setzt `spawn_version_check` vor der Ablage `update.timeout` neu.
|
||||
async fn check_for_update(
|
||||
app: &AppHandle,
|
||||
endpoint: tauri::Url,
|
||||
) -> tauri_plugin_updater::Result<Option<Update>> {
|
||||
let current_commit = env!("APP_COMMIT");
|
||||
app.updater_builder()
|
||||
.endpoints(vec![endpoint])?
|
||||
.timeout(Duration::from_secs(15))
|
||||
.version_comparator(move |current, release| {
|
||||
is_update_newer(¤t, current_commit, &release.version)
|
||||
})
|
||||
.build()?
|
||||
.check()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Versionsprüfung, aus `setup` herausgezogen, damit sie nach einem
|
||||
/// Serverwechsel erneut laufen kann (async, non-blocking wie bisher). Setzt
|
||||
/// den Update-Eintrag zuerst auf den Standardtext zurück -- nach einem
|
||||
/// Wechsel darf kein Hinweis des alten Servers stehen bleiben.
|
||||
/// den Update-Eintrag zuerst auf den Standardtext zurück und leert den
|
||||
/// abgelegten Update-Stand -- nach einem Wechsel darf kein Hinweis des alten
|
||||
/// Servers stehen bleiben.
|
||||
fn spawn_version_check(app: AppHandle, server_url: String) {
|
||||
let update_item = app.state::<TrayItems>().update.clone();
|
||||
let _ = update_item.set_text(UPDATE_ITEM_DEFAULT);
|
||||
let _ = update_item.set_enabled(false);
|
||||
if let Ok(mut pending) = app.state::<PendingUpdate>().0.lock() {
|
||||
*pending = None;
|
||||
}
|
||||
|
||||
// Gespeicherte Adressen sind immer parsebar (parse_server_url) -- reine
|
||||
// Absicherung.
|
||||
let Some(endpoint) = update_endpoint(&server_url) else {
|
||||
return;
|
||||
};
|
||||
// Cargo garantiert eine gueltige SemVer-Version; desktop-version.sh
|
||||
// schreibt immer reines X.Y.Z (D-07).
|
||||
let current = Version::parse(env!("CARGO_PKG_VERSION"))
|
||||
.expect("CARGO_PKG_VERSION muss eine gueltige SemVer-Version sein");
|
||||
|
||||
let app_version = env!("CARGO_PKG_VERSION").to_string();
|
||||
let app_commit = env!("APP_COMMIT").to_string();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let url = api_url(&server_url, "/desktop/latest");
|
||||
if let Ok(resp) = reqwest::get(&url).await {
|
||||
if let Ok(info) = resp.json::<DesktopLatest>().await {
|
||||
// Beta-Kanal (main) vergibt jedem Commit dieselbe X.Y.Z-Version
|
||||
// (D-07, desktop-collect.sh) -- ohne den Commit-Vergleich saehe
|
||||
// ein Beta-Client zwischen zwei Freigabe-Tags nie einen neueren
|
||||
// Bau (WR-02, Code-Review Phase 18). Fuer den Live-Kanal bleibt
|
||||
// es beim reinen Versionsvergleich. Die Texte fuer Menue und
|
||||
// Benachrichtigung liefert `update_labels`.
|
||||
let version_changed = info.version != app_version;
|
||||
let is_newer =
|
||||
version_changed || (info.channel == "beta" && info.commit != app_commit);
|
||||
if is_newer {
|
||||
let (menu_text, body) =
|
||||
update_labels(version_changed, &info.version, &info.commit);
|
||||
let _ = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Tessera-Update")
|
||||
.body(body)
|
||||
.show();
|
||||
let _ = update_item.set_text(menu_text);
|
||||
let _ = update_item.set_enabled(true);
|
||||
match check_for_update(&app, endpoint).await {
|
||||
Ok(Some(mut update)) => {
|
||||
let (menu_text, body) = Version::parse(&update.version)
|
||||
.map(|release| release_labels(¤t, &release))
|
||||
.unwrap_or_else(|_| update_labels(true, &update.version, ""));
|
||||
// 15 s reichen fuer die Pruefung, nicht fuer den ~100-MB-Download:
|
||||
// das Plugin wendet `Update.timeout` als Gesamt-Timeout des
|
||||
// Downloads an (updater.rs Z. 698-700).
|
||||
update.timeout = Some(Duration::from_secs(600));
|
||||
let _ = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Tessera-Update")
|
||||
.body(body)
|
||||
.show();
|
||||
let _ = update_item.set_text(&menu_text);
|
||||
let _ = update_item.set_enabled(true);
|
||||
if let Ok(mut pending) = app.state::<PendingUpdate>().0.lock() {
|
||||
*pending = Some((update, menu_text));
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
// Der Release-Bau verweigert http-Endpunkte (config.rs
|
||||
// `validate_endpoints`), der Debug-Bau warnt nur. Keine
|
||||
// `dangerousInsecureTransportProtocol`-Freigabe (T-KGC-06): der
|
||||
// Eintrag bleibt gesperrt und erklaert den Grund.
|
||||
Err(tauri_plugin_updater::Error::InsecureTransportProtocol) => {
|
||||
let _ = update_item.set_text(UPDATE_ITEM_INSECURE);
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Oeffnet die Seite Einstellungen → Desktop-App im System-Browser -- der
|
||||
/// bisherige Weg des Tray-Eintrags, jetzt Rueckfall, wenn kein Update
|
||||
/// abgelegt ist oder die Installation in der App fehlschlaegt. Bewusst ohne
|
||||
/// `desktop=1` (siehe `with_desktop_marker`).
|
||||
fn open_download_page(app: &AppHandle) {
|
||||
if let Some(server) = stored_server_url(app) {
|
||||
let target = format!("{}/settings/general/desktop", server.trim_end_matches('/'));
|
||||
let _ = app.opener().open_url(target, None::<&str>);
|
||||
}
|
||||
}
|
||||
|
||||
/// Laedt das abgelegte Update, prueft die Signatur, installiert es und
|
||||
/// startet die App neu. Fortschritt und Zustand stehen im Menuetext des
|
||||
/// gesperrten Eintrags. Fehler: Menuetext und Stand zurueck, Eintrag wieder
|
||||
/// aktiv, Benachrichtigung mit Grund, Einstellungsseite im Browser als
|
||||
/// Rueckfall.
|
||||
fn spawn_update_install(app: AppHandle, update: Update, menu_text: String) {
|
||||
let item = app.state::<TrayItems>().update.clone();
|
||||
let _ = item.set_enabled(false);
|
||||
let _ = item.set_text("Update wird heruntergeladen…");
|
||||
let _ = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Tessera-Update")
|
||||
.body("Update wird heruntergeladen…")
|
||||
.show();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let item_progress = item.clone();
|
||||
let item_finish = item.clone();
|
||||
let mut received: u64 = 0;
|
||||
let mut last_percent: Option<u64> = None;
|
||||
let on_chunk = move |chunk: usize, total: Option<u64>| {
|
||||
received += chunk as u64;
|
||||
if let Some(total) = total.filter(|t| *t > 0) {
|
||||
let percent = (received * 100 / total).min(100);
|
||||
if last_percent != Some(percent) {
|
||||
last_percent = Some(percent);
|
||||
let _ = item_progress.set_text(format!("Lädt … {} %", percent));
|
||||
}
|
||||
}
|
||||
};
|
||||
let on_finish = move || {
|
||||
let _ = item_finish.set_text("Wird installiert…");
|
||||
};
|
||||
|
||||
match update.download_and_install(on_chunk, on_finish).await {
|
||||
Ok(()) => {
|
||||
// Windows: nie erreicht -- das Plugin startet den NSIS-Installer
|
||||
// mit `/P /UPDATE /R /ARGS` und beendet den Prozess per
|
||||
// `std::process::exit(0)`; der Run-Handler wird dabei nicht
|
||||
// durchlaufen. Linux: die AppImage ist ersetzt; `restart()` aus
|
||||
// dem Nebenthread laeuft ueber RESTART_EXIT_CODE
|
||||
// (`code: Some(..)`) am `prevent_exit` des Run-Handlers vorbei
|
||||
// (tauri app.rs Z. 588-611).
|
||||
app.restart();
|
||||
}
|
||||
Err(e) => {
|
||||
let _ = item.set_text(&menu_text);
|
||||
if let Ok(mut pending) = app.state::<PendingUpdate>().0.lock() {
|
||||
*pending = Some((update, menu_text.clone()));
|
||||
}
|
||||
let _ = item.set_enabled(true);
|
||||
let _ = app
|
||||
.notification()
|
||||
.builder()
|
||||
.title("Tessera-Update")
|
||||
.body(format!(
|
||||
"Update fehlgeschlagen: {}. Die Download-Seite wird im Browser geöffnet.",
|
||||
e
|
||||
))
|
||||
.show();
|
||||
open_download_page(&app);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -298,6 +500,7 @@ pub fn run() {
|
||||
None,
|
||||
))
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
check_server,
|
||||
save_server_url,
|
||||
@@ -316,11 +519,13 @@ pub fn run() {
|
||||
}
|
||||
|
||||
// Tray menu: Verbunden mit ... (gesperrt) · — · Öffnen ·
|
||||
// Server-Adresse ändern... · Update herunterladen · — ·
|
||||
// Server-Adresse ändern... · Update installieren · — ·
|
||||
// Autostart-Haken · — · Beenden.
|
||||
// "connected" ist nicht anklickbar; "update" bleibt gesperrt, bis
|
||||
// die Versionspruefung eine neuere Version findet; "autostart"
|
||||
// spiegelt den tatsaechlichen Systemzustand beim Start.
|
||||
// die Versionspruefung eine neuere Version findet, und heisst dann
|
||||
// "Auf Version X.Y.Z aktualisieren" bzw. "Auf Beta-Stand ...
|
||||
// aktualisieren"; "autostart" spiegelt den tatsaechlichen
|
||||
// Systemzustand beim Start.
|
||||
let connected =
|
||||
MenuItemBuilder::with_id("connected", tray_labels(server_url.as_deref()).1)
|
||||
.enabled(false)
|
||||
@@ -357,6 +562,7 @@ pub fn run() {
|
||||
connected: connected.clone(),
|
||||
update: update.clone(),
|
||||
});
|
||||
app.manage(PendingUpdate(Mutex::new(None)));
|
||||
|
||||
let autostart_for_menu = autostart.clone();
|
||||
|
||||
@@ -383,12 +589,20 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
"update" => {
|
||||
if let Some(server) = stored_server_url(app) {
|
||||
let target = format!(
|
||||
"{}/settings/general/desktop",
|
||||
server.trim_end_matches('/')
|
||||
);
|
||||
let _ = app.opener().open_url(target, None::<&str>);
|
||||
// `take()` verhindert Doppelklick-Downloads, der
|
||||
// gesperrte Eintrag ebenso; ohne abgelegten Stand
|
||||
// bleibt der Browser-Weg (Einstellungen → Desktop-App).
|
||||
let pending = app
|
||||
.state::<PendingUpdate>()
|
||||
.0
|
||||
.lock()
|
||||
.ok()
|
||||
.and_then(|mut guard| guard.take());
|
||||
match pending {
|
||||
Some((update, menu_text)) => {
|
||||
spawn_update_install(app.clone(), update, menu_text)
|
||||
}
|
||||
None => open_download_page(app),
|
||||
}
|
||||
}
|
||||
"autostart" => {
|
||||
@@ -489,23 +703,124 @@ mod tests {
|
||||
#[test]
|
||||
fn update_labels_bei_versionswechsel() {
|
||||
let (menu_text, body) = update_labels(true, "1.2.0", "abc1234");
|
||||
assert_eq!(menu_text, "Version 1.2.0 herunterladen");
|
||||
assert_eq!(menu_text, "Auf Version 1.2.0 aktualisieren");
|
||||
assert_eq!(
|
||||
body,
|
||||
"Neue Version 1.2.0 verfügbar – Download über das Symbol im Infobereich."
|
||||
"Neue Version 1.2.0 verfügbar – Aktualisieren über das Symbol im Infobereich."
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_labels_bei_gleicher_version_beta_commit() {
|
||||
let (menu_text, body) = update_labels(false, "1.1.0", "abc1234");
|
||||
assert_eq!(menu_text, "Neuen Beta-Stand herunterladen");
|
||||
assert_eq!(menu_text, "Auf Beta-Stand abc1234 aktualisieren");
|
||||
assert_eq!(
|
||||
body,
|
||||
"Neuer Beta-Stand abc1234 verfügbar – Download über das Symbol im Infobereich."
|
||||
"Neuer Beta-Stand abc1234 verfügbar – Aktualisieren über das Symbol im Infobereich."
|
||||
);
|
||||
}
|
||||
|
||||
fn v(s: &str) -> Version {
|
||||
Version::parse(s).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_hoehere_basis_live() {
|
||||
assert!(is_update_newer(&v("1.2.0"), "abc1234", &v("1.3.0")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_hoehere_basis_beta() {
|
||||
assert!(is_update_newer(
|
||||
&v("1.2.0"),
|
||||
"abc1234",
|
||||
&v("1.3.0-beta.gdef5678")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_niedrigere_basis() {
|
||||
assert!(!is_update_newer(
|
||||
&v("1.3.0"),
|
||||
"abc1234",
|
||||
&v("1.2.0-beta.g9999999")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_gleiche_basis_anderer_beta_stempel() {
|
||||
assert!(is_update_newer(
|
||||
&v("1.2.0"),
|
||||
"abc1234",
|
||||
&v("1.2.0-beta.gdef5678")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_gleiche_basis_gleicher_beta_stempel() {
|
||||
assert!(!is_update_newer(
|
||||
&v("1.2.0"),
|
||||
"abc1234",
|
||||
&v("1.2.0-beta.gabc1234")
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_gleiche_basis_live_ohne_prerelease() {
|
||||
assert!(!is_update_newer(&v("1.2.0"), "abc1234", &v("1.2.0")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_leerer_client_commit_nur_basisvergleich() {
|
||||
assert!(!is_update_newer(&v("1.2.0"), "", &v("1.2.0-beta.gdef5678")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_update_newer_fremdes_prerelease_format() {
|
||||
assert!(!is_update_newer(&v("1.2.0"), "abc1234", &v("1.2.0-rc.1")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta_commit_mit_praefix_g_erlaubt_fuehrende_null() {
|
||||
assert!(Version::parse("1.2.0-beta.g0123456").is_ok());
|
||||
assert_eq!(beta_commit(&v("1.2.0-beta.g0123456")), Some("0123456"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta_commit_none_ohne_prerelease() {
|
||||
assert_eq!(beta_commit(&v("1.2.0")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn beta_commit_none_bei_fremdem_prerelease() {
|
||||
assert_eq!(beta_commit(&v("1.2.0-rc.1")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_endpoint_traegt_platzhalter_und_base() {
|
||||
assert_eq!(
|
||||
update_endpoint("https://tessera.ctl.de/").unwrap().as_str(),
|
||||
"https://tessera.ctl.de/api-proxy/desktop/update?target=%7B%7Btarget%7D%7D&arch=%7B%7Barch%7D%7D¤t=%7B%7Bcurrent_version%7D%7D&base=https%3A%2F%2Ftessera.ctl.de"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_endpoint_none_bei_unparsbarer_adresse() {
|
||||
assert!(update_endpoint("kein url").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_labels_neue_basis_nennt_version_auch_bei_beta() {
|
||||
let (menu_text, _) = release_labels(&v("1.2.0"), &v("1.3.0-beta.gdef5678"));
|
||||
assert_eq!(menu_text, "Auf Version 1.3.0 aktualisieren");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_labels_gleiche_basis_nennt_beta_stempel() {
|
||||
let (menu_text, _) = release_labels(&v("1.2.0"), &v("1.2.0-beta.gdef5678"));
|
||||
assert_eq!(menu_text, "Auf Beta-Stand def5678 aktualisieren");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_host_mit_pfad_und_ohne_port() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user