Skip to main content

modo_upload/
extractor.rs

1use crate::FromMultipart;
2use axum::extract::FromRequest;
3use axum::http::Request;
4use modo::app::AppState;
5use modo::error::{Error, HttpError};
6use modo::validate::Validate;
7use std::ops::Deref;
8
9/// Axum extractor that parses `multipart/form-data`, auto-sanitizes text
10/// fields, and exposes optional field-level validation.
11///
12/// `T` must implement [`FromMultipart`], which is derived automatically with
13/// `#[derive(FromMultipart)]`.  When `T` also implements [`modo::validate::Validate`]
14/// (derived with `#[derive(modo::Validate)]`), the `.validate()` method becomes
15/// available after extraction.
16///
17/// The global `max_file_size` from [`crate::UploadConfig`] is applied to every
18/// file field unless a per-field `#[upload(max_size = "...")]` attribute
19/// overrides it.
20pub struct MultipartForm<T>(pub T);
21
22impl<T> Deref for MultipartForm<T> {
23    type Target = T;
24    fn deref(&self) -> &Self::Target {
25        &self.0
26    }
27}
28
29impl<T> MultipartForm<T> {
30    /// Unwrap the inner parsed value.
31    pub fn into_inner(self) -> T {
32        self.0
33    }
34}
35
36impl<T: Validate> MultipartForm<T> {
37    /// Run field-level validation rules defined on `T`.
38    ///
39    /// Returns `Ok(())` when all rules pass, or a validation error whose
40    /// details map each failing field name to its error messages.
41    pub fn validate(&self) -> Result<(), Error> {
42        self.0.validate()
43    }
44}
45
46impl<T> FromRequest<AppState> for MultipartForm<T>
47where
48    T: FromMultipart + 'static,
49{
50    type Rejection = Error;
51
52    async fn from_request(
53        req: Request<axum::body::Body>,
54        state: &AppState,
55    ) -> Result<Self, Self::Rejection> {
56        let mut multipart = axum::extract::Multipart::from_request(req, state)
57            .await
58            .map_err(|e| HttpError::BadRequest.with_message(format!("{e}")))?;
59        let registered_config = state.services.get::<crate::config::UploadConfig>();
60        let config = match registered_config.as_deref() {
61            Some(cfg) => cfg,
62            None => {
63                return Err(Error::internal(
64                    "UploadConfig not configured — register it via .service(upload_config)",
65                ));
66            }
67        };
68        let max_file_size = config.max_file_size.as_ref().and_then(|s| {
69            modo::config::parse_size(s)
70                .inspect_err(|e| {
71                    tracing::warn!(
72                        size = %s,
73                        error = %e,
74                        "failed to parse max_file_size from UploadConfig, ignoring limit"
75                    );
76                })
77                .ok()
78        });
79        let mut value = T::from_multipart(&mut multipart, max_file_size).await?;
80        modo::sanitize::auto_sanitize(&mut value);
81        Ok(MultipartForm(value))
82    }
83}