704 lines
24 KiB
Rust
704 lines
24 KiB
Rust
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<String, String>,
|
|
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<String, String>,
|
|
) -> Result<bool, StatusCode> {
|
|
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<String> {
|
|
// Implement this based on your needs
|
|
None
|
|
}
|
|
|
|
// Create a simple ResponseBuilder to replace the missing one
|
|
struct ResponseBuilder;
|
|
|
|
impl ResponseBuilder {
|
|
pub fn json<T: serde::Serialize>(data: T) -> Result<Response, StatusCode> {
|
|
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, StatusCode> {
|
|
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, String>) -> 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<String>, Query, description = "Comma-separated tags to filter"),
|
|
("order" = Option<String>, Query, description = "Sort order: 'newest' or 'oldest'"),
|
|
("start" = Option<u64>, Query, description = "Pagination start index"),
|
|
("count" = Option<u64>, Query, description = "Number of items to return")
|
|
),
|
|
security(
|
|
("bearerAuth" = [])
|
|
),
|
|
tag = "item"
|
|
)]
|
|
pub async fn handle_list_items(
|
|
State(state): State<AppState>,
|
|
Query(params): Query<ListItemsQuery>,
|
|
) -> Result<Response, StatusCode> {
|
|
let tags: Vec<String> = 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<ItemInfo> = items_with_meta
|
|
.into_iter()
|
|
.map(|item_with_meta| {
|
|
let item_id = item_with_meta.item.id.unwrap_or(0);
|
|
let item_tags: Vec<String> =
|
|
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<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 = 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<AppState>,
|
|
) -> Result<Json<ApiResponse<ItemInfo>>, 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::<ItemInfo> {
|
|
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<String>, Query, description = "Tags to filter latest item"),
|
|
("allow_binary" = Option<bool>, Query, description = "Allow binary content"),
|
|
("offset" = Option<u64>, Query, description = "Byte offset to start reading"),
|
|
("length" = Option<u64>, Query, description = "Number of bytes to read"),
|
|
("stream" = Option<bool>, Query, description = "Stream response (true) or build in memory (false)"),
|
|
("as_meta" = Option<bool>, 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<AppState>,
|
|
Query(params): Query<ItemContentQuery>,
|
|
) -> Result<Response, StatusCode> {
|
|
let tags: Vec<String> = 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<bool>, Query, description = "Allow binary content"),
|
|
("offset" = Option<u64>, Query, description = "Byte offset to start reading"),
|
|
("length" = Option<u64>, Query, description = "Number of bytes to read"),
|
|
("stream" = Option<bool>, Query, description = "Stream response (true) or build in memory (false)"),
|
|
("as_meta" = Option<bool>, 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<AppState>,
|
|
Path(item_id): Path<i64>,
|
|
Query(params): Query<ItemQuery>,
|
|
) -> Result<Response, StatusCode> {
|
|
// 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<String>,
|
|
) -> Result<Response, StatusCode> {
|
|
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<String, String>,
|
|
allow_binary: bool,
|
|
offset: u64,
|
|
length: u64,
|
|
stream: bool,
|
|
filter: Option<String>,
|
|
) -> Result<Response, StatusCode> {
|
|
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<String>, Query, description = "Tags to filter latest item")
|
|
),
|
|
security(
|
|
("bearerAuth" = [])
|
|
),
|
|
tag = "item"
|
|
)]
|
|
pub async fn handle_get_item_latest_meta(
|
|
State(state): State<AppState>,
|
|
Query(params): Query<TagsQuery>,
|
|
) -> Result<Json<ApiResponse<HashMap<String, String>>>, StatusCode> {
|
|
let tags: Vec<String> = 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<AppState>,
|
|
Path(item_id): Path<i64>,
|
|
) -> Result<Json<ApiResponse<HashMap<String, String>>>, 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)),
|
|
}
|
|
}
|