From 077adc0cb028c7b43b845903b498be3e236647bb Mon Sep 17 00:00:00 2001 From: Andrew Phillips Date: Tue, 12 Aug 2025 14:32:17 -0300 Subject: [PATCH] refactor: remove redundant server API modules and update mod.rs exports Co-authored-by: aider (openai/andrew/openrouter/qwen/qwen3-coder) --- src/modes/server/api/mod.rs | 3 + src/modes/server/api/status.rs | 56 --- src/modes/server/content.rs | 624 --------------------------------- src/modes/server/items.rs | 397 --------------------- 4 files changed, 3 insertions(+), 1077 deletions(-) diff --git a/src/modes/server/api/mod.rs b/src/modes/server/api/mod.rs index e69de29..842675d 100644 --- a/src/modes/server/api/mod.rs +++ b/src/modes/server/api/mod.rs @@ -0,0 +1,3 @@ +pub mod item; +pub mod status; +pub mod docs; diff --git a/src/modes/server/api/status.rs b/src/modes/server/api/status.rs index a158bd5..e69de29 100644 --- a/src/modes/server/api/status.rs +++ b/src/modes/server/api/status.rs @@ -1,56 +0,0 @@ -use axum::{ - extract::{ConnectInfo, State}, - http::{HeaderMap, StatusCode}, - response::Json, -}; -use clap::Command; -use log::warn; -use serde_json::json; - -use crate::meta_plugin::MetaPluginType; -use crate::modes::status::{StatusInfo, generate_status_info}; -use crate::modes::server::common::{AppState, ApiResponse, check_auth}; - -pub async fn handle_status( - State(state): State, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request from {}", addr); - return Err(StatusCode::UNAUTHORIZED); - } - - // Use the actual args that the server was started with - let args = &state.args; - - // Determine which meta plugins would be enabled for a save operation - let mut meta_plugin_types: Vec = crate::modes::common::cmd_args_meta_plugin_types(&mut Command::new("keep"), args); - - // Add digest type if specified - let digest_type = crate::modes::common::cmd_args_digest_type(&mut Command::new("keep"), args); - let digest_meta_plugin_type = match digest_type { - crate::meta_plugin::MetaPluginType::DigestSha256 => Some(MetaPluginType::DigestSha256), - crate::meta_plugin::MetaPluginType::DigestMd5 => Some(MetaPluginType::DigestMd5), - _ => None, - }; - - if let Some(digest_plugin_type) = digest_meta_plugin_type { - if !meta_plugin_types.contains(&digest_plugin_type) { - meta_plugin_types.push(digest_plugin_type); - } - } - - let mut db_path = state.data_dir.clone(); - db_path.push("keep-1.db"); - - let status_info = generate_status_info(state.data_dir.clone(), db_path, &meta_plugin_types); - - let response = ApiResponse { - success: true, - data: Some(status_info), - error: None, - }; - - Ok(Json(response)) -} diff --git a/src/modes/server/content.rs b/src/modes/server/content.rs index 04d45d4..e69de29 100644 --- a/src/modes/server/content.rs +++ b/src/modes/server/content.rs @@ -1,624 +0,0 @@ -use anyhow::{Result, anyhow}; -use axum::{ - extract::{ConnectInfo, Path, Query, State}, - http::{HeaderMap, StatusCode, header}, - response::{Json, Response, IntoResponse}, -}; -use log::warn; -use serde_json::json; -use std::collections::HashMap; -use std::io::Read; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::str::FromStr; - -use crate::compression_engine::{CompressionType, get_compression_engine}; -use crate::db; -use super::common::{AppState, ApiResponse, TagsQuery, check_auth, ItemInfo}; - -pub async fn handle_get_content_latest( - State(state): State, - Query(params): Query, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/latest/content from {}", addr); - return Err(StatusCode::UNAUTHORIZED); - } - - let mut conn = state.db.lock().await; - - let item = if let Some(tags_str) = params.tags { - let tags: Vec = tags_str.split(',').map(|t| t.trim().to_string()).collect(); - db::get_item_matching(&mut *conn, &tags, &HashMap::new()) - .map_err(|e| { - warn!("Failed to get item matching tags {:?} for content: {}", tags, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - } else { - db::get_item_last(&mut *conn).map_err(|e| { - warn!("Failed to get last item for content: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })? - }; - - if let Some(item) = item { - match get_item_content(&item, &state.data_dir).await { - Ok(content) => { - let response = ApiResponse { - success: true, - data: Some(content), - error: None, - }; - Ok(Json(response)) - } - Err(e) => { - warn!("Failed to get content for item {}: {}", item.id.unwrap_or(0), e); - let response = ApiResponse:: { - success: false, - data: None, - error: Some(format!("Failed to retrieve content: {}", e)), - }; - Ok(Json(response)) - } - } - } else { - Err(StatusCode::NOT_FOUND) - } -} - -pub async fn handle_get_content( - State(state): State, - Path(item_id): Path, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/{}/content from {}", item_id, addr); - return Err(StatusCode::UNAUTHORIZED); - } - - if let Ok(id) = item_id.parse::() { - // Validate that item ID is positive to prevent path traversal issues - if id <= 0 { - warn!("Invalid item ID {} from {}", id, addr); - return Err(StatusCode::BAD_REQUEST); - } - - let mut conn = state.db.lock().await; - - if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| { - warn!("Failed to get item {} for content: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? { - match get_item_content(&item, &state.data_dir).await { - Ok(content) => { - let response = ApiResponse { - success: true, - data: Some(content), - error: None, - }; - Ok(Json(response)) - } - Err(e) => { - warn!("Failed to get content for item {}: {}", id, e); - let response = ApiResponse:: { - success: false, - data: None, - error: Some(format!("Failed to retrieve content: {}", e)), - }; - Ok(Json(response)) - } - } - } else { - Err(StatusCode::NOT_FOUND) - } - } else { - Err(StatusCode::BAD_REQUEST) - } -} - -pub async fn handle_get_content_latest_raw( - State(state): State, - Query(params): Query, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/latest/content from {}", addr); - return Err(StatusCode::UNAUTHORIZED); - } - - let mut conn = state.db.lock().await; - - let item = if let Some(tags_str) = params.tags { - let tags: Vec = tags_str.split(',').map(|t| t.trim().to_string()).collect(); - db::get_item_matching(&mut *conn, &tags, &HashMap::new()) - .map_err(|e| { - warn!("Failed to get item matching tags {:?} for content: {}", tags, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - } else { - db::get_item_last(&mut *conn).map_err(|e| { - warn!("Failed to get last item for content: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })? - }; - - if let Some(item) = item { - match get_item_raw_content(&item, &state.data_dir, &mut *conn).await { - Ok((content, mime_type)) => { - let mut response = content.into_response(); - response.headers_mut().insert( - header::CONTENT_TYPE, - mime_type.parse().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - ); - Ok(response) - } - Err(e) => { - warn!("Failed to get raw content for item {}: {}", item.id.unwrap_or(0), e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } else { - Err(StatusCode::NOT_FOUND) - } -} - -pub async fn handle_get_content_raw( - State(state): State, - Path(item_id): Path, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/{}/content from {}", item_id, addr); - return Err(StatusCode::UNAUTHORIZED); - } - - if let Ok(id) = item_id.parse::() { - // Validate that item ID is positive to prevent path traversal issues - if id <= 0 { - warn!("Invalid item ID {} from {}", id, addr); - return Err(StatusCode::BAD_REQUEST); - } - - let mut conn = state.db.lock().await; - - if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| { - warn!("Failed to get item {} for content: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? { - match get_item_raw_content(&item, &state.data_dir, &mut *conn).await { - Ok((content, mime_type)) => { - let mut response = content.into_response(); - response.headers_mut().insert( - header::CONTENT_TYPE, - mime_type.parse().map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - ); - Ok(response) - } - Err(e) => { - warn!("Failed to get raw content for item {}: {}", id, e); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } else { - Err(StatusCode::NOT_FOUND) - } - } else { - Err(StatusCode::BAD_REQUEST) - } -} - -async fn get_item_content(item: &db::Item, data_dir: &PathBuf) -> Result { - 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)); - } - - 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)?; - - // Read the content using the compression engine - let mut reader = compression_engine.open(item_path)?; - let mut content = String::new(); - reader.read_to_string(&mut content)?; - - Ok(content) -} - -async fn get_item_raw_content(item: &db::Item, data_dir: &PathBuf, conn: &mut rusqlite::Connection) -> Result<(Vec, String)> { - 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)); - } - - 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)?; - - // Read the raw content using the compression engine - let mut reader = compression_engine.open(item_path)?; - let mut content = Vec::new(); - reader.read_to_end(&mut content)?; - - // Get MIME type from metadata - let meta_entries = db::get_item_meta(conn, item) - .map_err(|e| anyhow!("Failed to get metadata: {}", e))?; - - let mime_type = meta_entries - .iter() - .find(|m| m.name == "file.mime") - .map(|m| m.value.clone()) - .unwrap_or_else(|| "application/octet-stream".to_string()); - - Ok((content, mime_type)) -} - -pub async fn handle_get_item_latest_meta( - State(state): State, - Query(params): Query, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/latest/meta from {}", addr); - return Err(StatusCode::UNAUTHORIZED); - } - - let mut conn = state.db.lock().await; - - let item = if let Some(tags_str) = params.tags { - let tags: Vec = tags_str.split(',').map(|t| t.trim().to_string()).collect(); - db::get_item_matching(&mut *conn, &tags, &HashMap::new()) - .map_err(|e| { - warn!("Failed to get item matching tags {:?} for meta: {}", tags, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - } else { - db::get_item_last(&mut *conn).map_err(|e| { - warn!("Failed to get last item for meta: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })? - }; - - if let Some(item) = item { - let item_meta = db::get_item_meta(&mut *conn, &item) - .map_err(|e| { - warn!("Failed to get metadata for item {}: {}", item.id.unwrap_or(0), e); - StatusCode::INTERNAL_SERVER_ERROR - })? - .into_iter() - .map(|m| (m.name, m.value)) - .collect(); - - let response = ApiResponse { - success: true, - data: Some(item_meta), - error: None, - }; - - Ok(Json(response)) - } else { - Err(StatusCode::NOT_FOUND) - } -} - -pub async fn handle_get_item_meta( - State(state): State, - Path(item_id): Path, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/{}/meta from {}", item_id, addr); - return Err(StatusCode::UNAUTHORIZED); - } - - if let Ok(id) = item_id.parse::() { - let mut conn = state.db.lock().await; - - if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| { - warn!("Failed to get item {} for meta: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? { - let item_meta = db::get_item_meta(&mut *conn, &item) - .map_err(|e| { - warn!("Failed to get metadata for item {}: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - .into_iter() - .map(|m| (m.name, m.value)) - .collect(); - - let response = ApiResponse { - success: true, - data: Some(item_meta), - error: None, - }; - - Ok(Json(response)) - } else { - Err(StatusCode::NOT_FOUND) - } - } else { - Err(StatusCode::BAD_REQUEST) - } -} - -pub async fn handle_get_item_latest( - State(state): State, - Query(params): Query, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/latest from {}", addr); - return Err(StatusCode::UNAUTHORIZED); - } - - let mut conn = state.db.lock().await; - - let item = if let Some(tags_str) = params.tags { - let tags: Vec = tags_str.split(',').map(|t| t.trim().to_string()).collect(); - db::get_item_matching(&mut *conn, &tags, &HashMap::new()) - .map_err(|e| { - warn!("Failed to get item matching tags {:?}: {}", tags, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - } else { - db::get_item_last(&mut *conn).map_err(|e| { - warn!("Failed to get last item: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })? - }; - - if let Some(item) = item { - let item_tags = db::get_item_tags(&mut *conn, &item) - .map_err(|e| { - warn!("Failed to get tags for item {}: {}", item.id.unwrap_or(0), e); - StatusCode::INTERNAL_SERVER_ERROR - })? - .into_iter() - .map(|t| t.name) - .collect(); - let item_meta = db::get_item_meta(&mut *conn, &item) - .map_err(|e| { - warn!("Failed to get metadata for item {}: {}", item.id.unwrap_or(0), e); - StatusCode::INTERNAL_SERVER_ERROR - })? - .into_iter() - .map(|m| (m.name, m.value)) - .collect(); - - let item_info = ItemInfo { - id: item.id.unwrap_or(0), - ts: item.ts.to_rfc3339(), - size: item.size, - compression: item.compression, - tags: item_tags, - metadata: item_meta, - }; - - let response = ApiResponse { - success: true, - data: Some(item_info), - error: None, - }; - - Ok(Json(response)) - } else { - Err(StatusCode::NOT_FOUND) - } -} - -pub async fn handle_get_item( - State(state): State, - Path(item_id): Path, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/{} from {}", item_id, addr); - return Err(StatusCode::UNAUTHORIZED); - } - - if let Ok(id) = item_id.parse::() { - let mut conn = state.db.lock().await; - - if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| { - warn!("Failed to get item {}: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? { - let item_tags = db::get_item_tags(&mut *conn, &item) - .map_err(|e| { - warn!("Failed to get tags for item {}: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - .into_iter() - .map(|t| t.name) - .collect(); - let item_meta = db::get_item_meta(&mut *conn, &item) - .map_err(|e| { - warn!("Failed to get metadata for item {}: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - .into_iter() - .map(|m| (m.name, m.value)) - .collect(); - - let item_info = ItemInfo { - id, - ts: item.ts.to_rfc3339(), - size: item.size, - compression: item.compression, - tags: item_tags, - metadata: item_meta, - }; - - let response = ApiResponse { - success: true, - data: Some(item_info), - error: None, - }; - - Ok(Json(response)) - } else { - Err(StatusCode::NOT_FOUND) - } - } else { - Err(StatusCode::BAD_REQUEST) - } -} - -pub fn get_content_openapi_spec() -> serde_json::Value { - json!({ - "/api/item/latest": { - "get": { - "summary": "Get latest item with metadata and content", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "string"}, - "description": "Comma-separated list of tags to filter by" - } - ], - "responses": { - "200": { - "description": "Item information", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInfo"} - } - } - }, - "404": {"description": "No items found"} - } - } - }, - "/api/item/latest/meta": { - "get": { - "summary": "Get metadata of latest item", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "string"}, - "description": "Comma-separated list of tags to filter by" - } - ], - "responses": { - "200": { - "description": "Item metadata", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": {"type": "string"} - } - } - } - }, - "404": {"description": "No items found"} - } - } - }, - "/api/item/latest/content": { - "get": { - "summary": "Get raw content of latest item", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "string"}, - "description": "Comma-separated list of tags to filter by" - } - ], - "responses": { - "200": {"description": "Raw item content"}, - "404": {"description": "No items found"} - } - } - }, - "/api/item/{id}": { - "get": { - "summary": "Get item by ID with metadata and content", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": { - "description": "Item information", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInfo"} - } - } - }, - "404": {"description": "Item not found"} - } - } - }, - "/api/item/{id}/meta": { - "get": { - "summary": "Get metadata by item ID", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": { - "description": "Item metadata", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": {"type": "string"} - } - } - } - }, - "404": {"description": "Item not found"} - } - } - }, - "/api/item/{id}/content": { - "get": { - "summary": "Get raw content by item ID", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": {"description": "Raw item content"}, - "404": {"description": "Item not found"} - } - } - } - }) -} diff --git a/src/modes/server/items.rs b/src/modes/server/items.rs index ca68616..e69de29 100644 --- a/src/modes/server/items.rs +++ b/src/modes/server/items.rs @@ -1,397 +0,0 @@ -use axum::{ - extract::{ConnectInfo, Path, Query, State}, - http::{HeaderMap, StatusCode}, - response::Json, -}; -use log::warn; -use serde_json::json; -use std::collections::HashMap; -use std::net::SocketAddr; - -use crate::db; -use super::common::{AppState, ApiResponse, ItemInfo, TagsQuery, check_auth}; - -#[derive(Debug, Deserialize)] -pub struct ListItemsQuery { - pub tags: Option, - pub order: Option, - pub start: Option, - pub count: Option, -} - -pub async fn handle_list_items( - State(state): State, - Query(params): Query, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to /api/item/ from {}", addr); - return Err(StatusCode::UNAUTHORIZED); - } - - let mut conn = state.db.lock().await; - - let tags: Vec = params.tags - .as_ref() - .map(|s| s.split(',').map(|t| t.trim().to_string()).collect()) - .unwrap_or_default(); - - let items = if tags.is_empty() { - db::get_items(&mut *conn).map_err(|e| { - warn!("Failed to get items: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })? - } else { - db::get_items_matching(&mut *conn, &tags, &HashMap::new()) - .map_err(|e| { - warn!("Failed to get items matching tags {:?}: {}", tags, e); - StatusCode::INTERNAL_SERVER_ERROR - })? - }; - - // Apply ordering (default is newest first) - let mut items = items; - match params.order.as_deref().unwrap_or("newest") { - "newest" => items.sort_by(|a, b| b.ts.cmp(&a.ts)), - "oldest" => items.sort_by(|a, b| a.ts.cmp(&b.ts)), - _ => items.sort_by(|a, b| b.ts.cmp(&a.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: Vec<_> = items.into_iter().skip(start).take(count).collect(); - - // Get item IDs for batch queries - let item_ids: Vec = items.iter().filter_map(|item| item.id).collect(); - - // Get tags and metadata for all items - let tags_map = db::get_tags_for_items(&mut *conn, &item_ids) - .map_err(|e| { - warn!("Failed to get tags for items: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - let meta_map = db::get_meta_for_items(&mut *conn, &item_ids) - .map_err(|e| { - warn!("Failed to get metadata for items: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - let item_infos: Vec = items - .into_iter() - .map(|item| { - let item_id = item.id.unwrap_or(0); - let item_tags = tags_map.get(&item_id) - .map(|tags| tags.iter().map(|t| t.name.clone()).collect()) - .unwrap_or_default(); - let item_meta = meta_map.get(&item_id) - .cloned() - .unwrap_or_default(); - - ItemInfo { - id: item_id, - ts: item.ts.to_rfc3339(), - size: item.size, - compression: item.compression, - tags: item_tags, - metadata: item_meta, - } - }) - .collect(); - - let response = ApiResponse { - success: true, - data: Some(item_infos), - error: None, - }; - - Ok(Json(response)) -} - -pub async fn handle_post_item( - State(state): State, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to POST /api/item/ from {}", addr); - return Err(StatusCode::UNAUTHORIZED); - } - - // 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)) -} - -pub async fn handle_delete_item( - State(state): State, - Path(item_id): Path, - headers: HeaderMap, - ConnectInfo(addr): ConnectInfo, -) -> Result>, StatusCode> { - if !check_auth(&headers, &state.password) { - warn!("Unauthorized request to DELETE /api/item/{} from {}", item_id, addr); - return Err(StatusCode::UNAUTHORIZED); - } - - if let Ok(id) = item_id.parse::() { - let mut conn = state.db.lock().await; - - if let Some(item) = db::get_item(&mut *conn, id).map_err(|e| { - warn!("Failed to get item {} for deletion: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })? { - db::delete_item(&mut *conn, item).map_err(|e| { - warn!("Failed to delete item {}: {}", id, e); - StatusCode::INTERNAL_SERVER_ERROR - })?; - - let response = ApiResponse::<()> { - success: true, - data: None, - error: None, - }; - Ok(Json(response)) - } else { - Err(StatusCode::NOT_FOUND) - } - } else { - Err(StatusCode::BAD_REQUEST) - } -} - -pub fn get_items_openapi_spec() -> serde_json::Value { - json!({ - "/api/item/": { - "get": { - "summary": "List items", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "string"}, - "description": "Comma-separated list of tags to filter by" - }, - { - "name": "order", - "in": "query", - "schema": {"type": "string", "enum": ["newest", "oldest"]}, - "description": "Order of items (default: newest)" - }, - { - "name": "start", - "in": "query", - "schema": {"type": "integer", "minimum": 0}, - "description": "Start index for pagination (default: 0)" - }, - { - "name": "count", - "in": "query", - "schema": {"type": "integer", "minimum": 1, "maximum": 1000}, - "description": "Number of items to return (default: 100)" - } - ], - "responses": { - "200": { - "description": "List of items", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": {"$ref": "#/components/schemas/ItemInfo"} - } - } - } - } - } - }, - "post": { - "summary": "Add new item", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "array", "items": {"type": "string"}}, - "description": "Tags for the new item (defaults to ['none'] if empty)" - }, - { - "name": "meta", - "in": "query", - "schema": {"type": "array", "items": {"type": "string"}}, - "description": "Metadata for the new item" - } - ], - "responses": { - "201": { - "description": "Item created", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInfo"} - } - } - } - } - } - }, - "/api/item/{id}": { - "get": { - "summary": "Get item by ID", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": { - "description": "Item information", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInfo"} - } - } - }, - "404": {"description": "Item not found"} - } - }, - "delete": { - "summary": "Delete item by ID", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": {"description": "Item deleted"}, - "404": {"description": "Item not found"} - } - } - }, - "/api/item/{id}/meta": { - "get": { - "summary": "Get metadata by item ID", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": { - "description": "Item metadata", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": {"type": "string"} - } - } - } - }, - "404": {"description": "Item not found"} - } - } - }, - "/api/item/{id}/content": { - "get": { - "summary": "Get raw content by item ID", - "parameters": [ - { - "name": "id", - "in": "path", - "required": true, - "schema": {"type": "integer"} - } - ], - "responses": { - "200": {"description": "Raw item content"}, - "404": {"description": "Item not found"} - } - } - }, - "/api/item/latest": { - "get": { - "summary": "Get latest item with metadata and content", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "string"}, - "description": "Comma-separated list of tags to filter by" - } - ], - "responses": { - "200": { - "description": "Item information", - "content": { - "application/json": { - "schema": {"$ref": "#/components/schemas/ItemInfo"} - } - } - }, - "404": {"description": "No items found"} - } - } - }, - "/api/item/latest/meta": { - "get": { - "summary": "Get metadata of latest item", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "string"}, - "description": "Comma-separated list of tags to filter by" - } - ], - "responses": { - "200": { - "description": "Item metadata", - "content": { - "application/json": { - "schema": { - "type": "object", - "additionalProperties": {"type": "string"} - } - } - } - }, - "404": {"description": "No items found"} - } - } - }, - "/api/item/latest/content": { - "get": { - "summary": "Get raw content of latest item", - "parameters": [ - { - "name": "tags", - "in": "query", - "schema": {"type": "string"}, - "description": "Comma-separated list of tags to filter by" - } - ], - "responses": { - "200": {"description": "Raw item content"}, - "404": {"description": "No items found"} - } - } - } - }) -}