// Claude Desktop — Auto-Update System // Prüft Forgejo-Releases und aktualisiert das AppImage use serde::{Deserialize, Serialize}; use std::path::PathBuf; use tauri::{AppHandle, Manager, Emitter}; /// Forgejo API Konfiguration const FORGEJO_URL: &str = "https://git.data-it-solution.de"; const REPO_OWNER: &str = "data"; const REPO_NAME: &str = "claude-desktop"; /// Aktuelle App-Version (aus Cargo.toml) const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Release-Info von Forgejo #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReleaseInfo { pub id: i64, pub tag_name: String, pub name: String, pub body: Option, pub draft: bool, pub prerelease: bool, pub created_at: String, pub published_at: Option, pub assets: Vec, } /// Release-Asset (z.B. AppImage) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ReleaseAsset { pub id: i64, pub name: String, pub size: i64, pub download_count: i64, pub browser_download_url: String, } /// Update-Status für Frontend #[derive(Debug, Clone, Serialize)] pub struct UpdateStatus { pub available: bool, pub current_version: String, pub latest_version: Option, pub release_notes: Option, pub download_url: Option, pub download_size: Option, } /// Prüft ob ein Update verfügbar ist #[tauri::command] pub async fn check_for_update() -> Result { let url = format!( "{}/api/v1/repos/{}/{}/releases?limit=1", FORGEJO_URL, REPO_OWNER, REPO_NAME ); let client = reqwest::Client::new(); let response = client .get(&url) .header("Accept", "application/json") .send() .await .map_err(|e| format!("Netzwerkfehler: {}", e))?; if !response.status().is_success() { return Ok(UpdateStatus { available: false, current_version: CURRENT_VERSION.to_string(), latest_version: None, release_notes: None, download_url: None, download_size: None, }); } let releases: Vec = response .json() .await .map_err(|e| format!("JSON-Fehler: {}", e))?; let latest = match releases.into_iter().find(|r| !r.draft && !r.prerelease) { Some(r) => r, None => { return Ok(UpdateStatus { available: false, current_version: CURRENT_VERSION.to_string(), latest_version: None, release_notes: None, download_url: None, download_size: None, }); } }; // Version vergleichen (tag_name ohne 'v' Prefix) let latest_version = latest.tag_name.trim_start_matches('v').to_string(); let is_newer = version_compare(&latest_version, CURRENT_VERSION); // AppImage-Asset finden let appimage = latest.assets.iter().find(|a| a.name.ends_with(".AppImage")); Ok(UpdateStatus { available: is_newer, current_version: CURRENT_VERSION.to_string(), latest_version: Some(latest_version), release_notes: latest.body, download_url: appimage.map(|a| a.browser_download_url.clone()), download_size: appimage.map(|a| a.size), }) } /// Download-Fortschritt #[derive(Debug, Clone, Serialize)] pub struct DownloadProgress { pub downloaded: u64, pub total: u64, pub percent: f32, } /// Lädt das Update herunter #[tauri::command] pub async fn download_update( app: AppHandle, download_url: String, ) -> Result { use std::io::Write; let client = reqwest::Client::new(); let response = client .get(&download_url) .send() .await .map_err(|e| format!("Download-Fehler: {}", e))?; let total_size = response.content_length().unwrap_or(0); // Temp-Verzeichnis für Download let cache_dir = app.path().app_cache_dir() .map_err(|e| format!("Cache-Verzeichnis nicht gefunden: {}", e))?; std::fs::create_dir_all(&cache_dir).ok(); let file_name = download_url.split('/').last().unwrap_or("update.AppImage"); let download_path = cache_dir.join(file_name); let mut file = std::fs::File::create(&download_path) .map_err(|e| format!("Datei erstellen fehlgeschlagen: {}", e))?; let mut downloaded: u64 = 0; let mut stream = response.bytes_stream(); use futures_util::StreamExt; while let Some(chunk) = stream.next().await { let chunk = chunk.map_err(|e| format!("Download-Chunk-Fehler: {}", e))?; file.write_all(&chunk) .map_err(|e| format!("Schreibfehler: {}", e))?; downloaded += chunk.len() as u64; // Fortschritt an Frontend senden let progress = DownloadProgress { downloaded, total: total_size, percent: if total_size > 0 { (downloaded as f32 / total_size as f32) * 100.0 } else { 0.0 }, }; app.emit("update-progress", &progress).ok(); } // Ausführbar machen #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut perms = std::fs::metadata(&download_path) .map_err(|e| format!("Metadata-Fehler: {}", e))? .permissions(); perms.set_mode(0o755); std::fs::set_permissions(&download_path, perms) .map_err(|e| format!("Permissions-Fehler: {}", e))?; } println!("✅ Update heruntergeladen: {:?}", download_path); Ok(download_path) } /// Wendet das Update an (ersetzt AppImage und startet neu) #[tauri::command] pub async fn apply_update(app: AppHandle, update_path: PathBuf) -> Result<(), String> { // Prüfen ob wir in einem AppImage laufen let appimage_path = std::env::var("APPIMAGE").ok(); if let Some(appimage) = appimage_path { let appimage_path = PathBuf::from(&appimage); // Backup erstellen let backup_path = appimage_path.with_extension("AppImage.backup"); std::fs::rename(&appimage_path, &backup_path) .map_err(|e| format!("Backup fehlgeschlagen: {}", e))?; // Neues AppImage verschieben std::fs::rename(&update_path, &appimage_path) .map_err(|e| { // Backup wiederherstellen bei Fehler std::fs::rename(&backup_path, &appimage_path).ok(); format!("Update-Installation fehlgeschlagen: {}", e) })?; // Backup löschen std::fs::remove_file(&backup_path).ok(); println!("✅ Update installiert, starte neu..."); // App neustarten (kehrt nicht zurück) app.restart(); } // Nicht in AppImage — Entwicklungsmodus Err("Update nur für AppImage-Builds verfügbar. In Entwicklung: manuell `git pull` und neu bauen.".to_string()) } /// Gibt aktuelle Version zurück #[tauri::command] pub fn get_current_version() -> String { CURRENT_VERSION.to_string() } /// Vergleicht zwei Versionen (semver-ähnlich) fn version_compare(new: &str, current: &str) -> bool { let parse = |v: &str| -> Vec { v.split('.') .filter_map(|s| s.parse().ok()) .collect() }; let new_parts = parse(new); let current_parts = parse(current); for i in 0..3 { let n = new_parts.get(i).copied().unwrap_or(0); let c = current_parts.get(i).copied().unwrap_or(0); if n > c { return true; } if n < c { return false; } } false } #[cfg(test)] mod tests { use super::*; #[test] fn test_version_compare() { assert!(version_compare("1.0.1", "1.0.0")); assert!(version_compare("1.1.0", "1.0.9")); assert!(version_compare("2.0.0", "1.9.9")); assert!(!version_compare("1.0.0", "1.0.0")); assert!(!version_compare("1.0.0", "1.0.1")); assert!(!version_compare("0.9.0", "1.0.0")); } }