use axum::{ body::Body, extract::{Path, State}, http::{header, StatusCode}, response::{Html, Response}, Json, }; use serde::Serialize; use std::sync::Arc; use tokio::fs::File; use tokio_util::io::ReaderStream; use crate::AppState; #[derive(Debug, Serialize)] pub struct ClientConfig { pub server_name: String, pub server_url: String, pub relay_url: String, pub version: String, } #[derive(Debug, Serialize)] pub struct ClientBuild { pub platform: String, pub arch: String, pub version: String, pub filename: String, pub size: String, pub download_count: u64, pub built_at: String, pub status: String, } pub async fn get_client_config(State(state): State>) -> Json { let config = state.server_config.read().unwrap(); let server_url = if !config.server_domain.is_empty() { format!("https://{}", config.server_domain) } else if !config.server_ip.is_empty() { format!("http://{}:3000", config.server_ip) } else { "http://localhost:3000".to_string() }; let relay_url = if !config.server_domain.is_empty() { format!("{}:7900", config.server_domain) } else if !config.server_ip.is_empty() { format!("{}:7900", config.server_ip) } else { "localhost:7900".to_string() }; Json(ClientConfig { server_name: config.server_name.clone(), server_url, relay_url, version: config.client_version.clone(), }) } pub async fn download_page(State(state): State>) -> Html { let config = state.server_config.read().unwrap(); let server_url = if !config.server_domain.is_empty() { format!("https://{}", config.server_domain) } else if !config.server_ip.is_empty() { format!("http://{}:3000", config.server_ip) } else { "http://localhost:3000".to_string() }; let html = format!(r##" FunMC 客户端下载 - {server_name}

FunMC

Minecraft 联机工具

{server_name}

选择你的平台

🪟

Windows

Windows 10/11

下载 .exe
🍎

macOS

macOS 11+

🐧

Linux

Ubuntu/Debian/Fedora

下载 AppImage

移动端

服务器信息

服务器地址: {server_url}

客户端版本: v{version}

下载并安装客户端后,启动程序会自动连接到此服务器,无需手动配置。

魔幻方开发 · FunMC
"##, server_name = config.server_name, server_url = server_url, version = config.client_version, ); Html(html) } pub async fn list_builds() -> Json> { let version = env!("CARGO_PKG_VERSION"); let now = chrono::Utc::now().to_rfc3339(); let builds = vec![ ClientBuild { platform: "windows-x64".to_string(), arch: "x64".to_string(), version: version.to_string(), filename: format!("FunMC-{}-windows-x64.exe", version), size: "45.2 MB".to_string(), download_count: 0, built_at: now.clone(), status: "ready".to_string(), }, ClientBuild { platform: "macos-arm64".to_string(), arch: "arm64".to_string(), version: version.to_string(), filename: format!("FunMC-{}-macos-arm64.dmg", version), size: "52.1 MB".to_string(), download_count: 0, built_at: now.clone(), status: "ready".to_string(), }, ClientBuild { platform: "macos-x64".to_string(), arch: "x64".to_string(), version: version.to_string(), filename: format!("FunMC-{}-macos-x64.dmg", version), size: "51.8 MB".to_string(), download_count: 0, built_at: now.clone(), status: "ready".to_string(), }, ClientBuild { platform: "linux-x64".to_string(), arch: "x64".to_string(), version: version.to_string(), filename: format!("FunMC-{}-linux-x64.AppImage", version), size: "48.7 MB".to_string(), download_count: 0, built_at: now.clone(), status: "ready".to_string(), }, ClientBuild { platform: "android-arm64".to_string(), arch: "arm64".to_string(), version: version.to_string(), filename: format!("FunMC-{}-android.apk", version), size: "38.5 MB".to_string(), download_count: 0, built_at: now, status: "ready".to_string(), }, ]; Json(builds) } pub async fn download_file(Path(filename): Path) -> Result { let downloads_dir = std::env::var("DOWNLOADS_DIR").unwrap_or_else(|_| "./downloads".to_string()); let file_path = std::path::Path::new(&downloads_dir).join(&filename); let file = File::open(&file_path).await.map_err(|_| StatusCode::NOT_FOUND)?; let stream = ReaderStream::new(file); let body = Body::from_stream(stream); let content_type = if filename.ends_with(".exe") { "application/x-msdownload" } else if filename.ends_with(".dmg") { "application/x-apple-diskimage" } else if filename.ends_with(".AppImage") { "application/x-executable" } else if filename.ends_with(".apk") { "application/vnd.android.package-archive" } else { "application/octet-stream" }; Ok(Response::builder() .header(header::CONTENT_TYPE, content_type) .header( header::CONTENT_DISPOSITION, format!("attachment; filename=\"{}\"", filename), ) .body(body) .unwrap()) } #[derive(Debug, serde::Deserialize)] pub struct TriggerBuildBody { pub platforms: Vec, } pub async fn trigger_build(Json(_body): Json) -> StatusCode { StatusCode::ACCEPTED }