5d29f03d50
- Moved boosapi-rs/* to repo root - Deleted all Node.js source (src/server/, src/web/, scripts/) - Deleted node_modules, package.json, tsconfig, vite.config - Updated .gitignore and AGENTS.md for Rust-only stack - Fixed static file path for frontend dist/web/ - Dockerfile stays the same (already Rust-only) - Kept dist/web/ for Rust static serving
116 lines
3.7 KiB
Rust
116 lines
3.7 KiB
Rust
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<SqlitePool>,
|
|
client: reqwest::Client,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UpdateInfo {
|
|
pub current_version: String,
|
|
pub latest_version: Option<String>,
|
|
pub update_available: bool,
|
|
pub release_notes: Option<String>,
|
|
pub download_url: Option<String>,
|
|
}
|
|
|
|
impl UpdateCenter {
|
|
pub fn new(db: Arc<SqlitePool>) -> Self {
|
|
Self {
|
|
db,
|
|
client: reqwest::Client::new(),
|
|
}
|
|
}
|
|
|
|
pub async fn check_for_updates(&self) -> AppResult<UpdateInfo> {
|
|
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<String> {
|
|
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<Value> {
|
|
let raw: Option<String> = 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())))
|
|
}
|
|
}
|