Files
k-photos/crates/presentation/src/handlers/assets.rs

187 lines
5.7 KiB
Rust

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<u32>,
pub offset: Option<u32>,
}
pub async fn ingest(
State(state): State<AppState>,
claims: JwtClaims,
multipart: Multipart,
) -> Result<(StatusCode, Json<IngestResponse>), 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<AppState>,
claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>,
) -> Result<Json<AssetResponse>, 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<AppState>,
claims: JwtClaims,
Query(params): Query<TimelineParams>,
) -> Result<Json<TimelineResponse>, 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<AppState>,
claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>,
Json(req): Json<api_types::requests::UpdateMetadataRequest>,
) -> Result<Json<serde_json::Value>, 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<AppState>,
_claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>,
) -> Result<Response, AppError> {
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<AppState>,
claims: JwtClaims,
Path((asset_id,)): Path<(uuid::Uuid,)>,
Json(req): Json<TagAssetRequest>,
) -> Result<(StatusCode, Json<TagResponse>), 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<AssetType, AppError> {
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<AppState>,
claims: JwtClaims,
Json(req): Json<RegisterAssetRequest>,
) -> Result<(StatusCode, Json<AssetResponse>), 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())),
))
}