use crate::{ constants::{DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE}, errors::AppError, extractors::{JwtClaims, UploadedAsset}, state::AppState, }; use api_types::{ requests::{RegisterAssetRequest, TagAssetRequest}, responses::{AssetResponse, IngestResponse, TagResponse, TimelineResponse}, }; use application::{ catalog::{ GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, RegisterAssetCommand, UpdateMetadataCommand, }, organization::TagAssetCommand, storage::IngestAssetCommand, }; use axum::{ Json, body::Body, extract::{Multipart, Path, Query, State}, http::{StatusCode, header}, response::Response, }; use domain::{ catalog::entities::AssetType, errors::DomainError, value_objects::{MetadataValue, StructuredData, SystemId}, }; #[derive(Debug, serde::Deserialize)] pub struct TimelineParams { pub limit: Option, pub offset: Option, } pub async fn ingest( State(state): State, claims: JwtClaims, multipart: Multipart, ) -> Result<(StatusCode, Json), AppError> { let upload = UploadedAsset::from_multipart(multipart).await?; let cmd = IngestAssetCommand { uploader_id: claims.user_id, client_device_id: upload.client_device_id, filename: upload.filename, target_path_id: upload.target_path_id, file_size: upload.data.len() as u64, data: upload.data, }; let (asset, session) = state.catalog.ingest_asset.execute(cmd).await?; Ok(( StatusCode::CREATED, Json(IngestResponse { asset: AssetResponse::from_domain(&asset, &StructuredData::new()), session_id: *session.session_id.as_uuid(), }), )) } pub async fn get_asset( State(state): State, claims: JwtClaims, Path((asset_id,)): Path<(uuid::Uuid,)>, ) -> Result, AppError> { let query = GetAssetQuery { asset_id: SystemId::from_uuid(asset_id), user_id: claims.user_id, }; let (asset, metadata) = state.catalog.get_asset.execute(query).await?; Ok(Json(AssetResponse::from_domain(&asset, &metadata))) } pub async fn timeline( State(state): State, claims: JwtClaims, Query(params): Query, ) -> Result, AppError> { let query = GetTimelineQuery { owner_id: claims.user_id, caller_id: None, limit: params.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(MAX_PAGE_SIZE), offset: params.offset.unwrap_or(0), }; let results = state.catalog.get_timeline.execute(query).await?; let total = results.len(); let assets = results .iter() .map(|(asset, meta)| AssetResponse::from_domain(asset, meta)) .collect(); Ok(Json(TimelineResponse { assets, total })) } pub async fn update_metadata( State(state): State, claims: JwtClaims, Path((asset_id,)): Path<(uuid::Uuid,)>, Json(req): Json, ) -> Result, AppError> { let mut data = StructuredData::new(); for (k, v) in req.data { data.insert(k, MetadataValue::from(v)); } let cmd = UpdateMetadataCommand { asset_id: SystemId::from_uuid(asset_id), user_id: claims.user_id, data, }; state.catalog.update_metadata.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 query = ReadAssetFileQuery { asset_id: SystemId::from_uuid(asset_id), }; let result = state.catalog.read_asset_file.execute(query).await?; Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, &result.mime_type) .header(header::CONTENT_LENGTH, result.data.len()) .header( header::CONTENT_DISPOSITION, format!("inline; filename=\"{}\"", result.filename), ) .body(Body::from(result.data)) .map_err(|e| AppError::from(domain::errors::DomainError::Internal(e.to_string()))) } pub async fn tag_asset( State(state): State, claims: JwtClaims, Path((asset_id,)): Path<(uuid::Uuid,)>, Json(req): Json, ) -> Result<(StatusCode, Json), AppError> { let cmd = TagAssetCommand { asset_id: SystemId::from_uuid(asset_id), tag_name: req.tag_name, user_id: claims.user_id, }; let (tag, _asset_tag) = state.organization.tag_asset.execute(cmd).await?; Ok((StatusCode::CREATED, Json(TagResponse::from_domain(&tag)))) } fn parse_asset_type(s: &str) -> Result { match s { "image" => Ok(AssetType::Image), "video" => Ok(AssetType::Video), "live_photo" => Ok(AssetType::LivePhoto), _ => Err(AppError::from(DomainError::Validation(format!( "Invalid asset type: {s}" )))), } } pub async fn register_asset( State(state): State, claims: JwtClaims, Json(req): Json, ) -> Result<(StatusCode, Json), AppError> { let asset_type = parse_asset_type(&req.asset_type)?; let cmd = RegisterAssetCommand { volume_id: SystemId::from_uuid(req.volume_id), relative_path: req.relative_path, checksum: req.checksum, asset_type, mime_type: req.mime_type, file_size: req.file_size, owner_id: claims.user_id, }; let (asset, _dup_group) = state.catalog.register_asset.execute(cmd).await?; Ok(( StatusCode::CREATED, Json(AssetResponse::from_domain(&asset, &StructuredData::new())), )) }