use actix_web::{HttpRequest, HttpResponse, get, post, web}; use base64::{Engine, engine::general_purpose}; use serde::Deserialize; use serde_json::json; use crate::{ DATA_STORAGE, logs::{log_asset_event, log_to_file}, }; #[derive(Deserialize, Debug)] pub struct UploadRequest { duration: u32, content_type: String, content: String, // text or base64 string } #[post("/api/upload")] async fn api_upload(req: HttpRequest, body: web::Json) -> Result { // Convert to bytes let content_bytes = if body.content_type == "text/plain" { body.content.as_bytes().to_vec() // UTF-8 bytes } else { // Decode base64 → bytes general_purpose::STANDARD.decode(&body.content).unwrap() }; let connection_info = req.connection_info(); let uploader_ip = connection_info .realip_remote_addr() .or_else(|| connection_info.peer_addr()) .unwrap_or("-") .to_string(); let asset = crate::data_mgt::Asset::new( body.duration, body.content_type.clone(), content_bytes, Some(uploader_ip.clone()), ); let id = asset .save() .map_err(|e| actix_web::error::ErrorInternalServerError(format!("Failed to save asset: {}", e)))?; log_asset_event( "upload", asset.id(), asset.mime(), asset.size_bytes(), asset.share_duration(), asset.created_at(), asset.expires_at(), asset.uploader_ip().unwrap_or("-"), ); let response_body = json!({ "link": format!("/bhs/{}", id) }); Ok(HttpResponse::Ok().json(response_body)) } #[get("/api/content/{id}")] async fn api_get_asset(req: HttpRequest, path: web::Path) -> Result { let now = std::time::Instant::now(); let id = path.into_inner(); let asset_path = format!("{}{}", DATA_STORAGE, id); let data = std::fs::read(&asset_path).map_err(|_| actix_web::error::ErrorNotFound("Asset not found"))?; let asset = serde_json::from_slice::(&data) .map_err(|_| actix_web::error::ErrorInternalServerError("Failed to parse asset data"))?; if asset.is_expired() { return Err(actix_web::error::ErrorNotFound("Asset has expired")); } log_to_file(&req, now); Ok(HttpResponse::Ok().content_type(asset.mime()).body(asset.content())) } #[derive(serde::Serialize)] struct StatsResponse { active_assets: usize, total_uploads: usize, total_deleted: usize, storage_bytes: u64, image_count: usize, text_count: usize, avg_response_ms: f64, total_requests: usize, recent_activity: Vec, } #[derive(serde::Serialize)] struct ActivityItem { action: String, mime: String, size_bytes: usize, timestamp: String, } #[get("/api/stats")] async fn api_stats() -> Result { use crate::LOG_DIR; use std::fs; let mut active_assets = 0; let mut storage_bytes: u64 = 0; let mut image_count = 0; let mut text_count = 0; // Count active assets and calculate storage if let Ok(entries) = fs::read_dir(DATA_STORAGE) { for entry in entries.flatten() { if let Ok(data) = fs::read(entry.path()) { if let Ok(asset) = serde_json::from_slice::(&data) { if !asset.is_expired() { active_assets += 1; storage_bytes += asset.size_bytes() as u64; if asset.mime().starts_with("image/") { image_count += 1; } else if asset.mime().starts_with("text/") { text_count += 1; } } } } } } // Parse log for upload/delete counts, response times, and recent activity let mut total_uploads = 0; let mut total_deleted = 0; let mut recent_activity: Vec = Vec::new(); let mut total_response_ms: f64 = 0.0; let mut request_count: usize = 0; let log_path = format!("{}access.log", LOG_DIR); if let Ok(content) = fs::read_to_string(&log_path) { for line in content.lines() { // Parse response time from request logs if line.contains("dur_ms=") { if let Some(dur_str) = line.split("dur_ms=").nth(1) { if let Some(dur_val) = dur_str.split_whitespace().next() { if let Ok(ms) = dur_val.parse::() { total_response_ms += ms; request_count += 1; } } } } if line.contains("event=asset") { if line.contains("action=upload") { total_uploads += 1; } else if line.contains("action=delete_expired") { total_deleted += 1; } // Parse for recent activity (last 20) if let Some(activity) = parse_activity_line(line) { recent_activity.push(activity); } } } } let avg_response_ms = if request_count > 0 { total_response_ms / request_count as f64 } else { 0.0 }; // Keep only last 20, most recent first recent_activity.reverse(); recent_activity.truncate(20); let response = StatsResponse { active_assets, total_uploads, total_deleted, storage_bytes, image_count, text_count, avg_response_ms, total_requests: request_count, recent_activity, }; Ok(HttpResponse::Ok().json(response)) } fn parse_activity_line(line: &str) -> Option { let timestamp = line.split_whitespace().next()?.to_string(); let action = if line.contains("action=upload") { "upload".to_string() } else if line.contains("action=delete_expired") { "delete".to_string() } else { return None; }; let mime = line.split("mime=").nth(1)?.split_whitespace().next()?.to_string(); let size_bytes: usize = line .split("size_bytes=") .nth(1)? .split_whitespace() .next()? .parse() .ok()?; Some(ActivityItem { action, mime, size_bytes, timestamp, }) }