feat: add file serving endpoint GET /assets/:id/file

This commit is contained in:
2026-05-31 05:59:19 +02:00
parent 3a18fd1d3f
commit 0f003a3bd6
4 changed files with 52 additions and 5 deletions

View File

@@ -64,7 +64,7 @@ pub async fn build_app(config: &Config) -> Result<Router> {
// File storage for ingest // File storage for ingest
let storage_path = std::env::var("STORAGE_PATH").unwrap_or_else(|_| "./data/media".to_string()); 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<LocalFileStorage> = Arc::new(LocalFileStorage::new(&storage_path));
// Album handlers // Album handlers
let create_album_handler = Arc::new(CreateAlbumHandler::new(album_repo.clone())); let create_album_handler = Arc::new(CreateAlbumHandler::new(album_repo.clone()));
@@ -78,7 +78,7 @@ pub async fn build_app(config: &Config) -> Result<Router> {
quota_repo, quota_repo,
ledger_repo, ledger_repo,
asset_repo.clone(), asset_repo.clone(),
file_storage, file_storage.clone(),
event_publisher.clone(), event_publisher.clone(),
)); ));
let get_asset_handler = Arc::new(GetAssetHandler::new( let get_asset_handler = Arc::new(GetAssetHandler::new(
@@ -90,7 +90,7 @@ pub async fn build_app(config: &Config) -> Result<Router> {
metadata_repo.clone(), metadata_repo.clone(),
)); ));
let update_metadata_handler = Arc::new(UpdateMetadataHandler::new( let update_metadata_handler = Arc::new(UpdateMetadataHandler::new(
asset_repo, asset_repo.clone(),
metadata_repo, metadata_repo,
event_publisher, event_publisher,
)); ));
@@ -115,6 +115,8 @@ pub async fn build_app(config: &Config) -> Result<Router> {
update_metadata_handler, update_metadata_handler,
register_volume_handler, register_volume_handler,
register_library_path_handler, register_library_path_handler,
file_storage,
asset_repo,
); );
let cors = CorsLayer::new() let cors = CorsLayer::new()

View File

@@ -9,8 +9,10 @@ use application::{
}; };
use axum::{ use axum::{
Json, Json,
body::Body,
extract::{Multipart, Path, Query, State}, extract::{Multipart, Path, Query, State},
http::StatusCode, http::{StatusCode, header},
response::Response,
}; };
use domain::value_objects::{MetadataValue, StructuredData, SystemId}; use domain::value_objects::{MetadataValue, StructuredData, SystemId};
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
@@ -171,3 +173,39 @@ pub async fn update_metadata(
state.update_metadata_handler.execute(cmd).await?; state.update_metadata_handler.execute(cmd).await?;
Ok(Json(serde_json::json!({ "status": "updated" }))) Ok(Json(serde_json::json!({ "status": "updated" })))
} }
pub async fn serve_file(
State(state): State<AppState>,
_claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>,
) -> Result<Response, AppError> {
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())
}

View File

@@ -27,6 +27,7 @@ pub fn api_v1_router() -> Router<AppState> {
.route("/assets/timeline", get(assets::timeline)) .route("/assets/timeline", get(assets::timeline))
.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))
// storage // storage
.route("/storage/volumes", post(storage::register_volume)) .route("/storage/volumes", post(storage::register_volume))
.route( .route(

View File

@@ -6,7 +6,7 @@ use application::{
}; };
use std::sync::Arc; use std::sync::Arc;
use domain::ports::{StoragePort, TokenIssuer}; use domain::ports::{AssetRepository, FileStoragePort, StoragePort, TokenIssuer};
#[derive(Clone)] #[derive(Clone)]
pub struct AppState { pub struct AppState {
@@ -24,6 +24,8 @@ pub struct AppState {
pub update_metadata_handler: Arc<UpdateMetadataHandler>, pub update_metadata_handler: Arc<UpdateMetadataHandler>,
pub register_volume_handler: Arc<RegisterVolumeHandler>, pub register_volume_handler: Arc<RegisterVolumeHandler>,
pub register_library_path_handler: Arc<RegisterLibraryPathHandler>, pub register_library_path_handler: Arc<RegisterLibraryPathHandler>,
pub file_storage: Arc<dyn FileStoragePort>,
pub asset_repo: Arc<dyn AssetRepository>,
} }
impl AppState { impl AppState {
@@ -43,6 +45,8 @@ impl AppState {
update_metadata_handler: Arc<UpdateMetadataHandler>, update_metadata_handler: Arc<UpdateMetadataHandler>,
register_volume_handler: Arc<RegisterVolumeHandler>, register_volume_handler: Arc<RegisterVolumeHandler>,
register_library_path_handler: Arc<RegisterLibraryPathHandler>, register_library_path_handler: Arc<RegisterLibraryPathHandler>,
file_storage: Arc<dyn FileStoragePort>,
asset_repo: Arc<dyn AssetRepository>,
) -> Self { ) -> Self {
Self { Self {
register_handler, register_handler,
@@ -59,6 +63,8 @@ impl AppState {
update_metadata_handler, update_metadata_handler,
register_volume_handler, register_volume_handler,
register_library_path_handler, register_library_path_handler,
file_storage,
asset_repo,
} }
} }
} }