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 = cv.size
FileField is always treated as a stealth field. The uploaded file is passed to handle_stealth_fields as a Starlette UploadFile object.
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()
path = f"uploads/{player.session.sid}_{player.pid}_{photo.filename}"
with open(path, "wb") as f:
f.write(contents)
player.photo_path = 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(
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 name field saves to player.name automatically. The photo field is handled separately.
Rendering in templates¶
Use the standard field rendering:
Or render all fields at once:
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 |