use axum::{ extract::{ConnectInfo, Path, Query, State}, http::{HeaderMap, StatusCode}, response::Json, }; use log::warn; use serde_json::json; use std::collections::HashMap; use std::net::SocketAddr; use crate::db; use super::common::{AppState, ApiResponse, ItemInfo, TagsQuery, check_auth}; 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 /item/ from {}", addr); return Err(StatusCode::UNAUTHORIZED); } let mut conn = state.db.lock().await; let tags: Vec = params.tags .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 })? }; // 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)) } pub async fn handle_get_item( State(state): State, Path(item_id): Path, Query(params): Query, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to /item/{} from {}", item_id, addr); return Err(StatusCode::UNAUTHORIZED); } let mut conn = state.db.lock().await; let item = if let Ok(id) = item_id.parse::() { db::get_item(&mut *conn, id).map_err(|e| { warn!("Failed to get item {}: {}", id, e); StatusCode::INTERNAL_SERVER_ERROR })? } else { // Try to find by tags 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 {:?}: {}", tags, e); StatusCode::INTERNAL_SERVER_ERROR })? } else { warn!("Invalid item ID '{}' and no tags provided", item_id); return Err(StatusCode::BAD_REQUEST); } }; if let Some(item) = item { let item_tags = db::get_item_tags(&mut *conn, &item) .map_err(|e| { warn!("Failed to get tags for item {}: {}", item.id.unwrap_or(0), e); StatusCode::INTERNAL_SERVER_ERROR })? .into_iter() .map(|t| t.name) .collect(); 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 item_info = ItemInfo { id: item.id.unwrap_or(0), ts: item.ts.to_rfc3339(), size: item.size, compression: item.compression, tags: item_tags, metadata: item_meta, }; let response = ApiResponse { success: true, data: Some(item_info), error: None, }; Ok(Json(response)) } else { Err(StatusCode::NOT_FOUND) } } pub async fn handle_put_item( State(state): State, headers: HeaderMap, ConnectInfo(addr): ConnectInfo, ) -> Result>, StatusCode> { if !check_auth(&headers, &state.password) { warn!("Unauthorized request to PUT /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("PUT /item/ not yet implemented".to_string()), }; Ok(Json(response)) } 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 /item/{} from {}", item_id, addr); return Err(StatusCode::UNAUTHORIZED); } if let Ok(id) = item_id.parse::() { let mut conn = state.db.lock().await; if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| { warn!("Failed to get item {} for deletion: {}", id, e); StatusCode::INTERNAL_SERVER_ERROR })? { db::delete_item(&mut *conn, item).map_err(|e| { warn!("Failed to delete item {}: {}", id, e); StatusCode::INTERNAL_SERVER_ERROR })?; let response = ApiResponse::<()> { success: true, data: None, error: None, }; Ok(Json(response)) } else { Err(StatusCode::NOT_FOUND) } } else { Err(StatusCode::BAD_REQUEST) } } pub fn get_items_openapi_spec() -> serde_json::Value { json!({ "/item/": { "get": { "summary": "List items", "parameters": [ { "name": "tags", "in": "query", "schema": {"type": "string"}, "description": "Comma-separated list of tags to filter by" } ], "responses": { "200": { "description": "List of items", "content": { "application/json": { "schema": { "type": "array", "items": {"$ref": "#/components/schemas/ItemInfo"} } } } } } }, "put": { "summary": "Add new item", "responses": { "201": { "description": "Item created", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/ItemInfo"} } } } } } }, "/item/{id}": { "get": { "summary": "Get item by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": {"type": "string"}, "description": "Item ID or use tags query parameter" }, { "name": "tags", "in": "query", "schema": {"type": "string"}, "description": "Comma-separated list of tags (when ID is not numeric)" } ], "responses": { "200": { "description": "Item information", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/ItemInfo"} } } }, "404": {"description": "Item not found"} } }, "delete": { "summary": "Delete item by ID", "parameters": [ { "name": "id", "in": "path", "required": true, "schema": {"type": "integer"} } ], "responses": { "200": {"description": "Item deleted"}, "404": {"description": "Item not found"} } } } }) }