diff --git a/Cargo.lock b/Cargo.lock index 3044055..09751c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1755,9 +1755,11 @@ dependencies = [ "config", "dotenvy", "futures", + "image", "nom-exif", "serde", "thiserror 2.0.17", + "tokio", "uuid", ] diff --git a/libertas_api/src/handlers/media_handlers.rs b/libertas_api/src/handlers/media_handlers.rs index 1650f2e..879a43f 100644 --- a/libertas_api/src/handlers/media_handlers.rs +++ b/libertas_api/src/handlers/media_handlers.rs @@ -1,13 +1,19 @@ use axum::{ Router, - extract::{DefaultBodyLimit, Multipart, Path, Request, State}, - http::StatusCode, + extract::{DefaultBodyLimit, Multipart, Path, Query, Request, State}, + http::{ + StatusCode, + header::{CONTENT_DISPOSITION, CONTENT_TYPE}, + }, response::{IntoResponse, Json}, routing::{get, post}, }; use futures::TryStreamExt; -use libertas_core::{error::CoreError, schema::UploadMediaData}; +use libertas_core::{ + error::CoreError, media_utils::strip_metadata_from_bytes, schema::UploadMediaData, +}; use std::{io, path::PathBuf}; +use tokio::fs; use tower::ServiceExt; use tower_http::services::ServeFile; @@ -17,7 +23,10 @@ use crate::{ error::ApiError, extractors::query_options::ApiListMediaOptions, middleware::auth::{OptionalUserId, UserId}, - schema::{MediaDetailsResponse, MediaResponse, PaginatedResponse, map_paginated_response}, + schema::{ + MediaDetailsResponse, MediaResponse, PaginatedResponse, ServeFileQuery, + map_paginated_response, + }, state::AppState, }; @@ -71,24 +80,49 @@ async fn get_media_file( State(state): State, OptionalUserId(user_id): OptionalUserId, Path(media_id): Path, + Query(query): Query, request: Request, ) -> Result { - let storage_path = state + let media = state .media_service - .get_media_filepath(media_id, user_id) + .get_media_for_serving(media_id, user_id) .await?; - let full_path = PathBuf::from(&state.config.media_library_path).join(&storage_path); + let full_path = PathBuf::from(&state.config.media_library_path).join(&media.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), - ))) - }) + match query.strip { + true => { + let file_bytes = fs::read(&full_path).await.map_err(|e| { + ApiError::from(CoreError::Io(io::Error::new( + io::ErrorKind::NotFound, + format!("File not found at {}: {}", full_path.display(), e), + ))) + })?; + + let body = strip_metadata_from_bytes(file_bytes, &media.mime_type).await?; + let disposition = format!("inline; filename=\"{}\"", media.original_filename); + let response = ( + StatusCode::OK, + [ + (CONTENT_TYPE, media.mime_type), + (CONTENT_DISPOSITION, disposition), + ], + body, + ) + .into_response(); + Ok(response) + } + false => 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), + ))) + }) + .map(|res| res.into_response()), + } } async fn get_media_thumbnail( diff --git a/libertas_api/src/schema.rs b/libertas_api/src/schema.rs index 08af345..e28d9d8 100644 --- a/libertas_api/src/schema.rs +++ b/libertas_api/src/schema.rs @@ -273,3 +273,9 @@ where has_prev_page: core_response.has_prev_page, } } + +#[derive(Deserialize)] +pub struct ServeFileQuery { + #[serde(default)] + pub strip: bool, +} diff --git a/libertas_api/src/services/media_service.rs b/libertas_api/src/services/media_service.rs index 201ffff..135086a 100644 --- a/libertas_api/src/services/media_service.rs +++ b/libertas_api/src/services/media_service.rs @@ -153,6 +153,20 @@ impl MediaService for MediaServiceImpl { .ok_or(CoreError::NotFound("Thumbnail for Media".to_string(), id)) } + async fn get_media_for_serving(&self, id: Uuid, user_id: Option) -> CoreResult { + self.auth_service + .check_permission(user_id, authz::Permission::ViewMedia(id)) + .await?; + + let media = self + .repo + .find_by_id(id) + .await? + .ok_or(CoreError::NotFound("Media".to_string(), id))?; + + Ok(media) + } + async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> { self.auth_service .check_permission(Some(user_id), authz::Permission::DeleteMedia(id)) diff --git a/libertas_core/Cargo.toml b/libertas_core/Cargo.toml index b2a29a7..1e1682f 100644 --- a/libertas_core/Cargo.toml +++ b/libertas_core/Cargo.toml @@ -15,3 +15,5 @@ serde = { version = "1.0.228", features = ["derive"] } nom-exif = { version = "2.5.4", features = ["serde", "async", "tokio"] } dotenvy = "0.15.7" config = "0.15.19" +image = "0.25.8" +tokio = { version = "1.48.0", features = ["rt"] } \ No newline at end of file diff --git a/libertas_core/src/media_utils.rs b/libertas_core/src/media_utils.rs index a7f4c0e..ad273da 100644 --- a/libertas_core/src/media_utils.rs +++ b/libertas_core/src/media_utils.rs @@ -4,7 +4,9 @@ use std::{ }; use chrono::{DateTime, Datelike, NaiveDateTime, Utc}; +use image::{ImageFormat, load_from_memory}; use nom_exif::{AsyncMediaParser, AsyncMediaSource, ExifIter, MediaParser, MediaSource, TrackInfo}; +use tokio::task; use crate::{ error::{CoreError, CoreResult}, @@ -190,3 +192,34 @@ pub fn is_invalid_exif_tag(tag_name: &str, tag_value: &str) -> bool { false } + +pub async fn strip_metadata_from_bytes( + file_bytes: Vec, + mime_type: &str, +) -> CoreResult> { + if mime_type != "image/jpeg" && mime_type != "image/png" { + return Ok(file_bytes); + } + + let mime_type = mime_type.to_string(); + + task::spawn_blocking(move || -> CoreResult> { + let img = load_from_memory(&file_bytes) + .map_err(|e| CoreError::Unknown(format!("Failed to parse image: {}", e)))?; + + let mut buffer = std::io::Cursor::new(Vec::new()); + + match mime_type.as_str() { + "image/jpeg" => img + .write_to(&mut buffer, ImageFormat::Jpeg) + .map_err(|e| CoreError::Unknown(format!("Failed to re-encode JPEG: {}", e)))?, + "image/png" => img + .write_to(&mut buffer, ImageFormat::Png) + .map_err(|e| CoreError::Unknown(format!("Failed to re-encode PNG: {}", e)))?, + _ => unreachable!(), // We already checked this + } + Ok(buffer.into_inner()) + }) + .await + .map_err(|e| CoreError::Unknown(format!("Blocking task failed: {}", e)))? +} diff --git a/libertas_core/src/services.rs b/libertas_core/src/services.rs index 5595528..91dd3fd 100644 --- a/libertas_core/src/services.rs +++ b/libertas_core/src/services.rs @@ -26,6 +26,7 @@ pub trait MediaService: Send + Sync { async fn get_media_filepath(&self, id: Uuid, user_id: Option) -> CoreResult; async fn get_media_thumbnail_path(&self, id: Uuid, user_id: Option) -> CoreResult; + async fn get_media_for_serving(&self, id: Uuid, user_id: Option) -> CoreResult; async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>; }