feat: serve derivative files via GET /assets/{id}/derivatives/{profile}

- ReadDerivativeHandler queries DerivativeRepository + FileStoragePort
- Profile URL param: thumbnail, thumbnail_large, web_optimized, video_sd
- Immutable cache headers (derivatives don't change once generated)
- Wired into bootstrap catalog service builder
This commit is contained in:
2026-05-31 21:10:58 +02:00
parent f85c0cb246
commit ef64e86439
7 changed files with 127 additions and 9 deletions

View File

@@ -10,8 +10,8 @@ use api_types::{
};
use application::{
catalog::{
GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, RegisterAssetCommand,
UpdateMetadataCommand,
GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, ReadDerivativeQuery,
RegisterAssetCommand, UpdateMetadataCommand,
},
organization::TagAssetCommand,
storage::IngestAssetCommand,
@@ -152,6 +152,40 @@ pub async fn tag_asset(
Ok((StatusCode::CREATED, Json(TagResponse::from_domain(&tag))))
}
pub async fn serve_derivative(
State(state): State<AppState>,
_claims: JwtClaims,
Path((asset_id, profile)): Path<(uuid::Uuid, String)>,
) -> Result<Response, AppError> {
let profile = parse_derivative_profile(&profile)?;
let query = ReadDerivativeQuery {
asset_id: SystemId::from_uuid(asset_id),
profile,
};
let result = state.catalog.read_derivative.execute(query).await?;
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, &result.mime_type)
.header(header::CONTENT_LENGTH, result.data.len())
.header(header::CACHE_CONTROL, "public, max-age=31536000, immutable")
.body(Body::from(result.data))
.map_err(|e| AppError::from(DomainError::Internal(e.to_string())))
}
fn parse_derivative_profile(s: &str) -> Result<domain::entities::DerivativeProfile, AppError> {
use domain::entities::DerivativeProfile;
match s {
"thumbnail" | "thumbnail_square" => Ok(DerivativeProfile::ThumbnailSquare),
"thumbnail_large" => Ok(DerivativeProfile::ThumbnailLarge),
"web" | "web_optimized" => Ok(DerivativeProfile::WebOptimized),
"video_sd" => Ok(DerivativeProfile::VideoSd),
_ => Err(AppError::from(DomainError::Validation(format!(
"Unknown derivative profile: {s}"
)))),
}
}
fn parse_asset_type(s: &str) -> Result<AssetType, AppError> {
match s {
"image" => Ok(AssetType::Image),

View File

@@ -29,6 +29,10 @@ pub fn api_v1_router() -> Router<AppState> {
.route("/assets/{id}", get(assets::get_asset))
.route("/assets/{id}/metadata", put(assets::update_metadata))
.route("/assets/{id}/file", get(assets::serve_file))
.route(
"/assets/{id}/derivatives/{profile}",
get(assets::serve_derivative),
)
.route("/assets/{id}/tags", post(assets::tag_asset))
// sharing
.route("/sharing", post(sharing::share_resource))

View File

@@ -2,8 +2,8 @@ use std::sync::Arc;
use application::{
catalog::{
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, RegisterAssetHandler,
UpdateMetadataHandler,
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, ReadDerivativeHandler,
RegisterAssetHandler, UpdateMetadataHandler,
},
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
organization::{
@@ -41,6 +41,7 @@ pub struct CatalogHandlers {
pub get_timeline: Arc<GetTimelineHandler>,
pub update_metadata: Arc<UpdateMetadataHandler>,
pub read_asset_file: Arc<ReadAssetFileHandler>,
pub read_derivative: Arc<ReadDerivativeHandler>,
pub register_asset: Arc<RegisterAssetHandler>,
}