greentic-designer 0.6.0

Greentic Designer — orchestrator that powers Adaptive Card design via the adaptive-card-mcp toolkit
Documentation
//! `POST /api/upload` — image upload with validation and processing.

use crate::ui::image;
use crate::ui::state::AppState;
use crate::ui::storage::ImageStore;
use axum::Json;
use axum::extract::{Multipart, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use serde_json::json;
use std::sync::Arc;

pub async fn post_upload(
    State(state): State<Arc<AppState>>,
    mut multipart: Multipart,
) -> impl IntoResponse {
    let field = match multipart.next_field().await {
        Ok(Some(f)) => f,
        Ok(None) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(json!({"error": "missing file field"})),
            )
                .into_response();
        }
        Err(e) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(json!({"error": format!("multipart error: {e}")})),
            )
                .into_response();
        }
    };

    let filename = field.file_name().unwrap_or("upload.png").to_string();

    let data = match field.bytes().await {
        Ok(b) => b,
        Err(e) => {
            return (
                StatusCode::BAD_REQUEST,
                Json(json!({"error": format!("failed to read upload: {e}")})),
            )
                .into_response();
        }
    };

    // Validate format and size.
    let format = match image::validate(&data) {
        Ok(f) => f,
        Err(e) => {
            let status = if e.to_string().contains("too large") {
                StatusCode::PAYLOAD_TOO_LARGE
            } else {
                StatusCode::UNSUPPORTED_MEDIA_TYPE
            };
            return (status, Json(json!({"error": e.to_string()}))).into_response();
        }
    };

    // Process: decode, resize, convert to PNG.
    let processed = match image::process(&data, format) {
        Ok(p) => p,
        Err(e) => {
            return (
                StatusCode::UNPROCESSABLE_ENTITY,
                Json(json!({"error": format!("failed to process image: {e}")})),
            )
                .into_response();
        }
    };

    // Store.
    match state.image_store.store(&filename, &processed).await {
        Ok(meta) => Json(json!({
            "id": meta.id,
            "filename": meta.original_filename,
            "size": meta.size_bytes,
        }))
        .into_response(),
        Err(_) => (
            StatusCode::INTERNAL_SERVER_ERROR,
            Json(json!({"error": "failed to store image"})),
        )
            .into_response(),
    }
}