aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorMax Audron <me@audron.dev>2026-01-30 18:19:42 +0100
committerMax Audron <me@audron.dev>2026-01-30 18:19:42 +0100
commit84e778c6f693027c4f9215eeeda203e36cc19f9a (patch)
tree0598fc34cac17c60d6530e0af7f86c8aa48276a6 /src
init
Diffstat (limited to 'src')
-rw-r--r--src/handlers.rs64
-rw-r--r--src/main.rs68
-rw-r--r--src/models.rs50
-rw-r--r--src/storage.rs60
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(())
+}