use axum::{ extract::{ConnectInfo, Path, Query, State}, http::{HeaderMap, StatusCode}, response::{Json, Response, IntoResponse}, http::header, }; use log::warn; use std::collections::HashMap; use std::net::SocketAddr; use std::path::PathBuf; use std::str::FromStr; use std::io::Read; use anyhow::{Result, anyhow}; use crate::compression_engine::{CompressionType, get_compression_engine}; use crate::db; use crate::modes::server::common::{AppState, ApiResponse, ItemInfo, TagsQuery, check_auth, ListItemsQuery}; #[utoipa::path( get, path = "/api/item/", responses( (status = 200, description = "Successfully retrieved list of items", body = ApiResponse>), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ), params( ("tags" = Option, Query, description = "Comma-separated list of tags to filter by"), ("order" = Option, Query, description = "Sort order (newest or oldest)"), ("start" = Option, Query, description = "Starting index for pagination"), ("count" = Option, Query, description = "Number of items to return") ), security( ("bearerAuth" = []) ) )] pub async fn handle_list_items( State(state): State, Query(params): Query, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /api/item/ from {}", addr); return Err(StatusCode::UNAUTHORIZED); } let mut conn = state.db.lock().await; let tags: Vec = params.tags .as_ref() .map(|s| s.split(',').map(|t| t.trim().to_string()).collect()) .unwrap_or_default(); let items = if tags.is_empty() { db::get_items(&mut *conn).map_err(|e| { warn!("Failed to get items: {}", e); StatusCode::INTERNAL_SERVER_ERROR })? } else { db::get_items_matching(&mut *conn, &tags, &HashMap::new()) .map_err(|e| { warn!("Failed to get items matching tags {:?}: {}", tags, e); StatusCode::INTERNAL_SERVER_ERROR })? }; // Apply ordering (default is newest first) let mut items = items; match params.order.as_deref().unwrap_or("newest") { "newest" => items.sort_by(|a, b| b.ts.cmp(&a.ts)), "oldest" => items.sort_by(|a, b| a.ts.cmp(&b.ts)), _ => items.sort_by(|a, b| b.ts.cmp(&a.ts)), // default to newest } // Apply pagination let start = params.start.unwrap_or(0) as usize; let count = params.count.unwrap_or(100) as usize; let items: Vec<_> = items.into_iter().skip(start).take(count).collect(); // Get item IDs for batch queries let item_ids: Vec = items.iter().filter_map(|item| item.id).collect(); // Get tags and metadata for all items let tags_map = db::get_tags_for_items(&mut *conn, &item_ids) .map_err(|e| { warn!("Failed to get tags for items: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; let meta_map = db::get_meta_for_items(&mut *conn, &item_ids) .map_err(|e| { warn!("Failed to get metadata for items: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; let item_infos: Vec = items .into_iter() .map(|item| { let item_id = item.id.unwrap_or(0); let item_tags = tags_map.get(&item_id) .map(|tags| tags.iter().map(|t| t.name.clone()).collect()) .unwrap_or_default(); let item_meta = meta_map.get(&item_id) .cloned() .unwrap_or_default(); ItemInfo { id: item_id, ts: item.ts.to_rfc3339(), size: item.size, compression: item.compression, tags: item_tags, metadata: item_meta, } }) .collect(); let response = ApiResponse { success: true, data: Some(item_infos), error: None, }; Ok(Json(response)) } #[utoipa::path( post, path = "/api/item/", responses( (status = 200, description = "Successfully created item", body = ApiResponse), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ), security( ("bearerAuth" = []) ) )] pub async fn handle_post_item( State(state): State, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to POST /api/item/ from {}", addr); return Err(StatusCode::UNAUTHORIZED); } // This is a simplified implementation // In a real implementation, you'd need to properly parse multipart/form-data // or JSON payload with the item data let response = ApiResponse:: { success: false, data: None, error: Some("POST /api/item/ not yet implemented".to_string()), }; Ok(Json(response)) } #[utoipa::path( delete, path = "/api/item/{item_id}", responses( (status = 200, description = "Successfully deleted item", body = ApiResponse<()>), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("item_id" = i64, Path, description = "ID of the item to delete") ), security( ("bearerAuth" = []) ) )] pub async fn handle_delete_item( State(state): State, Path(item_id): Path, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to DELETE /api/item/{} from {}", item_id, addr); return Err(StatusCode::UNAUTHORIZED); } // Validate that item ID is positive to prevent path traversal issues if item_id <= 0 { warn!("Invalid item ID {} from {}", item_id, addr); return Err(StatusCode::BAD_REQUEST); } let mut conn = state.db.lock().await; if let Some(item) = db::get_item(&mut *conn, item_id).map_err(|e| { warn!("Failed to get item {} for deletion: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })? { db::delete_item(&mut *conn, item).map_err(|e| { warn!("Failed to delete item {}: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })?; let response = ApiResponse::<()> { success: true, data: None, error: None, }; Ok(Json(response)) } else { Err(StatusCode::NOT_FOUND) } } #[utoipa::path( get, path = "/api/item/latest", responses( (status = 200, description = "Successfully retrieved latest item content", body = ApiResponse), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("tags" = Option, Query, description = "Comma-separated list of tags to filter by") ), security( ("bearerAuth" = []) ) )] pub async fn handle_get_item_latest( State(state): State, Query(params): Query, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /api/item/latest/content from {}", addr); return Err(StatusCode::UNAUTHORIZED); } let mut conn = state.db.lock().await; let item = if let Some(tags_str) = params.tags { let tags: Vec = tags_str.split(',').map(|t| t.trim().to_string()).collect(); db::get_item_matching(&mut *conn, &tags, &HashMap::new()) .map_err(|e| { warn!("Failed to get item matching tags {:?} for content: {}", tags, e); StatusCode::INTERNAL_SERVER_ERROR })? } else { db::get_item_last(&mut *conn).map_err(|e| { warn!("Failed to get last item for content: {}", e); StatusCode::INTERNAL_SERVER_ERROR })? }; if let Some(item) = item { match get_item_content(&item, &state.data_dir).await { Ok(content) => { let response = ApiResponse { success: true, data: Some(content), error: None, }; Ok(Json(response)) } Err(e) => { warn!("Failed to get content for item {}: {}", item.id.unwrap_or(0), e); let response = ApiResponse:: { success: false, data: None, error: Some(format!("Failed to retrieve content: {}", e)), }; Ok(Json(response)) } } } else { Err(StatusCode::NOT_FOUND) } } #[utoipa::path( get, path = "/api/item/{item_id}", responses( (status = 200, description = "Successfully retrieved item content", body = ApiResponse), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("item_id" = i64, Path, description = "ID of the item to retrieve") ), security( ("bearerAuth" = []) ) )] pub async fn handle_get_item( State(state): State, Path(item_id): Path, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /api/item/{}/content from {}", item_id, addr); return Err(StatusCode::UNAUTHORIZED); } // Validate that item ID is positive to prevent path traversal issues if item_id <= 0 { warn!("Invalid item ID {} from {}", item_id, addr); return Err(StatusCode::BAD_REQUEST); } let mut conn = state.db.lock().await; if let Some(item) = db::get_item(&mut *conn, item_id).map_err(|e| { warn!("Failed to get item {} for content: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })? { match get_item_content(&item, &state.data_dir).await { Ok(content) => { let response = ApiResponse { success: true, data: Some(content), error: None, }; Ok(Json(response)) } Err(e) => { warn!("Failed to get content for item {}: {}", item_id, e); let response = ApiResponse:: { success: false, data: None, error: Some(format!("Failed to retrieve content: {}", e)), }; Ok(Json(response)) } } } else { Err(StatusCode::NOT_FOUND) } } #[utoipa::path( get, path = "/api/item/latest/content", responses( (status = 200, description = "Successfully retrieved latest item raw content"), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("tags" = Option, Query, description = "Comma-separated list of tags to filter by") ), security( ("bearerAuth" = []) ) )] pub async fn handle_get_item_latest_content( State(state): State, Query(params): Query, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /api/item/latest/content from {}", addr); return Err(StatusCode::UNAUTHORIZED); } let mut conn = state.db.lock().await; let item = if let Some(tags_str) = params.tags { let tags: Vec = tags_str.split(',').map(|t| t.trim().to_string()).collect(); db::get_item_matching(&mut *conn, &tags, &HashMap::new()) .map_err(|e| { warn!("Failed to get item matching tags {:?} for content: {}", tags, e); StatusCode::INTERNAL_SERVER_ERROR })? } else { db::get_item_last(&mut *conn).map_err(|e| { warn!("Failed to get last item for content: {}", e); StatusCode::INTERNAL_SERVER_ERROR })? }; if let Some(item) = item { match get_item_raw_content(&item, &state.data_dir, &mut *conn).await { Ok((content, mime_type)) => { let mut response = content.into_response(); response.headers_mut().insert( header::CONTENT_TYPE, mime_type.parse().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? ); Ok(response) } Err(e) => { warn!("Failed to get raw content for item {}: {}", item.id.unwrap_or(0), e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } else { Err(StatusCode::NOT_FOUND) } } #[utoipa::path( get, path = "/api/item/{item_id}/content", responses( (status = 200, description = "Successfully retrieved item raw content"), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("item_id" = i64, Path, description = "ID of the item to retrieve") ), security( ("bearerAuth" = []) ) )] pub async fn handle_get_item_content( State(state): State, Path(item_id): Path, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /api/item/{}/content from {}", item_id, addr); return Err(StatusCode::UNAUTHORIZED); } // Validate that item ID is positive to prevent path traversal issues if item_id <= 0 { warn!("Invalid item ID {} from {}", item_id, addr); return Err(StatusCode::BAD_REQUEST); } let mut conn = state.db.lock().await; if let Some(item) = db::get_item(&mut *conn, item_id).map_err(|e| { warn!("Failed to get item {} for content: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })? { match get_item_raw_content(&item, &state.data_dir, &mut *conn).await { Ok((content, mime_type)) => { let mut response = content.into_response(); response.headers_mut().insert( header::CONTENT_TYPE, mime_type.parse().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? ); Ok(response) } Err(e) => { warn!("Failed to get raw content for item {}: {}", item_id, e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } else { Err(StatusCode::NOT_FOUND) } } async fn get_item_content(item: &db::Item, data_dir: &PathBuf) -> Result { let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?; // Validate that item ID is positive to prevent path traversal issues if item_id <= 0 { return Err(anyhow!("Invalid item ID: {}", item_id)); } let mut item_path = data_dir.clone(); item_path.push(item_id.to_string()); let compression_type = CompressionType::from_str(&item.compression)?; let compression_engine = get_compression_engine(compression_type)?; // Read the content using the compression engine let mut reader = compression_engine.open(item_path)?; let mut content = String::new(); reader.read_to_string(&mut content)?; Ok(content) } async fn get_item_raw_content(item: &db::Item, data_dir: &PathBuf, conn: &mut rusqlite::Connection) -> Result<(Vec, String)> { let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?; // Validate that item ID is positive to prevent path traversal issues if item_id <= 0 { return Err(anyhow!("Invalid item ID: {}", item_id)); } let mut item_path = data_dir.clone(); item_path.push(item_id.to_string()); let compression_type = CompressionType::from_str(&item.compression)?; let compression_engine = get_compression_engine(compression_type)?; // Read the raw content using the compression engine let mut reader = compression_engine.open(item_path)?; let mut content = Vec::new(); reader.read_to_end(&mut content)?; // Get MIME type from metadata let meta_entries = db::get_item_meta(conn, item) .map_err(|e| anyhow!("Failed to get metadata: {}", e))?; let mime_type = meta_entries .iter() .find(|m| m.name == "file.mime") .map(|m| m.value.clone()) .unwrap_or_else(|| "application/octet-stream".to_string()); Ok((content, mime_type)) } #[utoipa::path( get, path = "/api/item/latest/meta", responses( (status = 200, description = "Successfully retrieved latest item metadata", body = ApiResponse>), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("tags" = Option, Query, description = "Comma-separated list of tags to filter by") ), security( ("bearerAuth" = []) ) )] pub async fn handle_get_item_latest_meta( State(state): State, Query(params): Query, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /api/item/latest/meta from {}", addr); return Err(StatusCode::UNAUTHORIZED); } let mut conn = state.db.lock().await; let item = if let Some(tags_str) = params.tags { let tags: Vec = tags_str.split(',').map(|t| t.trim().to_string()).collect(); db::get_item_matching(&mut *conn, &tags, &HashMap::new()) .map_err(|e| { warn!("Failed to get item matching tags {:?} for meta: {}", tags, e); StatusCode::INTERNAL_SERVER_ERROR })? } else { db::get_item_last(&mut *conn).map_err(|e| { warn!("Failed to get last item for meta: {}", e); StatusCode::INTERNAL_SERVER_ERROR })? }; if let Some(item) = item { let item_meta = db::get_item_meta(&mut *conn, &item) .map_err(|e| { warn!("Failed to get metadata for item {}: {}", item.id.unwrap_or(0), e); StatusCode::INTERNAL_SERVER_ERROR })? .into_iter() .map(|m| (m.name, m.value)) .collect(); let response = ApiResponse { success: true, data: Some(item_meta), error: None, }; Ok(Json(response)) } else { Err(StatusCode::NOT_FOUND) } } #[utoipa::path( get, path = "/api/item/{item_id}/meta", responses( (status = 200, description = "Successfully retrieved item metadata", body = ApiResponse>), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("item_id" = i64, Path, description = "ID of the item to retrieve metadata for") ), security( ("bearerAuth" = []) ) )] pub async fn handle_get_item_meta( State(state): State, Path(item_id): Path, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /api/item/{}/meta from {}", item_id, addr); return Err(StatusCode::UNAUTHORIZED); } let mut conn = state.db.lock().await; if let Some(item) = db::get_item(&mut *conn, item_id).map_err(|e| { warn!("Failed to get item {} for meta: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })? { let item_meta = db::get_item_meta(&mut *conn, &item) .map_err(|e| { warn!("Failed to get metadata for item {}: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })? .into_iter() .map(|m| (m.name, m.value)) .collect(); let response = ApiResponse { success: true, data: Some(item_meta), error: None, }; Ok(Json(response)) } else { Err(StatusCode::NOT_FOUND) } }