feat: Add media serving functionality with optional metadata stripping
This commit is contained in:
@@ -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<AppState>,
|
||||
OptionalUserId(user_id): OptionalUserId,
|
||||
Path(media_id): Path<Uuid>,
|
||||
Query(query): Query<ServeFileQuery>,
|
||||
request: Request,
|
||||
) -> Result<impl IntoResponse, ApiError> {
|
||||
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(
|
||||
|
||||
@@ -273,3 +273,9 @@ where
|
||||
has_prev_page: core_response.has_prev_page,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ServeFileQuery {
|
||||
#[serde(default)]
|
||||
pub strip: bool,
|
||||
}
|
||||
|
||||
@@ -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<Uuid>) -> CoreResult<Media> {
|
||||
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))
|
||||
|
||||
Reference in New Issue
Block a user