193 lines
4.7 KiB
Rust
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(¤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");
|
|
}
|
|
}
|