//! Multipart form handling shared by the product and category admin forms. //! //! Both forms submit a mix of text fields and an optional `image` file part; //! this collects them into an easy-to-query [`MultipartForm`] and stores any //! uploaded image through the configured storage driver. use std::collections::HashMap; use axum::extract::Multipart; use loco_rs::prelude::*; use crate::media::{detect_image_extension, store_upload, IMAGE_MAX_BYTES, IMAGE_STORAGE_DIR}; fn normalize_empty(value: Option) -> Option { value.and_then(|value| { let value = value.trim().to_string(); (!value.is_empty()).then_some(value) }) } /// Collected multipart form: text fields keyed by name, plus the raw bytes of /// an `image` file part if one was uploaded (an empty file input is ignored). pub(crate) struct MultipartForm { fields: HashMap, pub(crate) image: Option>, } impl MultipartForm { /// Trimmed value of a text field, `None` when missing or blank. pub(crate) fn text(&self, key: &str) -> Option { normalize_empty(self.fields.get(key).cloned()) } /// Whether a checkbox-style field is checked. pub(crate) fn checked(&self, key: &str) -> bool { matches!( self.fields.get(key).map(String::as_str), Some("on" | "true" | "1") ) } } pub(crate) async fn read_multipart_form(mut multipart: Multipart) -> Result { let mut fields = HashMap::new(); let mut image = None; while let Some(mut field) = multipart .next_field() .await .map_err(|err| Error::BadRequest(format!("invalid multipart data: {err}")))? { let name = field.name().unwrap_or("").to_string(); if name == "image" { let mut data = Vec::new(); while let Some(chunk) = field .chunk() .await .map_err(|err| Error::BadRequest(format!("invalid multipart chunk: {err}")))? { data.extend_from_slice(&chunk); if data.len() > IMAGE_MAX_BYTES { return Err(Error::BadRequest(format!( "image is larger than {} MB", IMAGE_MAX_BYTES / 1024 / 1024 ))); } } if !data.is_empty() { image = Some(data); } } else { let value = field .text() .await .map_err(|err| Error::BadRequest(format!("invalid multipart field: {err}")))?; fields.insert(name, value); } } Ok(MultipartForm { fields, image }) } /// Store an uploaded image's bytes and return its generated filename. pub(crate) async fn store_image(ctx: &AppContext, data: Vec) -> Result { let extension = detect_image_extension(&data)?; store_upload(ctx, IMAGE_STORAGE_DIR, extension, data).await }