use axum::{ Router, extract::{DefaultBodyLimit, Multipart, Path, Request, State}, http::StatusCode, response::{IntoResponse, Json}, routing::{get, post}, }; use futures::TryStreamExt; use libertas_core::{error::CoreError, models::Media, schema::UploadMediaData}; use std::{io, path::PathBuf}; use tower::ServiceExt; use tower_http::services::ServeFile; use uuid::Uuid; use crate::{error::ApiError, extractors::query_options::ApiListMediaOptions, middleware::auth::UserId, schema::{MediaDetailsResponse, MediaMetadataResponse, MediaResponse}, state::AppState}; impl From for MediaResponse { fn from(media: Media) -> Self { Self { id: media.id, storage_path: media.storage_path, original_filename: media.original_filename, mime_type: media.mime_type, hash: media.hash, } } } pub fn media_routes(max_upload_size: usize) -> Router { Router::new() .route("/", post(upload_media).get(list_user_media)) .route("/{id}", get(get_media_details).delete(delete_media)) .route("/{id}/file", get(get_media_file)) .layer(DefaultBodyLimit::max(max_upload_size)) } async fn upload_media( State(state): State, UserId(user_id): UserId, mut multipart: Multipart, ) -> Result<(StatusCode, Json), ApiError> { let field = multipart .next_field() .await .map_err(|e| CoreError::Validation(format!("Multipart error: {}", e)))? .ok_or(ApiError::from(CoreError::Validation( "No file provided in 'file' field".to_string(), )))?; let filename = field.file_name().unwrap_or("unknown_file").to_string(); let mime_type = field .content_type() .unwrap_or("application/octet-stream") .to_string(); let stream = field.map_err(|e| io::Error::new(io::ErrorKind::Other, e)); let boxed_stream: Box< dyn futures::Stream> + Send + Unpin, > = Box::new(stream); let upload_data = UploadMediaData { owner_id: user_id, filename, mime_type, stream: boxed_stream, }; let media = state.media_service.upload_media(upload_data).await?; Ok((StatusCode::CREATED, Json(media.into()))) } async fn get_media_file( State(state): State, UserId(user_id): UserId, Path(media_id): Path, request: Request, ) -> Result { let storage_path = state .media_service .get_media_filepath(media_id, user_id) .await?; let full_path = PathBuf::from(&state.config.media_library_path).join(&storage_path); ServeFile::new(full_path) .oneshot(request) .await .map_err(|e| { ApiError::from(CoreError::Io(io::Error::new( io::ErrorKind::NotFound, format!("File not found: {}", e), ))) }) } async fn get_media_details( State(state): State, UserId(user_id): UserId, Path(id): Path, ) -> Result, ApiError> { let bundle = state.media_service.get_media_details(id, user_id).await?; let response = MediaDetailsResponse { id: bundle.media.id, storage_path: bundle.media.storage_path, original_filename: bundle.media.original_filename, mime_type: bundle.media.mime_type, hash: bundle.media.hash, metadata: bundle.metadata .into_iter() .map(MediaMetadataResponse::from) .collect(), }; Ok(Json(response)) } async fn delete_media( State(state): State, UserId(user_id): UserId, Path(id): Path, ) -> Result { state.media_service.delete_media(id, user_id).await?; Ok(StatusCode::NO_CONTENT) } async fn list_user_media( State(state): State, UserId(user_id): UserId, ApiListMediaOptions(options): ApiListMediaOptions, ) -> Result>, ApiError> { let media_list = state .media_service .list_user_media(user_id, options) .await?; let response = media_list.into_iter().map(MediaResponse::from).collect(); Ok(Json(response)) }