diff --git a/src/modes/server/api/item.rs b/src/modes/server/api/item.rs index c4139d6..7562f6c 100644 --- a/src/modes/server/api/item.rs +++ b/src/modes/server/api/item.rs @@ -97,6 +97,120 @@ pub async fn handle_list_items( Ok(Json(response)) } +/// 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 = if let Some(text_val) = metadata.get("text") { + 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).await { + Ok((_, _, is_binary)) => is_binary, + Err(e) => { + warn!("Failed to get content info for binary check for item {}: {}", item_id, e); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + } + }; + + // 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" + }); + + 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)?; + Ok(response) + } else { + // Get the content as text + match item_service.get_item_content_info(item_id).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 + }); + + let response = Response::builder() + .header(header::CONTENT_TYPE, "application/json") + .body(axum::body::Body::from(response_body.to_string())) + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + Ok(response) + } + Err(e) => { + warn!("Failed to get content for item {}: {}", item_id, e); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } + } +} + #[utoipa::path( post, @@ -190,7 +304,13 @@ pub async fn handle_get_item_latest_content( Ok(item) => { let item_id = item.item.id.unwrap(); let metadata = item.meta_as_map(); - stream_item_content_response_with_metadata(&item_service, item_id, &metadata, params.allow_binary, params.offset, params.length, params.stream).await + // 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).await + } } Err(CoreError::ItemNotFoundGeneric) => Err(StatusCode::NOT_FOUND), Err(e) => { @@ -246,12 +366,21 @@ pub async fn handle_get_item_content( state.cmd.clone(), state.settings.clone() ); - let result = stream_item_content_response(&item_service, item_id, params.allow_binary, params.offset, params.length, params.stream).await; - - if let Ok(response) = &result { - debug!("ITEM_API: Response content-length: {:?}", response.headers().get("content-length")); + // 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).await; + if let Ok(response) = &result { + debug!("ITEM_API: Response content-length: {:?}", response.headers().get("content-length")); + } + result } - result } async fn stream_item_content_response( diff --git a/src/modes/server/common.rs b/src/modes/server/common.rs index db3af4f..d3c8e3a 100644 --- a/src/modes/server/common.rs +++ b/src/modes/server/common.rs @@ -130,6 +130,8 @@ pub struct ItemQuery { pub length: u64, #[serde(default = "default_stream")] pub stream: bool, + #[serde(default = "default_as_meta")] + pub as_meta: bool, } #[derive(Debug, Deserialize, utoipa::ToSchema)] @@ -143,6 +145,8 @@ pub struct ItemContentQuery { pub length: u64, #[serde(default = "default_stream")] pub stream: bool, + #[serde(default = "default_as_meta")] + pub as_meta: bool, } fn check_bearer_auth(auth_str: &str, expected_password: &str, expected_hash: &Option) -> bool { @@ -169,6 +173,10 @@ fn default_stream() -> bool { false } +fn default_as_meta() -> bool { + false +} + fn check_basic_auth(auth_str: &str, expected_password: &str, expected_hash: &Option) -> bool { if !auth_str.starts_with("Basic ") { return false;