use crate::{errors::AppError, extractors::JwtClaims, state::AppState}; use api_types::{ requests::UpdateMetadataRequest, responses::{AssetResponse, IngestResponse, TimelineResponse}, }; use application::{ catalog::{GetAssetQuery, GetTimelineQuery, UpdateMetadataCommand}, storage::IngestAssetCommand, }; use axum::{ Json, extract::{Multipart, Path, Query, State}, http::StatusCode, }; use domain::value_objects::{MetadataValue, StructuredData, SystemId}; use sha2::{Digest, Sha256}; #[derive(Debug, serde::Deserialize)] pub struct TimelineParams { pub limit: Option, pub offset: Option, } pub async fn ingest( State(state): State, claims: JwtClaims, mut multipart: Multipart, ) -> Result<(StatusCode, Json), AppError> { let mut file_data: Option = None; let mut filename: Option = None; let mut target_path_id: Option = None; let mut client_device_id = "web".to_string(); while let Some(field) = multipart .next_field() .await .map_err(|e| AppError::from(domain::errors::DomainError::Validation(e.to_string())))? { let name = field.name().unwrap_or("").to_string(); match name.as_str() { "file" => { filename = field.file_name().map(|s| s.to_string()); let data = field .bytes() .await .map_err(|e| { AppError::from(domain::errors::DomainError::Internal(e.to_string())) })?; file_data = Some(data); } "target_path_id" => { let text = field .text() .await .map_err(|e| { AppError::from(domain::errors::DomainError::Validation(e.to_string())) })?; target_path_id = Some(text.parse::().map_err(|e| { AppError::from(domain::errors::DomainError::Validation(e.to_string())) })?); } "client_device_id" => { client_device_id = field .text() .await .map_err(|e| { AppError::from(domain::errors::DomainError::Validation(e.to_string())) })?; } _ => {} } } let data = file_data .ok_or_else(|| AppError::from(domain::errors::DomainError::Validation("Missing file field".to_string())))?; let fname = filename .ok_or_else(|| AppError::from(domain::errors::DomainError::Validation("Missing filename".to_string())))?; let path_id = target_path_id .ok_or_else(|| AppError::from(domain::errors::DomainError::Validation("Missing target_path_id".to_string())))?; let mut hasher = Sha256::new(); hasher.update(&data); let checksum = format!("{:x}", hasher.finalize()); let file_size = data.len() as u64; let cmd = IngestAssetCommand { uploader_id: claims.user_id, client_device_id, filename: fname, checksum, target_path_id: SystemId::from_uuid(path_id), file_size, data, }; let (asset, session) = state.ingest_asset_handler.execute(cmd).await?; let empty_meta = StructuredData::new(); Ok(( StatusCode::CREATED, Json(IngestResponse { asset: AssetResponse::from_domain(&asset, &empty_meta), 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), }; let (asset, metadata) = state.get_asset_handler.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, limit: params.limit.unwrap_or(50), offset: params.offset.unwrap_or(0), }; let results = state.get_timeline_handler.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 { let mv = match v { serde_json::Value::String(s) => MetadataValue::String(s), serde_json::Value::Number(n) => { if let Some(i) = n.as_i64() { MetadataValue::Integer(i) } else if let Some(f) = n.as_f64() { MetadataValue::Float(f) } else { MetadataValue::Null } } serde_json::Value::Bool(b) => MetadataValue::Boolean(b), serde_json::Value::Null => MetadataValue::Null, _ => MetadataValue::String(v.to_string()), }; data.insert(k, mv); } let cmd = UpdateMetadataCommand { asset_id: SystemId::from_uuid(asset_id), user_id: claims.user_id, data, }; state.update_metadata_handler.execute(cmd).await?; Ok(Json(serde_json::json!({ "status": "updated" }))) }