174 lines
5.7 KiB
Rust
174 lines
5.7 KiB
Rust
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<u32>,
|
|
pub offset: Option<u32>,
|
|
}
|
|
|
|
pub async fn ingest(
|
|
State(state): State<AppState>,
|
|
claims: JwtClaims,
|
|
mut 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 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<AppState>,
|
|
_claims: JwtClaims,
|
|
Path((asset_id,)): Path<(uuid::Uuid,)>,
|
|
) -> Result<Json<AssetResponse>, 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<AppState>,
|
|
claims: JwtClaims,
|
|
Query(params): Query<TimelineParams>,
|
|
) -> Result<Json<TimelineResponse>, 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<AppState>,
|
|
claims: JwtClaims,
|
|
Path((asset_id,)): Path<(uuid::Uuid,)>,
|
|
Json(req): Json<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);
|
|
}
|
|
|
|
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" })))
|
|
}
|