refactor: clean up presentation layer — AppState grouping, multipart extractor, thin handlers

This commit is contained in:
2026-05-31 06:14:19 +02:00
parent 34b231a8f6
commit 2d9dd2c2d0
14 changed files with 199 additions and 258 deletions

View File

@@ -1,10 +1,12 @@
use crate::{errors::AppError, extractors::JwtClaims, state::AppState};
use api_types::{
requests::UpdateMetadataRequest,
responses::{AssetResponse, IngestResponse, TimelineResponse},
use crate::{
constants::{DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE},
errors::AppError,
extractors::{JwtClaims, UploadedAsset},
state::AppState,
};
use api_types::responses::{AssetResponse, IngestResponse, TimelineResponse};
use application::{
catalog::{GetAssetQuery, GetTimelineQuery, UpdateMetadataCommand},
catalog::{GetAssetQuery, GetTimelineQuery, ReadAssetFileQuery, UpdateMetadataCommand},
storage::IngestAssetCommand,
};
use axum::{
@@ -25,78 +27,25 @@ pub struct TimelineParams {
pub async fn ingest(
State(state): State<AppState>,
claims: JwtClaims,
mut multipart: Multipart,
multipart: Multipart,
) -> Result<(StatusCode, Json<IngestResponse>), AppError> {
let mut file_data: Option<bytes::Bytes> = None;
let mut filename: Option<String> = None;
let mut target_path_id: Option<uuid::Uuid> = 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::<uuid::Uuid>().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 file_size = data.len() as u64;
let upload = UploadedAsset::from_multipart(multipart).await?;
let cmd = IngestAssetCommand {
uploader_id: claims.user_id,
client_device_id,
filename: fname,
target_path_id: SystemId::from_uuid(path_id),
file_size,
data,
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.ingest_asset_handler.execute(cmd).await?;
let empty_meta = StructuredData::new();
let (asset, session) = state.catalog.ingest_asset.execute(cmd).await?;
Ok((
StatusCode::CREATED,
Json(IngestResponse {
asset: AssetResponse::from_domain(&asset, &empty_meta),
asset: AssetResponse::from_domain(&asset, &StructuredData::new()),
session_id: *session.session_id.as_uuid(),
}),
))
@@ -111,7 +60,7 @@ pub async fn get_asset(
asset_id: SystemId::from_uuid(asset_id),
user_id: claims.user_id,
};
let (asset, metadata) = state.get_asset_handler.execute(query).await?;
let (asset, metadata) = state.catalog.get_asset.execute(query).await?;
Ok(Json(AssetResponse::from_domain(&asset, &metadata)))
}
@@ -122,10 +71,10 @@ pub async fn timeline(
) -> Result<Json<TimelineResponse>, AppError> {
let query = GetTimelineQuery {
owner_id: claims.user_id,
limit: params.limit.unwrap_or(50),
limit: params.limit.unwrap_or(DEFAULT_PAGE_SIZE).min(MAX_PAGE_SIZE),
offset: params.offset.unwrap_or(0),
};
let results = state.get_timeline_handler.execute(query).await?;
let results = state.catalog.get_timeline.execute(query).await?;
let total = results.len();
let assets = results
.iter()
@@ -138,26 +87,11 @@ pub async fn update_metadata(
State(state): State<AppState>,
claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>,
Json(req): Json<UpdateMetadataRequest>,
Json(req): Json<api_types::requests::UpdateMetadataRequest>,
) -> Result<Json<serde_json::Value>, 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);
data.insert(k, MetadataValue::from(v));
}
let cmd = UpdateMetadataCommand {
@@ -165,7 +99,7 @@ pub async fn update_metadata(
user_id: claims.user_id,
data,
};
state.update_metadata_handler.execute(cmd).await?;
state.catalog.update_metadata.execute(cmd).await?;
Ok(Json(serde_json::json!({ "status": "updated" })))
}
@@ -174,33 +108,19 @@ pub async fn serve_file(
_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 query = ReadAssetFileQuery {
asset_id: SystemId::from_uuid(asset_id),
};
let result = state.catalog.read_asset_file.execute(query).await?;
let data = state
.file_storage
.read_file(&asset.source_reference.relative_path)
.await?;
Ok(Response::builder()
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, &asset.mime_type)
.header(header::CONTENT_LENGTH, data.len())
.header(header::CONTENT_TYPE, &result.mime_type)
.header(header::CONTENT_LENGTH, result.data.len())
.header(
header::CONTENT_DISPOSITION,
format!(
"inline; filename=\"{}\"",
asset
.source_reference
.relative_path
.rsplit('/')
.next()
.unwrap_or("file")
),
format!("inline; filename=\"{}\"", result.filename),
)
.body(Body::from(data))
.unwrap())
.body(Body::from(result.data))
.map_err(|e| AppError::from(domain::errors::DomainError::Internal(e.to_string())))
}