Files
k-photos/libertas_api/src/handlers/media_handlers.rs

128 lines
3.5 KiB
Rust

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 serde::Serialize;
use std::{io, path::PathBuf};
use tower::ServiceExt;
use tower_http::services::ServeFile;
use uuid::Uuid;
use crate::{error::ApiError, middleware::auth::UserId, state::AppState};
#[derive(Serialize)]
pub struct MediaResponse {
id: uuid::Uuid,
storage_path: String,
original_filename: String,
mime_type: String,
hash: String,
}
impl From<Media> 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() -> Router<AppState> {
let max_size_mb = 100; // todo: get from config
Router::new()
.route("/", post(upload_media))
.route("/{id}", get(get_media_details).delete(delete_media))
.route("/{id}/file", get(get_media_file))
.layer(DefaultBodyLimit::max(max_size_mb * 1024 * 1024))
}
async fn upload_media(
State(state): State<AppState>,
UserId(user_id): UserId,
mut multipart: Multipart,
) -> Result<(StatusCode, Json<MediaResponse>), 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<Item = Result<bytes::Bytes, std::io::Error>> + 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<AppState>,
UserId(user_id): UserId,
Path(media_id): Path<Uuid>,
request: Request,
) -> Result<impl IntoResponse, ApiError> {
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<AppState>,
UserId(user_id): UserId,
Path(id): Path<Uuid>,
) -> Result<Json<MediaResponse>, ApiError> {
let media = state.media_service.get_media_details(id, user_id).await?;
Ok(Json(media.into()))
}
async fn delete_media(
State(state): State<AppState>,
UserId(user_id): UserId,
Path(id): Path<Uuid>,
) -> Result<StatusCode, ApiError> {
state.media_service.delete_media(id, user_id).await?;
Ok(StatusCode::NO_CONTENT)
}