use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use crate::AppState; #[derive(Debug, Serialize)] pub struct UpdateManifest { pub version: String, pub notes: String, pub pub_date: String, pub platforms: UpdatePlatforms, } #[derive(Debug, Serialize)] pub struct UpdatePlatforms { #[serde(rename = "windows-x86_64")] pub windows_x86_64: Option, } #[derive(Debug, Serialize)] pub struct PlatformEntry { pub signature: String, pub url: String, } #[derive(Debug, Deserialize)] pub struct VersionParts { pub major: u32, pub minor: u32, pub patch: u32, } impl VersionParts { pub fn parse(version: &str) -> Option { let parts: Vec<&str> = version.trim_start_matches('v').split('.').collect(); if parts.len() != 3 { return None; } Some(Self { major: parts[0].parse().ok()?, minor: parts[1].parse().ok()?, patch: parts[2].parse().ok()?, }) } pub fn is_newer_than(&self, other: &Self) -> bool { (self.major, self.minor, self.patch) > (other.major, other.minor, other.patch) } pub fn to_string(&self) -> String { format!("{}.{}.{}", self.major, self.minor, self.patch) } } pub async fn updater( State(state): State, Path((_target, arch, current_version)): Path<(String, String, String)>, ) -> Response { let current = match VersionParts::parse(¤t_version) { Some(v) => v, None => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Invalid version format" })), ) .into_response() } }; let release_dir = PathBuf::from(&state.config.release_dir); if !release_dir.exists() { return StatusCode::NO_CONTENT.into_response(); } let mut latest: Option = None; let entries = match std::fs::read_dir(&release_dir) { Ok(e) => e, Err(_) => return StatusCode::NO_CONTENT.into_response(), }; for entry in entries.flatten() { if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) { continue; } let dir_name = entry.file_name().to_string_lossy().to_string(); if let Some(ver) = VersionParts::parse(&dir_name) { if ver.is_newer_than(¤t) { if latest.as_ref().map_or(true, |l| ver.is_newer_than(l)) { latest = Some(ver); } } } } let latest = match latest { Some(v) => v, None => return StatusCode::NO_CONTENT.into_response(), }; let platform_dir = release_dir.join(latest.to_string()).join(&arch); if !platform_dir.exists() { return StatusCode::NO_CONTENT.into_response(); } // Find the release file let file_entry = std::fs::read_dir(&platform_dir).ok().and_then(|entries| { entries .flatten() .find(|e| e.file_type().map(|t| t.is_file()).unwrap_or(false)) }); let file_entry = match file_entry { Some(e) => e, None => return StatusCode::NO_CONTENT.into_response(), }; let file_name = file_entry.file_name().to_string_lossy().to_string(); let download_url = format!( "{}/releases/download/{}/{}/{}", state.config.app_url, latest.to_string(), arch, file_name ); let manifest = UpdateManifest { version: latest.to_string(), notes: format!("Update to version {}", latest.to_string()), pub_date: chrono::Utc::now().to_rfc3339(), platforms: UpdatePlatforms { windows_x86_64: Some(PlatformEntry { signature: String::new(), // TODO: Generate or store signature url: download_url, }), }, }; (StatusCode::OK, Json(manifest)).into_response() } #[cfg(test)] mod tests { use super::*; #[test] fn test_version_parse_valid() { let v = VersionParts::parse("0.2.0").unwrap(); assert_eq!(v.major, 0); assert_eq!(v.minor, 2); assert_eq!(v.patch, 0); } #[test] fn test_version_parse_with_v_prefix() { let v = VersionParts::parse("v1.0.0").unwrap(); assert_eq!(v.major, 1); assert_eq!(v.minor, 0); assert_eq!(v.patch, 0); } #[test] fn test_version_parse_invalid() { assert!(VersionParts::parse("invalid").is_none()); assert!(VersionParts::parse("1.0").is_none()); assert!(VersionParts::parse("1.0.0.0").is_none()); } #[test] fn test_version_is_newer_than() { let v1 = VersionParts::parse("0.1.0").unwrap(); let v2 = VersionParts::parse("0.2.0").unwrap(); let v3 = VersionParts::parse("1.0.0").unwrap(); assert!(v2.is_newer_than(&v1)); assert!(v3.is_newer_than(&v2)); assert!(!v1.is_newer_than(&v2)); assert!(!v1.is_newer_than(&v1)); } #[test] fn test_version_to_string() { let v = VersionParts::parse("1.2.3").unwrap(); assert_eq!(v.to_string(), "1.2.3"); } }