File Upload¶
A form with a file. The library renders the file input (with drag and drop)
and sends the form as multipart/form-data; your route checks and stores the
file.
models.py
from typing import Any
from pydantic_schemaforms import Field, FormModel
class UploadForm(FormModel):
title: str = Field(title='Title', min_length=2)
document: Any = Field(
None,
title='Document',
description='PDF or Word, up to 5 MB.',
ui_element='file',
ui_options={'accept': '.pdf,.docx'},
)
ALLOWED_TYPES = {'.pdf', '.docx'}
MAX_BYTES = 5 * 1024 * 1024
main.py
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from models import ALLOWED_TYPES, MAX_BYTES, UploadForm
from starlette.datastructures import UploadFile
from pydantic_schemaforms import html, render_form_html_async
app = FastAPI()
FORM_OPTIONS = {'submit_url': '/upload', 'include_framework_assets': True, 'asset_mode': 'cdn'}
def check_upload(upload: object) -> tuple[str | None, bytes]:
if not isinstance(upload, UploadFile) or not upload.filename:
return 'Choose a file to upload', b''
if Path(upload.filename).suffix.lower() not in ALLOWED_TYPES:
return 'Upload a PDF or Word document', b''
content = upload.file.read(MAX_BYTES + 1)
if len(content) > MAX_BYTES:
return 'The file is larger than 5 MB', b''
return None, content
@app.get('/upload', response_class=HTMLResponse)
async def show_upload():
return await render_form_html_async(UploadForm, **FORM_OPTIONS)
@app.post('/upload', response_class=HTMLResponse)
async def upload(request: Request):
form = await request.form()
result = UploadForm.validate(form, **FORM_OPTIONS)
file_error, content = check_upload(form.get('document'))
if not result.is_valid or file_error:
errors = {**result.errors, **({'document': file_error} if file_error else {})}
return await render_form_html_async(
UploadForm, form_data={'title': form.get('title')}, errors=errors, **FORM_OPTIONS
)
size = len(content)
return html(t'<p>Stored "{result.data["title"]}" ({size} bytes).</p>')
main.py
from pathlib import Path
from flask import Flask, request
from models import ALLOWED_TYPES, MAX_BYTES, UploadForm
from werkzeug.datastructures import CombinedMultiDict, FileStorage
from pydantic_schemaforms import html, render_form_html
app = Flask(__name__)
app.config['MAX_CONTENT_LENGTH'] = MAX_BYTES + 64 * 1024 # Flask refuses bigger requests
FORM_OPTIONS = {'submit_url': '/upload', 'include_framework_assets': True, 'asset_mode': 'cdn'}
def check_upload(upload: object) -> tuple[str | None, bytes]:
if not isinstance(upload, FileStorage) or not upload.filename:
return 'Choose a file to upload', b''
if Path(upload.filename).suffix.lower() not in ALLOWED_TYPES:
return 'Upload a PDF or Word document', b''
content = upload.stream.read(MAX_BYTES + 1)
if len(content) > MAX_BYTES:
return 'The file is larger than 5 MB', b''
return None, content
@app.get('/upload')
def show_upload():
return render_form_html(UploadForm, **FORM_OPTIONS)
@app.post('/upload')
def upload():
result = UploadForm.validate(CombinedMultiDict([request.form, request.files]), **FORM_OPTIONS)
file_error, content = check_upload(request.files.get('document'))
if not result.is_valid or file_error:
errors = {**result.errors, **({'document': file_error} if file_error else {})}
return render_form_html(
UploadForm,
form_data={'title': request.form.get('title')},
errors=errors,
**FORM_OPTIONS,
)
size = len(content)
return html(t'<p>Stored "{result.data["title"]}" ({size} bytes).</p>')
How it works¶
ui_element="file"renders the file input.acceptlimits what the file picker offers;multiple=Truewould allow several files. A form with a file input getsenctype="multipart/form-data"by itself.- The field is typed
Any, because the uploaded file arrives as your framework's own object: Starlette'sUploadFileor Werkzeug'sFileStorage. Flask keeps files apart from the other fields, so the route combinesrequest.formandrequest.filesbefore validating. check_upload()checks the file itself: that one was chosen, its extension, and its size, reading at most one byte more than the limit. Its message is added to the form's errors so it shows under the file input.- The browser can't keep a chosen file across a re-render, so after an error the user picks it again. The title is kept.
Treat uploads as untrusted
accept and the file name are only hints: check the content before you
use it, store files outside your web root under names you generate, and
never show a file name in HTML without escaping it (html(t"...") does).