use crate::modes::server::common::{ ApiResponse, AppState, ItemContentQuery, ItemInfo, ItemInfoListResponse, ItemInfoResponse, ItemQuery, ListItemsQuery, MetadataResponse, TagsQuery, }; use crate::services::async_item_service::AsyncItemService; use crate::services::error::CoreError; use axum::{ extract::{Path, Query, State}, http::{StatusCode, header}, response::{Json, Response}, }; use log::{debug, warn}; use std::collections::HashMap; // Helper functions to replace the missing binary_detection module async fn check_binary_content_allowed( item_service: &AsyncItemService, item_id: i64, metadata: &HashMap, allow_binary: bool, ) -> Result<(), StatusCode> { if !allow_binary { let is_binary = is_content_binary(item_service, item_id, metadata).await?; if is_binary { return Err(StatusCode::BAD_REQUEST); } } Ok(()) } /// Helper function to determine if content is binary async fn is_content_binary( item_service: &AsyncItemService, item_id: i64, metadata: &HashMap, ) -> Result { if let Some(text_val) = metadata.get("text") { Ok(text_val == "false") } else { // If text metadata isn't set, we need to check the content using streaming approach match item_service .get_item_content_info_streaming(item_id, None) .await { Ok((_, _, is_binary)) => Ok(is_binary), Err(e) => { log::warn!( "Failed to get content info for binary check for item {}: {}", item_id, e ); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } } // Helper function to replace missing build_filter_string fn build_filter_string(_params: &ItemQuery) -> Option { // Implement this based on your needs None } // Create a simple ResponseBuilder to replace the missing one struct ResponseBuilder; impl ResponseBuilder { pub fn json(data: T) -> Result { let json = serde_json::to_vec(&data).map_err(|e| { log::warn!("Failed to serialize response: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; Response::builder() .header(header::CONTENT_TYPE, "application/json") .header(header::CONTENT_LENGTH, json.len().to_string()) .body(axum::body::Body::from(json)) .map_err(|e| { log::warn!("Failed to build response: {}", e); StatusCode::INTERNAL_SERVER_ERROR }) } pub fn binary(content: &[u8], mime_type: &str) -> Result { Response::builder() .header(header::CONTENT_TYPE, mime_type) .header(header::CONTENT_LENGTH, content.len().to_string()) .body(axum::body::Body::from(content.to_vec())) .map_err(|e| { log::warn!("Failed to build response: {}", e); StatusCode::INTERNAL_SERVER_ERROR }) } } /// Helper function to get mime type from metadata fn get_mime_type(metadata: &HashMap) -> String { metadata .get("mime_type") .map(|s| s.to_string()) .unwrap_or_else(|| "application/octet-stream".to_string()) } /// Helper function to apply offset and length to content fn apply_offset_length(content: &[u8], offset: u64, length: u64) -> &[u8] { let content_len = content.len() as u64; let start = std::cmp::min(offset, content_len); let end = if length > 0 { std::cmp::min(start + length, content_len) } else { content_len }; if start < content_len { &content[start as usize..end as usize] } else { &[] } } /// Helper function to handle item not found errors fn handle_item_error(error: CoreError) -> StatusCode { match error { CoreError::ItemNotFound(_) | CoreError::ItemNotFoundGeneric => StatusCode::NOT_FOUND, _ => { warn!("Failed to get item: {}", error); StatusCode::INTERNAL_SERVER_ERROR } } } /// Helper function to create AsyncItemService from AppState fn create_item_service(state: &AppState) -> AsyncItemService { AsyncItemService::new( state.data_dir.clone(), state.db.clone(), state.item_service.clone(), state.cmd.clone(), state.settings.clone(), ) } #[utoipa::path( get, path = "/api/item/", operation_id = "keep_list_items", summary = "List stored items", description = "Get paginated items with metadata and tags. Filter by tags, sort by creation time.", responses( (status = 200, description = "Items retrieved", body = ItemInfoListResponse), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ), params( ("tags" = Option, Query, description = "Comma-separated tags to filter"), ("order" = Option, Query, description = "Sort order: 'newest' or 'oldest'"), ("start" = Option, Query, description = "Pagination start index"), ("count" = Option, Query, description = "Number of items to return") ), security( ("bearerAuth" = []) ), tag = "item" )] pub async fn handle_list_items( State(state): State, Query(params): Query, ) -> Result { let tags: Vec = params .tags .as_ref() .map(|s| s.split(',').map(|t| t.trim().to_string()).collect()) .unwrap_or_default(); let item_service = create_item_service(&state); let mut items_with_meta = item_service .list_items(tags, HashMap::new()) .await .map_err(|e| { warn!("Failed to get items: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; // Apply ordering (default is newest first) match params.order.as_deref().unwrap_or("newest") { "newest" => items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.ts)), "oldest" => items_with_meta.sort_by(|a, b| a.item.ts.cmp(&b.item.ts)), _ => items_with_meta.sort_by(|a, b| b.item.ts.cmp(&a.item.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_with_meta: Vec<_> = items_with_meta .into_iter() .skip(start) .take(count) .collect(); let item_infos: Vec = items_with_meta .into_iter() .map(|item_with_meta| { let item_id = item_with_meta.item.id.unwrap_or(0); let item_tags: Vec = item_with_meta.tags.iter().map(|t| t.name.clone()).collect(); let item_meta = item_with_meta.meta_as_map(); ItemInfo { id: item_id, ts: item_with_meta.item.ts.to_rfc3339(), size: item_with_meta.item.size, compression: item_with_meta.item.compression, tags: item_tags, metadata: item_meta, } }) .collect(); ResponseBuilder::json(ApiResponse { success: true, data: Some(item_infos), error: None, }) } /// Handle as_meta=true response by returning JSON with metadata and content async fn handle_as_meta_response( item_service: &AsyncItemService, item_id: i64, offset: u64, length: u64, ) -> Result { // Get the item with metadata let item_with_meta = item_service.get_item(item_id).await.map_err(|e| { warn!("Failed to get item {} for as_meta content: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })?; let metadata = item_with_meta.meta_as_map(); handle_as_meta_response_with_metadata(item_service, item_id, &metadata, offset, length).await } /// Handle as_meta=true response with pre-fetched metadata async fn handle_as_meta_response_with_metadata( item_service: &AsyncItemService, item_id: i64, metadata: &HashMap, offset: u64, length: u64, ) -> Result { // Check if content is binary let is_binary = is_content_binary(item_service, item_id, metadata).await?; // Get the content if it's not binary if is_binary { // Return JSON with content as None and error message let response_body = serde_json::json!({ "metadata": metadata, "content": serde_json::Value::Null, "error": "Content is binary" }); Response::builder() .header(header::CONTENT_TYPE, "application/json") .status(StatusCode::UNPROCESSABLE_ENTITY) .body(axum::body::Body::from(response_body.to_string())) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) } else { // Get the content as text match item_service.get_item_content_info(item_id, None).await { Ok((content, _, _)) => { // Apply offset and length let content_len = content.len() as u64; let start = std::cmp::min(offset, content_len); let end = if length > 0 { std::cmp::min(start + length, content_len) } else { content_len }; let response_content = if start < content_len { &content[start as usize..end as usize] } else { &[] }; // Convert to UTF-8 string let content_str = match String::from_utf8(response_content.to_vec()) { Ok(s) => s, Err(_) => { // This shouldn't happen since we checked is_binary, but handle it just in case let response_body = serde_json::json!({ "metadata": metadata, "content": serde_json::Value::Null, "error": "Content is not valid UTF-8" }); let response = Response::builder() .header(header::CONTENT_TYPE, "application/json") .status(StatusCode::UNPROCESSABLE_ENTITY) .body(axum::body::Body::from(response_body.to_string())) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; return Ok(response); } }; // Return JSON with metadata and content let response_body = serde_json::json!({ "metadata": metadata, "content": content_str, "error": serde_json::Value::Null }); Response::builder() .header(header::CONTENT_TYPE, "application/json") .body(axum::body::Body::from(response_body.to_string())) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR) } Err(e) => { warn!("Failed to get content for item {}: {}", item_id, e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } } #[utoipa::path( post, path = "/api/item/", operation_id = "keep_post_item", summary = "Store new item", description = "Upload content to store as a new item. Content is compressed, analyzed for metadata, and stored.", responses( (status = 201, description = "Item created", body = ItemInfoResponse), (status = 400, description = "Bad request"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ), request_body( content = String, description = "Content to store", content_type = "application/octet-stream" ), security( ("bearerAuth" = []) ), tag = "item" )] pub async fn handle_post_item( State(_state): State, ) -> Result>, StatusCode> { // 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( get, path = "/api/item/latest/content", operation_id = "keep_get_item_latest_content", summary = "Download latest item content", description = "Get raw content of the most recent item. Filter by tags. Binary content can be restricted. \ AI agents should use as_meta=true to get content and metadata in a structured JSON format.", responses( (status = 200, description = "Content retrieved"), (status = 400, description = "Binary content not allowed"), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("tags" = Option, Query, description = "Tags to filter latest item"), ("allow_binary" = Option, Query, description = "Allow binary content"), ("offset" = Option, Query, description = "Byte offset to start reading"), ("length" = Option, Query, description = "Number of bytes to read"), ("stream" = Option, Query, description = "Stream response (true) or build in memory (false)"), ("as_meta" = Option, Query, description = "Return content and metadata in JSON format (recommended for AI agents)") ), security( ("bearerAuth" = []) ), tag = "item" )] pub async fn handle_get_item_latest_content( State(state): State, Query(params): Query, ) -> Result { let tags: Vec = params .tags .as_ref() .map(|s| s.split(',').map(|t| t.trim().to_string()).collect()) .unwrap_or_default(); let item_service = create_item_service(&state); // First find the item to get its ID and metadata let item_with_meta = item_service.find_item(vec![], tags, HashMap::new()).await; match item_with_meta { Ok(item) => { let item_id = item.item.id.unwrap(); let metadata = item.meta_as_map(); // Handle as_meta parameter if params.as_meta { // Force stream=false and allow_binary=false for as_meta=true handle_as_meta_response_with_metadata( &item_service, item_id, &metadata, params.offset, params.length, ) .await } else { stream_item_content_response_with_metadata( &item_service, item_id, &metadata, params.allow_binary, params.offset, params.length, params.stream, None, ) .await } } Err(CoreError::ItemNotFoundGeneric) => Err(StatusCode::NOT_FOUND), Err(e) => { warn!("Failed to find latest item for content: {}", e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } #[utoipa::path( get, path = "/api/item/{item_id}/content", operation_id = "keep_get_item_content", summary = "Download item content", description = "Get raw content of a specific item by ID. Binary content can be restricted. \ AI agents should use as_meta=true to get content and metadata in a structured JSON format.", responses( (status = 200, description = "Content retrieved"), (status = 400, description = "Invalid ID or binary not allowed"), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("item_id" = i64, Path, description = "Item ID"), ("allow_binary" = Option, Query, description = "Allow binary content"), ("offset" = Option, Query, description = "Byte offset to start reading"), ("length" = Option, Query, description = "Number of bytes to read"), ("stream" = Option, Query, description = "Stream response (true) or build in memory (false)"), ("as_meta" = Option, Query, description = "Return content and metadata in JSON format (recommended for AI agents)") ), security( ("bearerAuth" = []) ), tag = "item" )] pub async fn handle_get_item_content( State(state): State, Path(item_id): Path, Query(params): Query, ) -> Result { // Validate that item ID is positive to prevent path traversal issues if item_id <= 0 { return Err(StatusCode::BAD_REQUEST); } debug!( "ITEM_API: Getting content for item {} with stream={}, allow_binary={}, offset={}, length={}", item_id, params.stream, params.allow_binary, params.offset, params.length ); let filter = build_filter_string(¶ms); let item_service = create_item_service(&state); // Handle as_meta parameter if params.as_meta { // Force stream=false and allow_binary=false for as_meta=true let result = handle_as_meta_response(&item_service, item_id, params.offset, params.length).await; if let Ok(response) = &result { debug!( "ITEM_API: Response content-length: {:?}", response.headers().get("content-length") ); } result } else { let result = stream_item_content_response( &item_service, item_id, params.allow_binary, params.offset, params.length, params.stream, filter, ) .await; if let Ok(response) = &result { debug!( "ITEM_API: Response content-length: {:?}", response.headers().get("content-length") ); } result } } async fn stream_item_content_response( item_service: &AsyncItemService, item_id: i64, allow_binary: bool, offset: u64, length: u64, stream: bool, filter: Option, ) -> Result { debug!("STREAM_ITEM_CONTENT_RESPONSE: stream={}", stream); // Get the item with metadata once let item_with_meta = item_service.get_item(item_id).await.map_err(|e| { warn!("Failed to get item {} for content: {}", item_id, e); StatusCode::INTERNAL_SERVER_ERROR })?; let metadata = item_with_meta.meta_as_map(); stream_item_content_response_with_metadata( item_service, item_id, &metadata, allow_binary, offset, length, stream, filter, ) .await } async fn stream_item_content_response_with_metadata( item_service: &AsyncItemService, item_id: i64, metadata: &HashMap, allow_binary: bool, offset: u64, length: u64, stream: bool, filter: Option, ) -> Result { debug!( "STREAM_ITEM_CONTENT_RESPONSE_WITH_METADATA: stream={}", stream ); let mime_type = get_mime_type(metadata); // Check if content is binary when allow_binary is false check_binary_content_allowed(item_service, item_id, metadata, allow_binary).await?; if stream { debug!("STREAMING: Using streaming approach"); match item_service .stream_item_content_by_id_with_metadata( item_id, metadata, true, offset, length, filter, ) .await { Ok((stream, _)) => { let body = axum::body::Body::from_stream(stream); let response = Response::builder() .header(header::CONTENT_TYPE, mime_type) .body(body) .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; Ok(response) } Err(e) => { warn!("Failed to stream content for item {}: {}", item_id, e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } else { debug!("NON-STREAMING: Building full response in memory"); match item_service.get_item_content_info(item_id, filter).await { Ok((content, _, _)) => { let response_content = apply_offset_length(&content, offset, length); debug!( "NON-STREAMING: Content length: {}, response length: {}", content.len(), response_content.len() ); ResponseBuilder::binary(response_content, &mime_type) } Err(e) => { warn!("Failed to get content for item {}: {}", item_id, e); Err(StatusCode::INTERNAL_SERVER_ERROR) } } } } #[utoipa::path( get, path = "/api/item/latest/meta", operation_id = "keep_get_item_latest_meta", summary = "Get latest item metadata", description = "Retrieve metadata for the most recent item. Filter by tags.", responses( (status = 200, description = "Metadata retrieved", body = MetadataResponse), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("tags" = Option, Query, description = "Tags to filter latest item") ), security( ("bearerAuth" = []) ), tag = "item" )] pub async fn handle_get_item_latest_meta( State(state): State, Query(params): Query, ) -> Result>>, StatusCode> { let tags: Vec = params .tags .as_ref() .map(|s| s.split(',').map(|t| t.trim().to_string()).collect()) .unwrap_or_default(); let item_service = create_item_service(&state); match item_service.find_item(vec![], tags, HashMap::new()).await { Ok(item_with_meta) => { let item_meta = item_with_meta.meta_as_map(); let response = ApiResponse { success: true, data: Some(item_meta), error: None, }; Ok(Json(response)) } Err(e) => Err(handle_item_error(e)), } } #[utoipa::path( get, path = "/api/item/{item_id}/meta", operation_id = "keep_get_item_meta", summary = "Get item metadata", description = "Retrieve metadata for a specific item by ID.", responses( (status = 200, description = "Metadata retrieved", body = MetadataResponse), (status = 400, description = "Invalid ID"), (status = 401, description = "Unauthorized"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error") ), params( ("item_id" = i64, Path, description = "Item ID") ), security( ("bearerAuth" = []) ), tag = "item" )] pub async fn handle_get_item_meta( State(state): State, Path(item_id): Path, ) -> Result>>, StatusCode> { let item_service = create_item_service(&state); match item_service.get_item(item_id).await { Ok(item_with_meta) => { let item_meta = item_with_meta.meta_as_map(); let response = ApiResponse { success: true, data: Some(item_meta), error: None, }; Ok(Json(response)) } Err(e) => Err(handle_item_error(e)), } }