Files
kompress_eshop/src/controllers/admin_form.rs
Priec af930b79bb
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
loco straucture
2026-06-16 23:40:53 +02:00

88 lines
3.0 KiB
Rust

//! 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::controllers::media::{detect_image_extension, store_upload, IMAGE_MAX_BYTES, IMAGE_STORAGE_DIR};
fn normalize_empty(value: Option<String>) -> Option<String> {
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<String, String>,
pub(crate) image: Option<Vec<u8>>,
}
impl MultipartForm {
/// Trimmed value of a text field, `None` when missing or blank.
pub(crate) fn text(&self, key: &str) -> Option<String> {
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<MultipartForm> {
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<u8>) -> Result<String> {
let extension = detect_image_extension(&data)?;
store_upload(ctx, IMAGE_STORAGE_DIR, extension, data).await
}