use axum::{ extract::{Path, State}, http::StatusCode, response::Json, }; use std::sync::Arc; use tracing::{debug, error, info}; use crate::{ auth::AuthUser, services::file_service::FileService, AppState, }; use super::types::DocumentDebugInfo; /// Get comprehensive debug information for a document #[utoipa::path( get, path = "/api/documents/{id}/debug", tag = "documents", security( ("bearer_auth" = []) ), params( ("id" = uuid::Uuid, Path, description = "Document ID") ), responses( (status = 200, description = "Document debug information", body = DocumentDebugInfo), (status = 404, description = "Document not found"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] pub async fn get_document_debug_info( State(state): State>, auth_user: AuthUser, Path(document_id): Path, ) -> Result, StatusCode> { let document = state .db .get_document_by_id(document_id, auth_user.user.id, auth_user.user.role) .await .map_err(|e| { error!("Database error getting document {}: {}", document_id, e); StatusCode::INTERNAL_SERVER_ERROR })? .ok_or(StatusCode::NOT_FOUND)?; let file_service = &state.file_service; // Check file existence and readability let file_exists = tokio::fs::metadata(&document.file_path).await.is_ok(); let readable = if file_exists { file_service.read_file(&document.file_path).await.is_ok() } else { false }; // Get file permissions (simplified) let permissions = if file_exists { Some("readable".to_string()) // This could be expanded with actual file permissions } else { None }; // Get user settings let user_settings = state .db .get_user_settings(auth_user.user.id) .await .map_err(|e| { error!("Database error getting user settings: {}", e); StatusCode::INTERNAL_SERVER_ERROR })? .map(|settings| settings.into()); // Construct processing steps based on document state let mut processing_steps = vec!["uploaded".to_string()]; if document.content.is_some() { processing_steps.push("content_extracted".to_string()); } match document.ocr_status.as_deref() { Some("pending") => processing_steps.push("ocr_queued".to_string()), Some("processing") => processing_steps.push("ocr_in_progress".to_string()), Some("completed") => processing_steps.push("ocr_completed".to_string()), Some("failed") => processing_steps.push("ocr_failed".to_string()), _ => {} } if document.ocr_text.is_some() { processing_steps.push("ocr_text_available".to_string()); } let debug_info = DocumentDebugInfo { document_id: document.id, filename: document.original_filename, file_path: document.file_path, file_size: document.file_size, mime_type: document.mime_type, created_at: document.created_at, ocr_status: document.ocr_status, ocr_confidence: document.ocr_confidence, ocr_word_count: document.ocr_word_count, processing_steps, file_exists, readable, permissions, user_settings, }; debug!("Debug info generated for document: {}", document_id); Ok(Json(debug_info)) } /// Get thumbnail for a document (if available) #[utoipa::path( get, path = "/api/documents/{id}/thumbnail", tag = "documents", security( ("bearer_auth" = []) ), params( ("id" = uuid::Uuid, Path, description = "Document ID") ), responses( (status = 200, description = "Document thumbnail", content_type = "image/jpeg"), (status = 404, description = "Document or thumbnail not found"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] pub async fn get_document_thumbnail( State(state): State>, auth_user: AuthUser, Path(document_id): Path, ) -> Result { let document = state .db .get_document_by_id(document_id, auth_user.user.id, auth_user.user.role) .await .map_err(|e| { error!("Database error getting document {}: {}", document_id, e); StatusCode::INTERNAL_SERVER_ERROR })? .ok_or(StatusCode::NOT_FOUND)?; let file_service = &state.file_service; // Use the FileService to get or generate thumbnail #[cfg(feature = "ocr")] match file_service.get_or_generate_thumbnail(&document.file_path, &document.original_filename).await { Ok(data) => { let response = axum::response::Response::builder() .status(StatusCode::OK) .header("Content-Type", "image/jpeg") .header("Content-Length", data.len().to_string()) .header("Cache-Control", "public, max-age=3600") // Cache for 1 hour .body(axum::body::Body::from(data)) .map_err(|e| { error!("Failed to build thumbnail response: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; debug!("Thumbnail served for document: {}", document_id); Ok(response) } Err(e) => { error!("Failed to get or generate thumbnail for document {}: {}", document_id, e); Err(StatusCode::NOT_FOUND) } } #[cfg(not(feature = "ocr"))] { error!("Thumbnail generation requires OCR feature to be enabled"); Err(StatusCode::NOT_FOUND) } } /// Get processed image for a document (if available) #[utoipa::path( get, path = "/api/documents/{id}/processed-image", tag = "documents", security( ("bearer_auth" = []) ), params( ("id" = uuid::Uuid, Path, description = "Document ID") ), responses( (status = 200, description = "Processed image", content_type = "image/png"), (status = 404, description = "Document or processed image not found"), (status = 401, description = "Unauthorized"), (status = 500, description = "Internal server error") ) )] pub async fn get_processed_image( State(state): State>, auth_user: AuthUser, Path(document_id): Path, ) -> Result { let document = state .db .get_document_by_id(document_id, auth_user.user.id, auth_user.user.role) .await .map_err(|e| { error!("Database error getting document {}: {}", document_id, e); StatusCode::INTERNAL_SERVER_ERROR })? .ok_or(StatusCode::NOT_FOUND)?; // Check if this is an image document if !document.mime_type.starts_with("image/") { return Err(StatusCode::BAD_REQUEST); } let file_service = &state.file_service; // Try to read processed image from the processed directory let processed_path = format!("{}/processed/{}.png", state.config.upload_path, document.id); match file_service.read_file(&processed_path).await { Ok(image_data) => { let response = axum::response::Response::builder() .status(StatusCode::OK) .header("Content-Type", "image/png") .header("Content-Length", image_data.len().to_string()) .header("Cache-Control", "public, max-age=3600") // Cache for 1 hour .body(axum::body::Body::from(image_data)) .map_err(|e| { error!("Failed to build processed image response: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; debug!("Processed image served for document: {}", document_id); Ok(response) } Err(_) => { Err(StatusCode::NOT_FOUND) } } } /// Get system-wide document statistics pub async fn get_document_statistics( State(state): State>, auth_user: AuthUser, ) -> Result, StatusCode> { // Get OCR statistics let (total, pending, completed, failed) = state .db .count_documents_by_ocr_status(auth_user.user.id, auth_user.user.role) .await .map_err(|e| { error!("Database error getting OCR stats: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; // Get MIME type distribution let mime_type_facets = state .db .get_mime_type_facets(auth_user.user.id, auth_user.user.role) .await .map_err(|e| { error!("Database error getting MIME type facets: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; // Get recent upload activity (simplified) let recent_documents = state .db .get_documents_by_user_with_role(auth_user.user.id, auth_user.user.role, 10, 0) .await .map_err(|e| { error!("Database error getting recent documents: {}", e); StatusCode::INTERNAL_SERVER_ERROR })?; let total_file_size: i64 = recent_documents.iter().map(|d| d.file_size).sum(); Ok(Json(serde_json::json!({ "document_counts": { "total": total, "pending_ocr": pending, "completed_ocr": completed, "failed_ocr": failed }, "mime_types": mime_type_facets, "storage": { "recent_documents_size": total_file_size, "recent_documents_count": recent_documents.len() }, "activity": { "recent_uploads": recent_documents.len(), "last_upload": recent_documents.first().map(|d| d.created_at) } }))) } /// Validate document integrity pub async fn validate_document_integrity( State(state): State>, auth_user: AuthUser, Path(document_id): Path, ) -> Result, StatusCode> { let document = state .db .get_document_by_id(document_id, auth_user.user.id, auth_user.user.role) .await .map_err(|e| { error!("Database error getting document {}: {}", document_id, e); StatusCode::INTERNAL_SERVER_ERROR })? .ok_or(StatusCode::NOT_FOUND)?; let file_service = &state.file_service; let mut issues = Vec::new(); let mut checks = Vec::new(); // Check file existence checks.push("file_existence".to_string()); if tokio::fs::metadata(&document.file_path).await.is_err() { issues.push("File does not exist on disk".to_string()); } // Check file readability checks.push("file_readability".to_string()); match file_service.read_file(&document.file_path).await { Ok(data) => { // Verify file size matches if data.len() as i64 != document.file_size { issues.push(format!( "File size mismatch: database={}, actual={}", document.file_size, data.len() )); } } Err(e) => { issues.push(format!("Cannot read file: {}", e)); } } // Check OCR consistency checks.push("ocr_consistency".to_string()); if document.ocr_text.is_some() && document.ocr_status.as_deref() != Some("completed") { issues.push("OCR text exists but status is not 'completed'".to_string()); } if document.ocr_text.is_none() && document.ocr_status.as_deref() == Some("completed") { issues.push("OCR status is 'completed' but no OCR text available".to_string()); } // Check confidence consistency checks.push("confidence_consistency".to_string()); if let Some(confidence) = document.ocr_confidence { if confidence < 0.0 || confidence > 100.0 { issues.push(format!("Invalid OCR confidence value: {}", confidence)); } } let is_valid = issues.is_empty(); info!("Document {} integrity check: {} issues found", document_id, issues.len()); Ok(Json(serde_json::json!({ "document_id": document_id, "is_valid": is_valid, "checks_performed": checks, "issues": issues, "summary": if is_valid { "Document integrity is good".to_string() } else { format!("Found {} integrity issues", issues.len()) } }))) }