feat: add as_meta parameter to content endpoint

Co-authored-by: aider (openai/andrew/openrouter/deepseek/deepseek-chat-v3.1) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-08-28 17:57:50 -03:00
parent 2ab0ed1938
commit 729d7a060b
2 changed files with 143 additions and 6 deletions

View File

@@ -97,6 +97,120 @@ pub async fn handle_list_items(
Ok(Json(response)) 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<Response, StatusCode> {
// 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<String, String>,
offset: u64,
length: u64,
) -> Result<Response, StatusCode> {
// 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( #[utoipa::path(
post, post,
@@ -190,8 +304,14 @@ pub async fn handle_get_item_latest_content(
Ok(item) => { Ok(item) => {
let item_id = item.item.id.unwrap(); let item_id = item.item.id.unwrap();
let metadata = item.meta_as_map(); 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).await 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(CoreError::ItemNotFoundGeneric) => Err(StatusCode::NOT_FOUND),
Err(e) => { Err(e) => {
warn!("Failed to find latest item for content: {}", e); warn!("Failed to find latest item for content: {}", e);
@@ -246,12 +366,21 @@ pub async fn handle_get_item_content(
state.cmd.clone(), state.cmd.clone(),
state.settings.clone() state.settings.clone()
); );
let result = stream_item_content_response(&item_service, item_id, 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
let result = handle_as_meta_response(&item_service, item_id, params.offset, params.length).await;
if let Ok(response) = &result { if let Ok(response) = &result {
debug!("ITEM_API: Response content-length: {:?}", response.headers().get("content-length")); debug!("ITEM_API: Response content-length: {:?}", response.headers().get("content-length"));
} }
result 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
}
} }
async fn stream_item_content_response( async fn stream_item_content_response(

View File

@@ -130,6 +130,8 @@ pub struct ItemQuery {
pub length: u64, pub length: u64,
#[serde(default = "default_stream")] #[serde(default = "default_stream")]
pub stream: bool, pub stream: bool,
#[serde(default = "default_as_meta")]
pub as_meta: bool,
} }
#[derive(Debug, Deserialize, utoipa::ToSchema)] #[derive(Debug, Deserialize, utoipa::ToSchema)]
@@ -143,6 +145,8 @@ pub struct ItemContentQuery {
pub length: u64, pub length: u64,
#[serde(default = "default_stream")] #[serde(default = "default_stream")]
pub stream: bool, 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<String>) -> bool { fn check_bearer_auth(auth_str: &str, expected_password: &str, expected_hash: &Option<String>) -> bool {
@@ -169,6 +173,10 @@ fn default_stream() -> bool {
false false
} }
fn default_as_meta() -> bool {
false
}
fn check_basic_auth(auth_str: &str, expected_password: &str, expected_hash: &Option<String>) -> bool { fn check_basic_auth(auth_str: &str, expected_password: &str, expected_hash: &Option<String>) -> bool {
if !auth_str.starts_with("Basic ") { if !auth_str.starts_with("Basic ") {
return false; return false;