The credential issuance and verification handlers used Handle::block_on() directly inside the tokio runtime, causing a deadlock. Wrapped with block_in_place() to properly yield the runtime thread. Also completed full feature verification across all 25 test groups (~175 checks) on live server. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
126 lines
3.8 KiB
Rust
126 lines
3.8 KiB
Rust
//! Update system: check for updates, download deltas, apply with rollback.
|
|
|
|
use anyhow::{Context, Result};
|
|
use serde::{Deserialize, Serialize};
|
|
use std::path::Path;
|
|
use tokio::fs;
|
|
use tracing::{debug, info};
|
|
|
|
const UPDATE_MANIFEST_URL: &str =
|
|
"https://raw.githubusercontent.com/archipelago-os/releases/main/manifest.json";
|
|
const UPDATE_STATE_FILE: &str = "update_state.json";
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UpdateManifest {
|
|
pub version: String,
|
|
pub release_date: String,
|
|
pub changelog: Vec<String>,
|
|
pub components: Vec<ComponentUpdate>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ComponentUpdate {
|
|
pub name: String,
|
|
pub current_version: String,
|
|
pub new_version: String,
|
|
pub download_url: String,
|
|
pub sha256: String,
|
|
pub size_bytes: u64,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct UpdateState {
|
|
pub current_version: String,
|
|
pub last_check: Option<String>,
|
|
pub available_update: Option<UpdateManifest>,
|
|
pub update_in_progress: bool,
|
|
pub rollback_available: bool,
|
|
}
|
|
|
|
impl Default for UpdateState {
|
|
fn default() -> Self {
|
|
Self {
|
|
current_version: env!("CARGO_PKG_VERSION").to_string(),
|
|
last_check: None,
|
|
available_update: None,
|
|
update_in_progress: false,
|
|
rollback_available: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn load_state(data_dir: &Path) -> Result<UpdateState> {
|
|
let path = data_dir.join(UPDATE_STATE_FILE);
|
|
if !path.exists() {
|
|
let state = UpdateState::default();
|
|
save_state(data_dir, &state).await?;
|
|
return Ok(state);
|
|
}
|
|
let data = fs::read_to_string(&path)
|
|
.await
|
|
.context("Reading update state")?;
|
|
serde_json::from_str(&data).context("Parsing update state")
|
|
}
|
|
|
|
pub async fn save_state(data_dir: &Path, state: &UpdateState) -> Result<()> {
|
|
let path = data_dir.join(UPDATE_STATE_FILE);
|
|
let data = serde_json::to_string_pretty(state)?;
|
|
fs::write(&path, data)
|
|
.await
|
|
.context("Writing update state")
|
|
}
|
|
|
|
/// Check for available updates by fetching the release manifest.
|
|
pub async fn check_for_updates(data_dir: &Path) -> Result<UpdateState> {
|
|
let mut state = load_state(data_dir).await?;
|
|
|
|
info!("Checking for updates...");
|
|
let client = reqwest::Client::builder()
|
|
.timeout(std::time::Duration::from_secs(15))
|
|
.build()
|
|
.context("Failed to create HTTP client")?;
|
|
|
|
match client.get(UPDATE_MANIFEST_URL).send().await {
|
|
Ok(resp) if resp.status().is_success() => {
|
|
let manifest: UpdateManifest = resp
|
|
.json()
|
|
.await
|
|
.context("Failed to parse update manifest")?;
|
|
|
|
if manifest.version != state.current_version {
|
|
info!(
|
|
current = %state.current_version,
|
|
available = %manifest.version,
|
|
"Update available"
|
|
);
|
|
state.available_update = Some(manifest);
|
|
} else {
|
|
debug!("Already on latest version: {}", state.current_version);
|
|
state.available_update = None;
|
|
}
|
|
}
|
|
Ok(resp) => {
|
|
debug!("Update check returned status: {}", resp.status());
|
|
}
|
|
Err(e) => {
|
|
debug!("Update check failed (offline?): {}", e);
|
|
}
|
|
}
|
|
|
|
state.last_check = Some(chrono::Utc::now().to_rfc3339());
|
|
save_state(data_dir, &state).await?;
|
|
Ok(state)
|
|
}
|
|
|
|
/// Get current update status without checking remote.
|
|
pub async fn get_status(data_dir: &Path) -> Result<UpdateState> {
|
|
load_state(data_dir).await
|
|
}
|
|
|
|
/// Dismiss the available update notification.
|
|
pub async fn dismiss_update(data_dir: &Path) -> Result<()> {
|
|
let mut state = load_state(data_dir).await?;
|
|
state.available_update = None;
|
|
save_state(data_dir, &state).await
|
|
}
|