feat: Add media thumbnail retrieval functionality and update MediaResponse structure
This commit is contained in:
@@ -17,7 +17,7 @@ use crate::{
|
|||||||
error::ApiError,
|
error::ApiError,
|
||||||
extractors::query_options::ApiListMediaOptions,
|
extractors::query_options::ApiListMediaOptions,
|
||||||
middleware::auth::{OptionalUserId, UserId},
|
middleware::auth::{OptionalUserId, UserId},
|
||||||
schema::{MediaDetailsResponse, MediaMetadataResponse, MediaResponse},
|
schema::{MediaDetailsResponse, MediaResponse},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,6 +26,7 @@ pub fn media_routes(max_upload_size: usize) -> Router<AppState> {
|
|||||||
.route("/", post(upload_media).get(list_user_media))
|
.route("/", post(upload_media).get(list_user_media))
|
||||||
.route("/{id}", get(get_media_details).delete(delete_media))
|
.route("/{id}", get(get_media_details).delete(delete_media))
|
||||||
.route("/{id}/file", get(get_media_file))
|
.route("/{id}/file", get(get_media_file))
|
||||||
|
.route("/{id}/thumbnail", get(get_media_thumbnail))
|
||||||
.layer(DefaultBodyLimit::max(max_upload_size))
|
.layer(DefaultBodyLimit::max(max_upload_size))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,26 +91,37 @@ async fn get_media_file(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_media_thumbnail(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
OptionalUserId(user_id): OptionalUserId,
|
||||||
|
Path(media_id): Path<Uuid>,
|
||||||
|
request: Request,
|
||||||
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
let thumbnail_path = state
|
||||||
|
.media_service
|
||||||
|
.get_media_thumbnail_path(media_id, user_id)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let full_path = PathBuf::from(&thumbnail_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(
|
async fn get_media_details(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
OptionalUserId(user_id): OptionalUserId,
|
OptionalUserId(user_id): OptionalUserId,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
) -> Result<Json<MediaDetailsResponse>, ApiError> {
|
) -> Result<Json<MediaDetailsResponse>, ApiError> {
|
||||||
let bundle = state.media_service.get_media_details(id, user_id).await?;
|
let bundle = state.media_service.get_media_details(id, user_id).await?;
|
||||||
let response = MediaDetailsResponse {
|
let response = MediaDetailsResponse::from(bundle);
|
||||||
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,
|
|
||||||
thumbnail_path: bundle.media.thumbnail_path,
|
|
||||||
metadata: bundle
|
|
||||||
.metadata
|
|
||||||
.into_iter()
|
|
||||||
.map(MediaMetadataResponse::from)
|
|
||||||
.collect(),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Json(response))
|
Ok(Json(response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use libertas_core::models::{
|
use libertas_core::models::{
|
||||||
Album, AlbumPermission, FaceRegion, Media, MediaMetadata, Person, PersonPermission, Tag,
|
Album, AlbumPermission, FaceRegion, Media, MediaBundle, MediaMetadata, Person,
|
||||||
|
PersonPermission, Tag,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
@@ -7,22 +8,24 @@ use uuid::Uuid;
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct MediaResponse {
|
pub struct MediaResponse {
|
||||||
pub id: uuid::Uuid,
|
pub id: uuid::Uuid,
|
||||||
pub storage_path: String,
|
|
||||||
pub original_filename: String,
|
pub original_filename: String,
|
||||||
pub mime_type: String,
|
pub mime_type: String,
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
pub thumbnail_path: Option<String>,
|
pub file_url: String,
|
||||||
|
pub thumbnail_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Media> for MediaResponse {
|
impl From<Media> for MediaResponse {
|
||||||
fn from(media: Media) -> Self {
|
fn from(media: Media) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: media.id,
|
id: media.id,
|
||||||
storage_path: media.storage_path,
|
|
||||||
original_filename: media.original_filename,
|
original_filename: media.original_filename,
|
||||||
mime_type: media.mime_type,
|
mime_type: media.mime_type,
|
||||||
hash: media.hash,
|
hash: media.hash,
|
||||||
thumbnail_path: media.thumbnail_path,
|
file_url: format!("/api/v1/media/{}/file", media.id),
|
||||||
|
thumbnail_url: media
|
||||||
|
.thumbnail_path
|
||||||
|
.map(|_| format!("/api/v1/media/{}/thumbnail", media.id)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,15 +133,24 @@ impl From<MediaMetadata> for MediaMetadataResponse {
|
|||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct MediaDetailsResponse {
|
pub struct MediaDetailsResponse {
|
||||||
pub id: uuid::Uuid,
|
#[serde(flatten)]
|
||||||
pub storage_path: String,
|
pub media: MediaResponse,
|
||||||
pub original_filename: String,
|
|
||||||
pub mime_type: String,
|
|
||||||
pub hash: String,
|
|
||||||
pub thumbnail_path: Option<String>,
|
|
||||||
pub metadata: Vec<MediaMetadataResponse>,
|
pub metadata: Vec<MediaMetadataResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<MediaBundle> for MediaDetailsResponse {
|
||||||
|
fn from(bundle: MediaBundle) -> Self {
|
||||||
|
Self {
|
||||||
|
media: MediaResponse::from(bundle.media),
|
||||||
|
metadata: bundle
|
||||||
|
.metadata
|
||||||
|
.into_iter()
|
||||||
|
.map(MediaMetadataResponse::from)
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct TagResponse {
|
pub struct TagResponse {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
|
|||||||
@@ -127,6 +127,26 @@ impl MediaService for MediaServiceImpl {
|
|||||||
Ok(media.storage_path)
|
Ok(media.storage_path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_media_thumbnail_path(
|
||||||
|
&self,
|
||||||
|
id: Uuid,
|
||||||
|
user_id: Option<Uuid>,
|
||||||
|
) -> CoreResult<String> {
|
||||||
|
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))?;
|
||||||
|
|
||||||
|
media
|
||||||
|
.thumbnail_path
|
||||||
|
.ok_or(CoreError::NotFound("Thumbnail for Media".to_string(), id))
|
||||||
|
}
|
||||||
|
|
||||||
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()> {
|
||||||
self.auth_service
|
self.auth_service
|
||||||
.check_permission(Some(user_id), authz::Permission::DeleteMedia(id))
|
.check_permission(Some(user_id), authz::Permission::DeleteMedia(id))
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub trait MediaService: Send + Sync {
|
|||||||
options: ListMediaOptions,
|
options: ListMediaOptions,
|
||||||
) -> CoreResult<Vec<Media>>;
|
) -> CoreResult<Vec<Media>>;
|
||||||
async fn get_media_filepath(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<String>;
|
async fn get_media_filepath(&self, id: Uuid, user_id: Option<Uuid>) -> CoreResult<String>;
|
||||||
|
async fn get_media_thumbnail_path(&self, id: Uuid, user_id: Option<Uuid>)
|
||||||
|
-> CoreResult<String>;
|
||||||
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
async fn delete_media(&self, id: Uuid, user_id: Uuid) -> CoreResult<()>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user