Files
clean-copy/server/src/handlers/updater.rs
T

193 lines
4.7 KiB
Rust

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(&current_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(&current) {
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");
}
}