use crate::error::{AppError, AppResult}; use serde::{Deserialize, Serialize}; use serde_json::Value; use sqlx::SqlitePool; use std::sync::Arc; const UPDATE_CONFIG_KEY: &str = "update_center_config"; #[derive(Clone)] pub struct UpdateCenter { db: Arc, client: reqwest::Client, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdateInfo { pub current_version: String, pub latest_version: Option, pub update_available: bool, pub release_notes: Option, pub download_url: Option, } impl UpdateCenter { pub fn new(db: Arc) -> Self { Self { db, client: reqwest::Client::new(), } } pub async fn check_for_updates(&self) -> AppResult { let config = self.load_config().await?; let current_version = env!("CARGO_PKG_VERSION").to_string(); let Some(endpoint) = config .get("check_url") .and_then(Value::as_str) .filter(|url| !url.trim().is_empty()) else { return Ok(UpdateInfo { current_version, latest_version: None, update_available: false, release_notes: None, download_url: None, }); }; let payload: Value = self .client .get(endpoint) .send() .await .map_err(|err| AppError::UpstreamError(format!("Update check failed: {err}")))? .json() .await .map_err(|err| AppError::UpstreamError(format!("Invalid update response: {err}")))?; let latest_version = payload .get("version") .or_else(|| payload.get("latestVersion")) .and_then(Value::as_str) .map(str::to_string); let update_available = latest_version .as_deref() .map(|latest| latest != current_version) .unwrap_or(false); Ok(UpdateInfo { current_version, latest_version, update_available, release_notes: payload .get("release_notes") .or_else(|| payload.get("releaseNotes")) .and_then(Value::as_str) .map(str::to_string), download_url: payload .get("download_url") .or_else(|| payload.get("downloadUrl")) .and_then(Value::as_str) .map(str::to_string), }) } pub async fn apply_update(&self, version: &str) -> AppResult { let task_id = format!("update-{}", uuid::Uuid::new_v4()); sqlx::query( "INSERT INTO background_tasks (id, task_type, title, status, message, dedupe_key, created_at, updated_at) VALUES (?, 'update-center-deploy', ?, 'pending', ?, ?, datetime('now'), datetime('now'))", ) .bind(&task_id) .bind(format!("Apply BoosAPI {version}")) .bind("Update request accepted; an external supervisor should perform download and restart") .bind(format!("update-center:deploy:{version}")) .execute(&*self.db) .await?; Ok(task_id) } async fn load_config(&self) -> AppResult { let raw: Option = sqlx::query_scalar("SELECT value FROM settings WHERE key = ?") .bind(UPDATE_CONFIG_KEY) .fetch_optional(&*self.db) .await?; raw.as_deref() .map(serde_json::from_str) .transpose() .map_err(|err| AppError::BadRequest(format!("Invalid update center config: {err}")))? .map(Ok) .unwrap_or_else(|| Ok(Value::Object(Default::default()))) } }