feat: enhance HTTP logging and API responses with content metadata

Co-authored-by: aider (openai/andrew/openrouter/anthropic/claude-sonnet-4) <aider@aider.chat>
This commit is contained in:
Andrew Phillips
2025-08-13 13:20:57 -03:00
parent 8bd918129c
commit b07c9df812
4 changed files with 173 additions and 72 deletions

View File

@@ -13,25 +13,27 @@ use anyhow::{Result, anyhow};
use crate::compression_engine::{CompressionType, get_compression_engine};
use crate::db;
use crate::modes::server::common::{AppState, ApiResponse, ItemInfo, TagsQuery, ListItemsQuery};
use crate::modes::server::common::{AppState, ApiResponse, ItemInfo, ItemContentInfo, TagsQuery, ListItemsQuery};
use crate::common::is_binary::is_binary;
#[utoipa::path(
get,
path = "/api/item/",
responses(
(status = 200, description = "Successfully retrieved list of items", body = ApiResponse<Vec<ItemInfo>>),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved paginated list of items with metadata and tags", body = ApiResponse<Vec<ItemInfo>>),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 500, description = "Internal server error - Failed to retrieve items from database")
),
params(
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by"),
("order" = Option<String>, Query, description = "Sort order (newest or oldest)"),
("start" = Option<u64>, Query, description = "Starting index for pagination"),
("count" = Option<u64>, Query, description = "Number of items to return")
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by (e.g., 'important,work')"),
("order" = Option<String>, Query, description = "Sort order: 'newest' (default) or 'oldest'"),
("start" = Option<u64>, Query, description = "Starting index for pagination (default: 0)"),
("count" = Option<u64>, Query, description = "Maximum number of items to return (default: 100, max: 1000)")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_list_items(
State(state): State<AppState>,
@@ -121,13 +123,20 @@ pub async fn handle_list_items(
post,
path = "/api/item/",
responses(
(status = 200, description = "Successfully created item", body = ApiResponse<ItemInfo>),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Internal server error")
(status = 201, description = "Successfully created new item", body = ApiResponse<ItemInfo>),
(status = 400, description = "Bad request - Invalid input data"),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 500, description = "Internal server error - Failed to create item")
),
request_body(
content = String,
description = "Content to store as new item",
content_type = "application/octet-stream"
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_post_item(
State(_state): State<AppState>,
@@ -150,17 +159,19 @@ pub async fn handle_post_item(
delete,
path = "/api/item/{item_id}",
responses(
(status = 200, description = "Successfully deleted item"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Item not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully deleted item and associated metadata"),
(status = 400, description = "Bad request - Invalid item ID"),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 404, description = "Item not found - No item exists with the specified ID"),
(status = 500, description = "Internal server error - Failed to delete item")
),
params(
("item_id" = i64, Path, description = "ID of the item to delete")
("item_id" = i64, Path, description = "Unique identifier of the item to delete (must be positive)")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_delete_item(
State(state): State<AppState>,
@@ -194,22 +205,23 @@ pub async fn handle_delete_item(
get,
path = "/api/item/latest",
responses(
(status = 200, description = "Successfully retrieved latest item content", body = ApiResponse<String>),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Item not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved latest item with content and metadata. Content is included if item is text-based, otherwise only metadata is returned.", body = ApiResponse<ItemContentInfo>),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 404, description = "Item not found - No items exist or no items match the specified tags"),
(status = 500, description = "Internal server error - Failed to retrieve item content")
),
params(
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by")
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by (e.g., 'important,work'). If specified, returns the latest item matching ALL tags.")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_get_item_latest(
State(state): State<AppState>,
Query(params): Query<TagsQuery>,
) -> Result<Json<ApiResponse<String>>, StatusCode> {
) -> Result<Json<ApiResponse<ItemContentInfo>>, StatusCode> {
let mut conn = state.db.lock().await;
@@ -228,18 +240,18 @@ pub async fn handle_get_item_latest(
};
if let Some(item) = item {
match get_item_content(&item, &state.data_dir).await {
Ok(content) => {
match get_item_content_info(&item, &state.data_dir, &mut *conn).await {
Ok(content_info) => {
let response = ApiResponse {
success: true,
data: Some(content),
data: Some(content_info),
error: None,
};
Ok(Json(response))
}
Err(e) => {
warn!("Failed to get content for item {}: {}", item.id.unwrap_or(0), e);
let response = ApiResponse::<String> {
let response = ApiResponse::<ItemContentInfo> {
success: false,
data: None,
error: Some(format!("Failed to retrieve content: {}", e)),
@@ -256,22 +268,24 @@ pub async fn handle_get_item_latest(
get,
path = "/api/item/{item_id}",
responses(
(status = 200, description = "Successfully retrieved item content", body = ApiResponse<String>),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Item not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved item with content and metadata. Content is included if item is text-based, otherwise only metadata is returned.", body = ApiResponse<ItemContentInfo>),
(status = 400, description = "Bad request - Invalid item ID"),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 404, description = "Item not found - No item exists with the specified ID"),
(status = 500, description = "Internal server error - Failed to retrieve item content")
),
params(
("item_id" = i64, Path, description = "ID of the item to retrieve")
("item_id" = i64, Path, description = "Unique identifier of the item to retrieve (must be positive)")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_get_item(
State(state): State<AppState>,
Path(item_id): Path<i64>,
) -> Result<Json<ApiResponse<String>>, StatusCode> {
) -> Result<Json<ApiResponse<ItemContentInfo>>, StatusCode> {
// Validate that item ID is positive to prevent path traversal issues
if item_id <= 0 {
return Err(StatusCode::BAD_REQUEST);
@@ -283,18 +297,18 @@ pub async fn handle_get_item(
warn!("Failed to get item {} for content: {}", item_id, e);
StatusCode::INTERNAL_SERVER_ERROR
})? {
match get_item_content(&item, &state.data_dir).await {
Ok(content) => {
match get_item_content_info(&item, &state.data_dir, &mut *conn).await {
Ok(content_info) => {
let response = ApiResponse {
success: true,
data: Some(content),
data: Some(content_info),
error: None,
};
Ok(Json(response))
}
Err(e) => {
warn!("Failed to get content for item {}: {}", item_id, e);
let response = ApiResponse::<String> {
let response = ApiResponse::<ItemContentInfo> {
success: false,
data: None,
error: Some(format!("Failed to retrieve content: {}", e)),
@@ -311,17 +325,18 @@ pub async fn handle_get_item(
get,
path = "/api/item/latest/content",
responses(
(status = 200, description = "Successfully retrieved latest item raw content"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Item not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved latest item raw content with appropriate MIME type header"),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 404, description = "Item not found - No items exist or no items match the specified tags"),
(status = 500, description = "Internal server error - Failed to retrieve item content")
),
params(
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by")
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by (e.g., 'important,work'). If specified, returns the latest item matching ALL tags.")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_get_item_latest_content(
State(state): State<AppState>,
@@ -368,17 +383,19 @@ pub async fn handle_get_item_latest_content(
get,
path = "/api/item/{item_id}/content",
responses(
(status = 200, description = "Successfully retrieved item raw content"),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Item not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved item raw content with appropriate MIME type header"),
(status = 400, description = "Bad request - Invalid item ID"),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 404, description = "Item not found - No item exists with the specified ID"),
(status = 500, description = "Internal server error - Failed to retrieve item content")
),
params(
("item_id" = i64, Path, description = "ID of the item to retrieve")
("item_id" = i64, Path, description = "Unique identifier of the item to retrieve (must be positive)")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_get_item_content(
State(state): State<AppState>,
@@ -436,6 +453,57 @@ async fn get_item_content(item: &db::Item, data_dir: &PathBuf) -> Result<String>
Ok(content)
}
async fn get_item_content_info(item: &db::Item, data_dir: &PathBuf, conn: &mut rusqlite::Connection) -> Result<ItemContentInfo> {
let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?;
// Validate that item ID is positive to prevent path traversal issues
if item_id <= 0 {
return Err(anyhow!("Invalid item ID: {}", item_id));
}
// Get metadata
let meta_entries = db::get_item_meta(conn, item)
.map_err(|e| anyhow!("Failed to get metadata: {}", e))?;
let metadata: HashMap<String, String> = meta_entries
.iter()
.map(|m| (m.name.clone(), m.value.clone()))
.collect();
// Determine if content is binary
let is_binary = if let Some(binary_meta) = metadata.get("binary") {
binary_meta == "true"
} else {
// Fall back to checking the actual content
let mut item_path = data_dir.clone();
item_path.push(item_id.to_string());
let compression_type = CompressionType::from_str(&item.compression)?;
let compression_engine = get_compression_engine(compression_type)?;
let mut reader = compression_engine.open(item_path)?;
let mut buffer = [0u8; 8192]; // Read first 8KB to check
let bytes_read = reader.read(&mut buffer)?;
is_binary(&buffer[..bytes_read])
};
// Get content if not binary
let content = if is_binary {
None
} else {
match get_item_content(item, data_dir).await {
Ok(content_str) => Some(content_str),
Err(_) => None, // If we can't read as string, treat as binary
}
};
Ok(ItemContentInfo {
metadata,
content,
binary: is_binary,
})
}
async fn get_item_raw_content(item: &db::Item, data_dir: &PathBuf, conn: &mut rusqlite::Connection) -> Result<(Vec<u8>, String)> {
let item_id = item.id.ok_or_else(|| anyhow!("Item missing ID"))?;
@@ -472,17 +540,18 @@ async fn get_item_raw_content(item: &db::Item, data_dir: &PathBuf, conn: &mut ru
get,
path = "/api/item/latest/meta",
responses(
(status = 200, description = "Successfully retrieved latest item metadata", body = ApiResponse<HashMap<String, String>>),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Item not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved latest item metadata including file type, encoding, size, and other system information", body = ApiResponse<HashMap<String, String>>),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 404, description = "Item not found - No items exist or no items match the specified tags"),
(status = 500, description = "Internal server error - Failed to retrieve item metadata")
),
params(
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by")
("tags" = Option<String>, Query, description = "Comma-separated list of tags to filter by (e.g., 'important,work'). If specified, returns the latest item matching ALL tags.")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_get_item_latest_meta(
State(state): State<AppState>,
@@ -531,17 +600,19 @@ pub async fn handle_get_item_latest_meta(
get,
path = "/api/item/{item_id}/meta",
responses(
(status = 200, description = "Successfully retrieved item metadata", body = ApiResponse<HashMap<String, String>>),
(status = 401, description = "Unauthorized"),
(status = 404, description = "Item not found"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved item metadata including file type, encoding, size, and other system information", body = ApiResponse<HashMap<String, String>>),
(status = 400, description = "Bad request - Invalid item ID"),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 404, description = "Item not found - No item exists with the specified ID"),
(status = 500, description = "Internal server error - Failed to retrieve item metadata")
),
params(
("item_id" = i64, Path, description = "ID of the item to retrieve metadata for")
("item_id" = i64, Path, description = "Unique identifier of the item to retrieve metadata for (must be positive)")
),
security(
("bearerAuth" = [])
)
),
tag = "item"
)]
pub async fn handle_get_item_meta(
State(state): State<AppState>,

View File

@@ -12,6 +12,14 @@ use utoipa_swagger_ui::SwaggerUi;
#[derive(OpenApi)]
#[openapi(
info(
title = "Keep API",
version = "0.1.0",
description = "REST API for Keep - a tool to manage temporary files with automatic compression and metadata generation",
contact(
name = "Keep Project",
)
),
paths(
status::handle_status,
item::handle_list_items,
@@ -27,11 +35,19 @@ use utoipa_swagger_ui::SwaggerUi;
schemas(
crate::common::status::StatusInfo,
crate::modes::server::common::ItemInfo,
crate::modes::server::common::ItemContentInfo,
),
security_schemes(
("bearerAuth" = ("http", "bearer")),
("basicAuth" = ("http", "basic"))
)
),
tags(
(name = "status", description = "Status API endpoints"),
(name = "item", description = "Item management API endpoints")
(name = "status", description = "System status and health check endpoints"),
(name = "item", description = "Item management endpoints for storing, retrieving, and managing content with metadata")
),
servers(
(url = "/", description = "Local server")
)
)]
struct ApiDoc;

View File

@@ -12,13 +12,14 @@ use crate::meta_plugin::MetaPluginType;
get,
path = "/api/status",
responses(
(status = 200, description = "Successfully retrieved status information", body = ApiResponse<StatusInfo>),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Internal server error")
(status = 200, description = "Successfully retrieved status information including database path, data directory, and supported plugins", body = ApiResponse<StatusInfo>),
(status = 401, description = "Unauthorized - Invalid or missing authentication credentials"),
(status = 500, description = "Internal server error - Failed to retrieve status information")
),
security(
("bearerAuth" = [])
)
),
tag = "status"
)]
pub async fn handle_status(
State(state): State<AppState>,