mod api; mod data_mgt; mod logs; use actix_files::NamedFile; use actix_web::{ App, HttpRequest, HttpServer, get, route, web::{self}, }; use serde_json::Value; use std::{env, fs, path::PathBuf, sync::LazyLock}; pub static HTML_DIR: &str = "data/html/"; pub static LOG_DIR: &str = "data/logs/"; pub static LOG_FILE_NAME: &str = "log.txt"; pub static MIN_ASSET_DURATION: u32 = 1; // in minutes pub static MAX_ASSET_DURATION: u32 = 60; // in minutes pub static MAX_ASSETS: usize = 1000; pub static MAX_ASSET_SIZE_BYTES: usize = 3 * 1024 * 1024; // 3 MB pub static MAX_UPLOADS_PER_USER: usize = 10; pub static BIND_ADDR: LazyLock = LazyLock::new(|| match env::var("BIND_ADDR") { Ok(addr) => { println!("Binding to address: {}", addr); addr.parse().unwrap_or("127.0.0.1".to_string()) } Err(_) => { println!("Binding to default address: 0.0.0.0"); "0.0.0.0".to_string() } }); pub static BIND_PORT: LazyLock = LazyLock::new(|| match env::var("BIND_PORT") { Ok(port_str) => { println!("Binding to port: {}", port_str); port_str.parse().unwrap_or(8080) } Err(_) => { println!("Binding to default port: 8080"); 8080 } }); pub static STATIC_PAGES: LazyLock> = LazyLock::new(|| { fs::read_dir(HTML_DIR) .unwrap() .filter_map(|entry| entry.ok().and_then(|e| e.file_name().to_str().map(|s| s.to_string()))) .collect() }); use crate::{ api::{api_get_asset, api_stats, api_upload}, logs::{LogEventType, log_event}, }; #[get("/")] async fn index(req: HttpRequest) -> actix_web::Result { let path: PathBuf = PathBuf::from(HTML_DIR.to_string() + "index.html"); log_event(LogEventType::HttpRequest(req.into())); Ok(NamedFile::open(path)?) } #[get("/stats")] async fn stats(req: HttpRequest) -> actix_web::Result { let path: PathBuf = PathBuf::from(HTML_DIR.to_string() + "stats.html"); log_event(LogEventType::HttpRequest(req.into())); Ok(NamedFile::open(path)?) } #[get("/bhs/{id}")] async fn view_asset(req: HttpRequest) -> actix_web::Result { let path: PathBuf = PathBuf::from(HTML_DIR.to_string() + "view.html"); log_event(LogEventType::HttpRequest(req.into())); Ok(NamedFile::open(path)?) } #[route("/{tail:.*}", method = "GET", method = "POST")] async fn catch_all(req: HttpRequest, _payload: Option>) -> actix_web::Result { println!("Catch-all route triggered for path: {}", req.uri().path()); let response = match req.uri().path() { path if STATIC_PAGES.contains(&path[1..].into()) => { let file_path = HTML_DIR.to_string() + path; Ok(NamedFile::open(file_path)?) } _ => { let file_path = PathBuf::from(HTML_DIR.to_string() + "error.html"); Ok(NamedFile::open(file_path)?) } }; log_event(LogEventType::HttpRequest(req.into())); response } #[actix_web::main] async fn main() -> std::io::Result<()> { let _ = fs::create_dir_all(LOG_DIR); let log_filename = format!("{}{}", LOG_DIR, LOG_FILE_NAME); let log_filename_path = std::path::Path::new(&log_filename); let time_tag = chrono::Local::now().format("%Y_%m_%d_%H_%M_%S"); if log_filename_path.exists() { println!("File: {}, exists, rotating.", &log_filename_path.display()); fs::rename( &log_filename_path, format!("{}{}_{}", LOG_DIR, time_tag, &LOG_FILE_NAME), ) .unwrap_or_else(|e| { println!( "No existing log file {} to rotate. Error: {}", log_filename_path.to_string_lossy(), e ) }); println!("Rotated log file to: {}_{}", time_tag, &LOG_FILE_NAME); } let app_state = data_mgt::AppState::default(); println!("Starting server at http://{}:{}/", *BIND_ADDR, *BIND_PORT); let inner_appt_state = app_state.clone(); tokio::spawn(async move { let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1)); loop { interval.tick().await; if let Err(e) = data_mgt::clear_app_data(&inner_appt_state).await { eprintln!("Error clearing assets: {}", e); } } }); HttpServer::new(move || { App::new() .app_data(web::JsonConfig::default().limit(1024 * 1024 * 3)) // 3MB limit .app_data(web::Data::new(app_state.clone())) .service(index) .service(stats) .service(view_asset) .service(api_get_asset) .service(api_upload) .service(api_stats) .service(catch_all) }) .bind((BIND_ADDR.clone(), *BIND_PORT))? .run() .await }