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

@@ -1,3 +1,4 @@
pub mod get_asset;
pub mod get_timeline;
pub mod read_asset_file;
pub mod read_derivative;

View File

@@ -0,0 +1,68 @@
use bytes::Bytes;
use domain::{
entities::{DerivativeProfile, GenerationStatus},
errors::DomainError,
ports::{DerivativeRepository, FileStoragePort},
value_objects::SystemId,
};
use std::sync::Arc;
pub struct ReadDerivativeQuery {
pub asset_id: SystemId,
pub profile: DerivativeProfile,
}
pub struct DerivativeFileResult {
pub data: Bytes,
pub mime_type: String,
}
pub struct ReadDerivativeHandler {
derivative_repo: Arc<dyn DerivativeRepository>,
file_storage: Arc<dyn FileStoragePort>,
}
impl ReadDerivativeHandler {
pub fn new(
derivative_repo: Arc<dyn DerivativeRepository>,
file_storage: Arc<dyn FileStoragePort>,
) -> Self {
Self {
derivative_repo,
file_storage,
}
}
pub async fn execute(
&self,
query: ReadDerivativeQuery,
) -> Result<DerivativeFileResult, DomainError> {
let derivative = self
.derivative_repo
.find_by_asset_and_profile(&query.asset_id, query.profile)
.await?
.ok_or_else(|| {
DomainError::NotFound(format!(
"Derivative {:?} not found for asset {}",
query.profile, query.asset_id
))
})?;
if derivative.generation_status != GenerationStatus::Ready {
return Err(DomainError::NotFound(format!(
"Derivative {:?} not ready for asset {}",
query.profile, query.asset_id
)));
}
let data = self
.file_storage
.read_file(&derivative.storage_path)
.await?;
Ok(DerivativeFileResult {
data,
mime_type: derivative.mime_type,
})
}
}