tuitbot_server/routes/
media.rs1use std::sync::Arc;
4
5use axum::extract::{Multipart, Query, State};
6use axum::http::header;
7use axum::response::{IntoResponse, Response};
8use axum::Json;
9use serde::Deserialize;
10use serde_json::{json, Value};
11use tuitbot_core::storage::media;
12
13use crate::error::ApiError;
14use crate::state::AppState;
15
16pub async fn upload(
21 State(state): State<Arc<AppState>>,
22 mut multipart: Multipart,
23) -> Result<Json<Value>, ApiError> {
24 let field = multipart
25 .next_field()
26 .await
27 .map_err(|e| ApiError::BadRequest(format!("invalid multipart data: {e}")))?
28 .ok_or_else(|| ApiError::BadRequest("no file field in request".to_string()))?;
29
30 let filename = field.file_name().unwrap_or("upload.bin").to_string();
31 let content_type = field.content_type().map(|s| s.to_string());
32
33 let data = field
34 .bytes()
35 .await
36 .map_err(|e| ApiError::BadRequest(format!("failed to read file data: {e}")))?;
37
38 let media_type =
39 media::detect_media_type(&filename, content_type.as_deref()).ok_or_else(|| {
40 ApiError::BadRequest(
41 "unsupported media type; accepted: jpeg, png, webp, gif, mp4".to_string(),
42 )
43 })?;
44
45 if data.len() as u64 > media_type.max_size() {
47 return Err(ApiError::BadRequest(format!(
48 "file size {}B exceeds maximum {}B for {}",
49 data.len(),
50 media_type.max_size(),
51 media_type.mime_type()
52 )));
53 }
54
55 let local = media::store_media(&state.data_dir, &data, &filename, media_type)
56 .await
57 .map_err(|e| ApiError::Internal(format!("failed to store media: {e}")))?;
58
59 Ok(Json(json!({
60 "path": local.path,
61 "media_type": media_type.mime_type(),
62 "size": local.size,
63 })))
64}
65
66#[derive(Deserialize)]
68pub struct MediaFileQuery {
69 pub path: String,
71}
72
73pub async fn serve_file(
75 State(state): State<Arc<AppState>>,
76 Query(params): Query<MediaFileQuery>,
77) -> Result<Response, ApiError> {
78 if !media::is_safe_media_path(¶ms.path, &state.data_dir) {
80 return Err(ApiError::BadRequest("invalid media path".to_string()));
81 }
82
83 let data = media::read_media(¶ms.path)
84 .await
85 .map_err(|e| ApiError::NotFound(format!("media file not found: {e}")))?;
86
87 let content_type = media::detect_media_type(¶ms.path, None)
88 .map(|mt| mt.mime_type())
89 .unwrap_or("application/octet-stream");
90
91 Ok(([(header::CONTENT_TYPE, content_type)], data).into_response())
92}