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:
@@ -7,4 +7,7 @@ pub use commands::update_metadata::{UpdateMetadataCommand, UpdateMetadataHandler
|
|||||||
pub use queries::get_asset::{GetAssetHandler, GetAssetQuery};
|
pub use queries::get_asset::{GetAssetHandler, GetAssetQuery};
|
||||||
pub use queries::get_timeline::{GetTimelineHandler, GetTimelineQuery};
|
pub use queries::get_timeline::{GetTimelineHandler, GetTimelineQuery};
|
||||||
pub use queries::read_asset_file::{AssetFileResult, ReadAssetFileHandler, ReadAssetFileQuery};
|
pub use queries::read_asset_file::{AssetFileResult, ReadAssetFileHandler, ReadAssetFileQuery};
|
||||||
|
pub use queries::read_derivative::{
|
||||||
|
DerivativeFileResult, ReadDerivativeHandler, ReadDerivativeQuery,
|
||||||
|
};
|
||||||
pub use visibility::VisibilityFilteredAssetRepository;
|
pub use visibility::VisibilityFilteredAssetRepository;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
pub mod get_asset;
|
pub mod get_asset;
|
||||||
pub mod get_timeline;
|
pub mod get_timeline;
|
||||||
pub mod read_asset_file;
|
pub mod read_asset_file;
|
||||||
|
pub mod read_derivative;
|
||||||
|
|||||||
68
crates/application/src/catalog/queries/read_derivative.rs
Normal file
68
crates/application/src/catalog/queries/read_derivative.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use adapters_postgres::{
|
use adapters_postgres::{
|
||||||
PgPool, PostgresAssetMetadataRepository, PostgresAssetRepository, PostgresDuplicateRepository,
|
PgPool, PostgresAssetMetadataRepository, PostgresAssetRepository, PostgresDerivativeRepository,
|
||||||
PostgresIngestTransaction,
|
PostgresDuplicateRepository, PostgresIngestTransaction,
|
||||||
};
|
};
|
||||||
use adapters_storage::LocalFileStorage;
|
use adapters_storage::LocalFileStorage;
|
||||||
use application::catalog::{
|
use application::catalog::{
|
||||||
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, RegisterAssetHandler,
|
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, ReadDerivativeHandler,
|
||||||
UpdateMetadataHandler,
|
RegisterAssetHandler, UpdateMetadataHandler,
|
||||||
};
|
};
|
||||||
use application::storage::IngestAssetHandler;
|
use application::storage::IngestAssetHandler;
|
||||||
use domain::ports::EventPublisher;
|
use domain::ports::EventPublisher;
|
||||||
@@ -23,6 +23,7 @@ pub fn build(
|
|||||||
) -> CatalogHandlers {
|
) -> CatalogHandlers {
|
||||||
let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone()));
|
let asset_repo = Arc::new(PostgresAssetRepository::new(pool.clone()));
|
||||||
let metadata_repo = Arc::new(PostgresAssetMetadataRepository::new(pool.clone()));
|
let metadata_repo = Arc::new(PostgresAssetMetadataRepository::new(pool.clone()));
|
||||||
|
let derivative_repo = Arc::new(PostgresDerivativeRepository::new(pool.clone()));
|
||||||
let duplicate_repo = Arc::new(PostgresDuplicateRepository::new(pool.clone()));
|
let duplicate_repo = Arc::new(PostgresDuplicateRepository::new(pool.clone()));
|
||||||
let ingest_tx = Arc::new(PostgresIngestTransaction::new(pool.clone()));
|
let ingest_tx = Arc::new(PostgresIngestTransaction::new(pool.clone()));
|
||||||
|
|
||||||
@@ -49,7 +50,12 @@ pub fn build(
|
|||||||
event_publisher.clone(),
|
event_publisher.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
let read_asset_file = Arc::new(ReadAssetFileHandler::new(asset_repo.clone(), file_storage));
|
let read_asset_file = Arc::new(ReadAssetFileHandler::new(
|
||||||
|
asset_repo.clone(),
|
||||||
|
file_storage.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
|
let read_derivative = Arc::new(ReadDerivativeHandler::new(derivative_repo, file_storage));
|
||||||
|
|
||||||
let register_asset = Arc::new(RegisterAssetHandler::new(
|
let register_asset = Arc::new(RegisterAssetHandler::new(
|
||||||
asset_repo,
|
asset_repo,
|
||||||
@@ -63,6 +69,7 @@ pub fn build(
|
|||||||
get_timeline,
|
get_timeline,
|
||||||
update_metadata,
|
update_metadata,
|
||||||
read_asset_file,
|
read_asset_file,
|
||||||
|
read_derivative,
|
||||||
register_asset,
|
register_asset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ use api_types::{
|
|||||||
};
|
};
|
||||||
use application::{
|
use application::{
|
||||||
catalog::{
|
catalog::{
|
||||||
GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, RegisterAssetCommand,
|
GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, ReadDerivativeQuery,
|
||||||
UpdateMetadataCommand,
|
RegisterAssetCommand, UpdateMetadataCommand,
|
||||||
},
|
},
|
||||||
organization::TagAssetCommand,
|
organization::TagAssetCommand,
|
||||||
storage::IngestAssetCommand,
|
storage::IngestAssetCommand,
|
||||||
@@ -152,6 +152,40 @@ pub async fn tag_asset(
|
|||||||
Ok((StatusCode::CREATED, Json(TagResponse::from_domain(&tag))))
|
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> {
|
fn parse_asset_type(s: &str) -> Result<AssetType, AppError> {
|
||||||
match s {
|
match s {
|
||||||
"image" => Ok(AssetType::Image),
|
"image" => Ok(AssetType::Image),
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ pub fn api_v1_router() -> Router<AppState> {
|
|||||||
.route("/assets/{id}", get(assets::get_asset))
|
.route("/assets/{id}", get(assets::get_asset))
|
||||||
.route("/assets/{id}/metadata", put(assets::update_metadata))
|
.route("/assets/{id}/metadata", put(assets::update_metadata))
|
||||||
.route("/assets/{id}/file", get(assets::serve_file))
|
.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))
|
.route("/assets/{id}/tags", post(assets::tag_asset))
|
||||||
// sharing
|
// sharing
|
||||||
.route("/sharing", post(sharing::share_resource))
|
.route("/sharing", post(sharing::share_resource))
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
catalog::{
|
catalog::{
|
||||||
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, RegisterAssetHandler,
|
GetAssetHandler, GetTimelineHandler, ReadAssetFileHandler, ReadDerivativeHandler,
|
||||||
UpdateMetadataHandler,
|
RegisterAssetHandler, UpdateMetadataHandler,
|
||||||
},
|
},
|
||||||
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
|
identity::{GetProfileHandler, LoginUserHandler, RegisterUserHandler},
|
||||||
organization::{
|
organization::{
|
||||||
@@ -41,6 +41,7 @@ pub struct CatalogHandlers {
|
|||||||
pub get_timeline: Arc<GetTimelineHandler>,
|
pub get_timeline: Arc<GetTimelineHandler>,
|
||||||
pub update_metadata: Arc<UpdateMetadataHandler>,
|
pub update_metadata: Arc<UpdateMetadataHandler>,
|
||||||
pub read_asset_file: Arc<ReadAssetFileHandler>,
|
pub read_asset_file: Arc<ReadAssetFileHandler>,
|
||||||
|
pub read_derivative: Arc<ReadDerivativeHandler>,
|
||||||
pub register_asset: Arc<RegisterAssetHandler>,
|
pub register_asset: Arc<RegisterAssetHandler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user