From 0f003a3bd6dcfbffa24326b311c4713ee3a066ef Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 31 May 2026 05:59:19 +0200 Subject: [PATCH] feat: add file serving endpoint GET /assets/:id/file --- crates/bootstrap/src/factory.rs | 8 +++-- crates/presentation/src/handlers/assets.rs | 40 +++++++++++++++++++++- crates/presentation/src/routes.rs | 1 + crates/presentation/src/state.rs | 8 ++++- 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/bootstrap/src/factory.rs b/crates/bootstrap/src/factory.rs index 68f19d7..3df2a4f 100644 --- a/crates/bootstrap/src/factory.rs +++ b/crates/bootstrap/src/factory.rs @@ -64,7 +64,7 @@ pub async fn build_app(config: &Config) -> Result { // File storage for ingest let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string()); - let file_storage = Arc::new(LocalFileStorage::new(&storage_path)); + let file_storage: Arc = Arc::new(LocalFileStorage::new(&storage_path)); // Album handlers let create_album_handler = Arc::new(CreateAlbumHandler::new(album_repo.clone())); @@ -78,7 +78,7 @@ pub async fn build_app(config: &Config) -> Result { quota_repo, ledger_repo, asset_repo.clone(), - file_storage, + file_storage.clone(), event_publisher.clone(), )); let get_asset_handler = Arc::new(GetAssetHandler::new( @@ -90,7 +90,7 @@ pub async fn build_app(config: &Config) -> Result { metadata_repo.clone(), )); let update_metadata_handler = Arc::new(UpdateMetadataHandler::new( - asset_repo, + asset_repo.clone(), metadata_repo, event_publisher, )); @@ -115,6 +115,8 @@ pub async fn build_app(config: &Config) -> Result { update_metadata_handler, register_volume_handler, register_library_path_handler, + file_storage, + asset_repo, ); let cors = CorsLayer::new() diff --git a/crates/presentation/src/handlers/assets.rs b/crates/presentation/src/handlers/assets.rs index 39aac59..cd3b8b1 100644 --- a/crates/presentation/src/handlers/assets.rs +++ b/crates/presentation/src/handlers/assets.rs @@ -9,8 +9,10 @@ use application::{ }; use axum::{ Json, + body::Body, extract::{Multipart, Path, Query, State}, - http::StatusCode, + http::{StatusCode, header}, + response::Response, }; use domain::value_objects::{MetadataValue, StructuredData, SystemId}; use sha2::{Digest, Sha256}; @@ -171,3 +173,39 @@ pub async fn update_metadata( state.update_metadata_handler.execute(cmd).await?; Ok(Json(serde_json::json!({ "status": "updated" }))) } + +pub async fn serve_file( + State(state): State, + _claims: JwtClaims, + Path((asset_id,)): Path<(uuid::Uuid,)>, +) -> Result { + let asset = state + .asset_repo + .find_by_id(&SystemId::from_uuid(asset_id)) + .await? + .ok_or_else(|| domain::errors::DomainError::NotFound("Asset not found".into()))?; + + let data = state + .file_storage + .read_file(&asset.source_reference.relative_path) + .await?; + + Ok(Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, &asset.mime_type) + .header(header::CONTENT_LENGTH, data.len()) + .header( + header::CONTENT_DISPOSITION, + format!( + "inline; filename=\"{}\"", + asset + .source_reference + .relative_path + .rsplit('/') + .next() + .unwrap_or("file") + ), + ) + .body(Body::from(data)) + .unwrap()) +} diff --git a/crates/presentation/src/routes.rs b/crates/presentation/src/routes.rs index 2cf966a..03e4f7d 100644 --- a/crates/presentation/src/routes.rs +++ b/crates/presentation/src/routes.rs @@ -27,6 +27,7 @@ pub fn api_v1_router() -> Router { .route("/assets/timeline", get(assets::timeline)) .route("/assets/{id}", get(assets::get_asset)) .route("/assets/{id}/metadata", put(assets::update_metadata)) + .route("/assets/{id}/file", get(assets::serve_file)) // storage .route("/storage/volumes", post(storage::register_volume)) .route( diff --git a/crates/presentation/src/state.rs b/crates/presentation/src/state.rs index 90bb2ac..013e350 100644 --- a/crates/presentation/src/state.rs +++ b/crates/presentation/src/state.rs @@ -6,7 +6,7 @@ use application::{ }; use std::sync::Arc; -use domain::ports::{StoragePort, TokenIssuer}; +use domain::ports::{AssetRepository, FileStoragePort, StoragePort, TokenIssuer}; #[derive(Clone)] pub struct AppState { @@ -24,6 +24,8 @@ pub struct AppState { pub update_metadata_handler: Arc, pub register_volume_handler: Arc, pub register_library_path_handler: Arc, + pub file_storage: Arc, + pub asset_repo: Arc, } impl AppState { @@ -43,6 +45,8 @@ impl AppState { update_metadata_handler: Arc, register_volume_handler: Arc, register_library_path_handler: Arc, + file_storage: Arc, + asset_repo: Arc, ) -> Self { Self { register_handler, @@ -59,6 +63,8 @@ impl AppState { update_metadata_handler, register_volume_handler, register_library_path_handler, + file_storage, + asset_repo, } } }