Practical Examples

File upload

Upload form

use runique::prelude::*;
use serde::Serialize;

#[derive(Serialize, Debug, Clone)]
#[serde(transparent)]
pub struct ImageForm {
    pub form: Forms,
}

impl FormAccess for ImageForm {
    fn register_fields(form: &mut Forms) {
        form.field(
            &MediaField::image("image")
                .label("Image")
                .upload_to_env()        // → {MEDIA_ROOT}/image/
                .max_size(FileSize::mb(5))
                .max_files(1)
                .max_dimensions(1920, 1080)
                .allowed_extensions(vec!["jpg", "png", "webp", "avif"])
        );
    }
    impl_form_access!();
}

impl RuniqueForm for ImageForm {}

Configuring the upload path

The folder is always relative to MEDIA_ROOT: the file is moved there on validation, and the database stores the relative path (avatars/photo.png), served under /media/. Three forms:

// 1 — explicit subfolder → {MEDIA_ROOT}/avatars/
MediaField::image("avatar").upload_to("avatars")

// 2 — subfolder named after the field → {MEDIA_ROOT}/img/
MediaField::image("img").upload_to_env()

// 3 — root of MEDIA_ROOT (same as no upload_to)
let config = StaticConfig::from_env();
MediaField::image("img").upload_to(&config)

Don't include MEDIA_ROOT in the path: upload_to("media/avatars") with MEDIA_ROOT=media/ would give media/media/avatars/.

.env configuration:

MEDIA_ROOT=media/

Available field types

MediaField::image("img")     // jpg jpeg png gif webp avif
FileField::document("doc")  // pdf doc docx txt odt
FileField::any("f")         // any extension, except active ones (.svg, .html, .js…)

// Custom extensions:
FileField::any("data").allowed_extensions(vec!["csv", "json"])

FileField::any does not check the content. The framework always refuses the active extensions a browser runs: .svg, .svgz, .html, .htm, .xhtml, .xht, .shtml, .xml, .js, .mjs. What the file is, and how it is served, is the application's responsibility. For a closed list, use image(), document(), or allowed_extensions.


Upload handler

pub async fn upload_image(mut request: Request) -> AppResult<Response> {
    let form: ImageForm = request.form("upload");
    let template = "forms/upload_image.html";

    let validated = match ValidationForm::try_form(form, &request).await {
        Ok(validated) => validated,
        Err(form) => {
            context_update!(request => { "title" => "Upload an image", "image_form" => &form });
            return request.render(template);
        }
    };

    // A form without a model: no `save()`, so the file is published here.
    // Once validated, it still sits in the request's staging folder, removed
    // when the request ends.
    let mut form = validated.into_form();
    let path = form.cleaned::<String>("image").unwrap_or_default(); // relative to MEDIA_ROOT
    if let Err(error) = form.forms_mut().publish_uploads().await {
        form.forms_mut().errors.push(error);
        context_update!(request => { "title" => "Upload an image", "image_form" => &form });
        return request.render(template);
    }
    form.forms_mut().complete_uploads().await;

    // `path`: store it wherever the application needs it.
    success!(request.notices => "File uploaded successfully!");
    Ok(Redirect::to("/").into_response())
}

Upload template

{% extends "base.html" %}

{% block content %}
    <h1>{{ title }}</h1>

    <form method="post" enctype="multipart/form-data">
        {% form.image_form %}
        <button type="submit">Upload</button>
    </form>
{% endblock %}

{% form.image_form %} puts the CSRF token before the fields. In a hand-written form, put {% csrf %} before the first <input type="file">: no file is written to disk until the token has been read and checked, and a file arriving before it gets the request refused (403). A JavaScript upload can send the token in the X-CSRF-Token header instead.