ograf_core/handlers/
graphics.rs1use axum::{
2 extract::{Path, Query, State},
3 http::{HeaderMap, StatusCode},
4 response::{IntoResponse, Response},
5 Json,
6};
7use serde::Deserialize;
8use serde_json::{json, Value};
9
10use crate::{
11 error::Result,
12 handlers::api_key_from,
13 models::Graphic,
14 store::graphics::{is_valid_graphic_id, safe_join, GraphicStore},
15 AppState,
16};
17
18pub async fn list_graphics(
24 State(state): State<AppState>,
25 headers: HeaderMap,
26) -> Result<Json<Value>> {
27 let api_key = api_key_from(&headers);
28 let all_graphics = GraphicStore::new(&state.config.graphics_storage)
29 .list()
30 .await?;
31 let visible = state.access.filter_graphics(&api_key, all_graphics).await;
32 let list: Vec<Value> = visible.iter().map(Graphic::list_info).collect();
33 Ok(Json(json!({ "graphics": list })))
34}
35
36pub async fn get_graphic(
37 State(state): State<AppState>,
38 Path(graphic_id): Path<String>,
39) -> Result<Json<Value>> {
40 let graphic = GraphicStore::new(&state.config.graphics_storage)
41 .get(&graphic_id)
42 .await?;
43 Ok(Json(json!({
44 "graphic": graphic.manifest,
45 "metadata": {
46 "createdAt": graphic.uploaded_at,
47 }
48 })))
49}
50
51#[derive(Deserialize)]
52pub struct ThumbnailQuery {
53 pub file: String,
54}
55
56pub async fn get_thumbnail(
57 State(state): State<AppState>,
58 Path(graphic_id): Path<String>,
59 Query(query): Query<ThumbnailQuery>,
60) -> Response {
61 if !is_valid_graphic_id(&graphic_id) {
62 return StatusCode::NOT_FOUND.into_response();
63 }
64
65 let mime = mime_guess::from_path(&query.file).first_or_octet_stream();
66 let is_supported = matches!(
67 mime.essence_str(),
68 "image/png" | "image/jpeg" | "image/gif" | "image/webp"
69 );
70 if !is_supported {
71 return (
72 StatusCode::BAD_REQUEST,
73 Json(json!({ "error": "unsupported thumbnail file type" })),
74 )
75 .into_response();
76 }
77
78 let storage_path = GraphicStore::new(&state.config.graphics_storage).path_for(&graphic_id);
79 let Some(file_path) = safe_join(&storage_path, &query.file) else {
80 return (
81 StatusCode::BAD_REQUEST,
82 Json(json!({ "error": "invalid thumbnail file reference" })),
83 )
84 .into_response();
85 };
86
87 match tokio::fs::read(&file_path).await {
88 Ok(bytes) => (
89 StatusCode::OK,
90 [(axum::http::header::CONTENT_TYPE, mime.to_string())],
91 bytes,
92 )
93 .into_response(),
94 Err(_) => StatusCode::NOT_FOUND.into_response(),
95 }
96}
97
98pub async fn serve_graphic_asset(
99 State(state): State<AppState>,
100 Path((graphic_id, asset_path)): Path<(String, String)>,
101) -> Response {
102 let storage_path = match GraphicStore::new(&state.config.graphics_storage)
103 .get(&graphic_id)
104 .await
105 {
106 Ok(graphic) => graphic.storage_path,
107 Err(_) => return StatusCode::NOT_FOUND.into_response(),
108 };
109 let Some(file_path) = safe_join(std::path::Path::new(&storage_path), &asset_path) else {
110 return StatusCode::NOT_FOUND.into_response();
111 };
112
113 match tokio::fs::read(&file_path).await {
114 Ok(bytes) => {
115 let mime = mime_guess::from_path(&asset_path)
116 .first_or_octet_stream()
117 .to_string();
118 (
119 StatusCode::OK,
120 [(axum::http::header::CONTENT_TYPE, mime)],
121 bytes,
122 )
123 .into_response()
124 }
125 Err(_) => StatusCode::NOT_FOUND.into_response(),
126 }
127}