File Uploads

File Uploads

Blixt provides MultipartForm and UploadedFile for handling file uploads with built-in CSRF validation, size/type checking, and Storage integration.

Basic upload

use blixt::prelude::*;

async fn upload_avatar(
    mut form: MultipartForm,
    State(ctx): State<AppContext>,
) -> Result<impl IntoResponse> {
    while let Some(field) = form.next_field().await? {
        if field.name() == Some("avatar") {
            let file = UploadedFile::from_field(field).await?;
            file.validate()
                .max_size(5 * 1024 * 1024)
                .allowed_types(&["image/jpeg", "image/png", "image/webp"])
                .save(&ctx.storage, "avatars/user-42.jpg")
                .await?;
        }
    }
    Ok(Redirect::to("/profile").with_flash(Flash::success("Avatar updated")))
}

The HTML form

Upload forms must use enctype="multipart/form-data" and include a _csrf hidden field as the first field:

<form method="post" action="/upload" enctype="multipart/form-data">
    <input type="hidden" name="_csrf" value="{{ csrf }}">
    <input type="file" name="avatar" accept="image/*">
    <button type="submit">Upload</button>
</form>

The _csrf field must come before file fields in the form. MultipartForm reads it first and validates against the CSRF cookie before yielding other fields. Alternatively, send the token as an x-csrf-token header.

UploadedFile

Created from a multipart field:

let file = UploadedFile::from_field(field).await?;

file.filename()     // Option<&str> — original filename
file.content_type() // Option<&str> — MIME type
file.size()         // usize — bytes
file.data()         // &[u8] — raw bytes

Validation

Chain validators before saving:

file.validate()
    .max_size(10 * 1024 * 1024)                    // 10MB limit
    .allowed_types(&["image/jpeg", "image/png"])    // MIME whitelist
    .save(&ctx.storage, "uploads/photo.jpg")        // validate + save
    .await?;

On failure, returns Error::BadRequest with a message like "File too large (max 10MB)" or "File type image/gif not allowed".

Use .finish() instead of .save() to validate without saving:

let validated = file.validate()
    .max_size(1024 * 1024)
    .allowed_types(&["application/pdf"])
    .finish()?;
// validated is an UploadedFile — do something custom with it
let bytes = validated.into_bytes();

Saving without validation

Skip the validation chain and save directly:

let file = UploadedFile::from_field(field).await?;
file.save(&ctx.storage, "documents/report.pdf").await?;

Multiple files

Process multiple file fields in one form:

async fn upload_gallery(
    mut form: MultipartForm,
    State(ctx): State<AppContext>,
) -> Result<impl IntoResponse> {
    let mut count = 0;
    while let Some(field) = form.next_field().await? {
        if field.name() == Some("photos") {
            let file = UploadedFile::from_field(field).await?;
            let path = format!("gallery/{count}.jpg");
            file.validate()
                .max_size(5 * 1024 * 1024)
                .allowed_types(&["image/jpeg", "image/png"])
                .save(&ctx.storage, &path)
                .await?;
            count += 1;
        }
    }
    Ok(Redirect::to("/gallery"))
}

Security

  • CSRF validated automatically by MultipartForm
  • Always validate file type with allowed_types() — don't trust the filename extension
  • Always validate file size with max_size() — prevent denial-of-service
  • Files without a content type are treated as application/octet-stream