Skip to content

File uploads

uproot supports file uploads from participants using FileField. Uploaded files are handled as stealth fields: They are not stored in the database automatically, giving you full control over how to process them.

Basic file upload

Add a FileField to your page:

class Upload(Page):
    fields = dict(
        cv=FileField(label="Please upload your curriculum vitæ."),
    )

    @classmethod
    async def handle_stealth_fields(page, player, data):
        cv = data["cv"]
        contents = await cv.read()
        player.file_name = cv.filename
        player.file_size = len(contents)

FileField is always treated as a stealth field. The uploaded file is passed to handle_stealth_fields as a Starlette UploadFile object.

See the upload example

The UploadFile object

The uploaded file has these attributes:

Attribute Type Description
filename str Original filename
size int File size in bytes
content_type str MIME type (e.g., image/png)

And this method:

Method Returns Description
await file.read() bytes Read the full file contents

Storing uploaded files

Since uploads are stealth fields, you decide where to store the data. Common approaches:

Save to disk

@classmethod
async def handle_stealth_fields(page, player, data):
    photo = data["photo"]
    if photo.size > 0:
        contents = await photo.read()
        from pathlib import Path

        path = Path("uploads") / (
            f"{player.session.name}_{player.name}_{uuid()}"
            f"{Path(photo.filename or '').suffix.lower()}"
        )
        path.parent.mkdir(parents=True, exist_ok=True)
        with path.open("wb") as f:
            f.write(contents)
        player.photo_path = str(path)

Store metadata only

@classmethod
async def handle_stealth_fields(page, player, data):
    doc = data["document"]
    player.doc_name = doc.filename
    player.doc_size = doc.size
    player.doc_type = doc.content_type

Validating uploads

Return error messages from handle_stealth_fields to reject the submission:

@classmethod
async def handle_stealth_fields(page, player, data):
    photo = data["photo"]

    if photo.size == 0:
        return "Please select a file"

    if photo.size > 5_000_000:
        return "File too large (max 5 MB)"

    if photo.content_type not in ("image/png", "image/jpeg"):
        return "Only PNG and JPEG files are accepted"

    contents = await photo.read()
    player.photo_size = len(contents)

Combining uploads with regular fields

You can mix file uploads with regular fields on the same page. Regular fields are saved automatically; the file field goes through handle_stealth_fields:

class UploadPage(Page):
    fields = dict(
        participant_name=StringField(label="Your name"),
        photo=FileField(label="Upload a photo"),
    )

    @classmethod
    async def handle_stealth_fields(page, player, data):
        photo = data["photo"]
        if photo.size > 0:
            contents = await photo.read()
            player.photo_name = photo.filename

The participant_name field saves to player.participant_name automatically. player.name is reserved for uproot’s generated player name. The photo field is handled separately.

Rendering in templates

Use the standard field rendering:

{% extends "Base.html" %}

{% block main %}
{{ field(form.cv) }}
{% endblock main %}

Or render all fields at once:

{{ fields() }}

Summary

Feature Purpose
FileField(label=...) File upload field
handle_stealth_fields(page, player, data) Process uploaded files
await file.read() Get file contents as bytes
file.filename Original filename
file.size File size in bytes
file.content_type MIME type
Return a string from handler Validation error message