diff options
| author | Max Audron <me@audron.dev> | 2026-01-30 18:19:42 +0100 |
|---|---|---|
| committer | Max Audron <me@audron.dev> | 2026-01-30 18:19:42 +0100 |
| commit | 84e778c6f693027c4f9215eeeda203e36cc19f9a (patch) | |
| tree | 0598fc34cac17c60d6530e0af7f86c8aa48276a6 /src | |
init
Diffstat (limited to 'src')
| -rw-r--r-- | src/handlers.rs | 64 | ||||
| -rw-r--r-- | src/main.rs | 68 | ||||
| -rw-r--r-- | src/models.rs | 50 | ||||
| -rw-r--r-- | src/storage.rs | 60 |
4 files changed, 242 insertions, 0 deletions
diff --git a/src/handlers.rs b/src/handlers.rs new file mode 100644 index 0000000..d2a3af6 --- /dev/null +++ b/src/handlers.rs @@ -0,0 +1,64 @@ +use crate::models::ListResponse; +use crate::storage::SharedStorage; +use axum::{ + extract::{Path, State}, + http::{StatusCode, header}, + response::{IntoResponse, Response}, + Json, +}; + +pub async fn list_all(State(storage): State<SharedStorage>) -> Json<ListResponse> { + let storage = storage.read().unwrap(); + Json(ListResponse::from(&*storage)) +} + +pub async fn get_item( + State(storage): State<SharedStorage>, + Path((content_type, name)): Path<(String, String)>, +) -> Response { + let storage = storage.read().unwrap(); + + let item = match content_type.as_str() { + "car" => storage.car.get(&name), + "track" => storage.track.get(&name), + "luaapp" => storage.luaapp.get(&name), + "app" => storage.app.get(&name), + "filter" => storage.filter.get(&name), + _ => return (StatusCode::NOT_FOUND, "Invalid content type").into_response(), + }; + + match item { + Some(item) => Json(item.clone()).into_response(), + None => (StatusCode::NOT_FOUND, "Item not found").into_response(), + } +} + +pub async fn get_download( + State(storage): State<SharedStorage>, + Path((content_type, name)): Path<(String, String)>, +) -> Response { + let storage = storage.read().unwrap(); + + let item = match content_type.as_str() { + "car" => storage.car.get(&name), + "track" => storage.track.get(&name), + "luaapp" => storage.luaapp.get(&name), + "app" => storage.app.get(&name), + "filter" => storage.filter.get(&name), + _ => return (StatusCode::NOT_FOUND, "Invalid content type").into_response(), + }; + + match item { + Some(item) => { + ( + StatusCode::FOUND, + [(header::LOCATION, item.download_url.as_str())], + ).into_response() + }, + None => (StatusCode::NOT_FOUND, "Item not found").into_response(), + } +} + + + + diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e49cada --- /dev/null +++ b/src/main.rs @@ -0,0 +1,68 @@ +mod handlers; +mod models; +mod storage; + +use axum::{ + routing::get, + Router, +}; +use std::env; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; +use tower_http::trace::TraceLayer; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() { + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "ac_cup_server=debug,tower_http=debug,axum=debug".into()), + ) + .with(tracing_subscriber::fmt::layer()) + .init(); + + let storage_path = env::var("STORAGE_PATH") + .unwrap_or_else(|_| "storage.json".to_string()); + let storage_path = PathBuf::from(storage_path); + + let storage = storage::load_storage(&storage_path) + .unwrap_or_else(|e| { + tracing::warn!("Failed to load storage: {}. Using empty storage.", e); + models::Storage::default() + }); + + let shared_storage = Arc::new(RwLock::new(storage)); + + // Start watching the storage file for changes + let watch_storage = shared_storage.clone(); + let watch_path = storage_path.clone(); + if let Err(e) = storage::watch_storage(watch_path, watch_storage) { + tracing::error!("Failed to start storage file watcher: {}", e); + } + + let app = Router::new() + .route("/", get(handlers::list_all)) + .route("/{content_type}/{name}", get(handlers::get_item)) + .route("/{content_type}/{name}/get", get(handlers::get_download)) + .layer(TraceLayer::new_for_http()) + .with_state(shared_storage); + + let port = env::var("PORT") + .unwrap_or_else(|_| "3000".to_string()) + .parse::<u16>() + .expect("PORT must be a valid number"); + + let addr = format!("0.0.0.0:{}", port); + let listener = tokio::net::TcpListener::bind(&addr) + .await + .expect("Failed to bind to address"); + + tracing::info!("Server listening on {}", addr); + + axum::serve(listener, app) + .await + .expect("Server failed"); +} + + diff --git a/src/models.rs b/src/models.rs new file mode 100644 index 0000000..66389ae --- /dev/null +++ b/src/models.rs @@ -0,0 +1,50 @@ +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ContentItem { + pub name: String, + pub author: String, + pub information_url: String, + pub version: String, + pub active: bool, + pub clean_installation: bool, + #[serde(skip_serializing)] + pub download_url: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Storage { + #[serde(default)] + pub car: HashMap<String, ContentItem>, + #[serde(default)] + pub track: HashMap<String, ContentItem>, + #[serde(default)] + pub luaapp: HashMap<String, ContentItem>, + #[serde(default)] + pub app: HashMap<String, ContentItem>, + #[serde(default)] + pub filter: HashMap<String, ContentItem>, +} + +#[derive(Debug, Serialize)] +pub struct ListResponse { + pub car: HashMap<String, String>, + pub track: HashMap<String, String>, + pub luaapp: HashMap<String, String>, + pub app: HashMap<String, String>, + pub filter: HashMap<String, String>, +} + +impl From<&Storage> for ListResponse { + fn from(storage: &Storage) -> Self { + Self { + car: storage.car.iter().map(|(k, v)| (k.clone(), v.version.clone())).collect(), + track: storage.track.iter().map(|(k, v)| (k.clone(), v.version.clone())).collect(), + luaapp: storage.luaapp.iter().map(|(k, v)| (k.clone(), v.version.clone())).collect(), + app: storage.app.iter().map(|(k, v)| (k.clone(), v.version.clone())).collect(), + filter: storage.filter.iter().map(|(k, v)| (k.clone(), v.version.clone())).collect(), + } + } +} diff --git a/src/storage.rs b/src/storage.rs new file mode 100644 index 0000000..1055839 --- /dev/null +++ b/src/storage.rs @@ -0,0 +1,60 @@ +use crate::models::Storage; +use notify::{Config, Event, RecommendedWatcher, RecursiveMode, Watcher}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::{Arc, RwLock}; + +pub type SharedStorage = Arc<RwLock<Storage>>; + +pub fn load_storage(path: &Path) -> Result<Storage, Box<dyn std::error::Error>> { + if path.exists() { + let content = fs::read_to_string(path)?; + let storage = serde_json::from_str(&content)?; + Ok(storage) + } else { + Ok(Storage::default()) + } +} + +pub fn watch_storage( + storage_path: PathBuf, + shared_storage: SharedStorage, +) -> Result<(), Box<dyn std::error::Error>> { + let (tx, rx) = std::sync::mpsc::channel::<Result<Event, notify::Error>>(); + + let mut watcher = RecommendedWatcher::new(tx, Config::default())?; + watcher.watch(&storage_path, RecursiveMode::NonRecursive)?; + + tracing::info!("Watching storage file: {:?}", storage_path); + + std::thread::spawn(move || { + let _watcher = watcher; // Keep watcher alive + + for res in rx { + match res { + Ok(event) => { + if event.kind.is_modify() || event.kind.is_create() { + tracing::info!("Storage file changed, reloading..."); + + match load_storage(&storage_path) { + Ok(new_storage) => { + if let Ok(mut storage) = shared_storage.write() { + *storage = new_storage; + tracing::info!("Storage reloaded successfully"); + } else { + tracing::error!("Failed to acquire write lock on storage"); + } + } + Err(e) => { + tracing::error!("Failed to reload storage: {}", e); + } + } + } + } + Err(e) => tracing::error!("Watch error: {:?}", e), + } + } + }); + + Ok(()) +} |
