feat(server): build axum web server for licensing, landing page, and updates
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
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<PlatformEntry>,
|
||||
}
|
||||
|
||||
#[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<Self> {
|
||||
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<AppState>,
|
||||
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<VersionParts> = 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user