Skip to content
How-toAdvancedMove files in and out of the app.

Upload and download files

Goal

You want to move files across your app's boundary: visitors handing files in, your app handing files out, and your code reading data files that were bundled with it.

Before you start

You can build a form. One mental adjustment is needed: your app lives in the browser, so "files" means the visitor's files (which they must explicitly choose) and files bundled with your app. Your code cannot browse a hard drive on its own.

Recipe: receive a file

A FileUpload component delivers the chosen file to a route parameter; the parameter's type annotation decides what arrives.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    poem: str


@route
def index(state: State) -> Page:
    return Page(state, [
        Header("Poem Inspector"),
        "The current poem has " + str(len(state.poem)) + " characters.\n",
        "Upload a text file:",
        FileUpload("poem_file", accept=".txt"),
        "\n",
        Button("Inspect", "inspect")
    ])


@route
def inspect(state: State, poem_file: str) -> Page:
    state.poem = poem_file
    return Page(state, [
        "The file begins: " + state.poem[:40] + "\n",
        Link("Back", "index")
    ])


start_server(State(""))

Each annotation produces a different kind of value:

Annotation What the route receives
str The file's contents as text.
bytes The raw bytes, for any file type.
Picture An image, decoded; see Use pictures.
DrafterTextFile / DrafterBinaryFile Contents plus the filename and metadata; see File types.

Add | None to any of them to make "no file chosen" arrive as None instead of an error. A FileUpload with the multiple attribute delivers a list.

Recipe: offer a download

Download works in the opposite direction: it offers text (or bytes) your app built as a file the visitor can save.

from drafter import *
from dataclasses import dataclass


@dataclass
class State:
    entries: list[str]


@route
def index(state: State) -> Page:
    journal = ""
    for entry in state.entries:
        journal = journal + entry + "\n"
    return Page(state, [
        Header("Field Journal"),
        BulletedList(state.entries),
        "New entry:",
        TextBox("entry"),
        "\n",
        Button("Record", "record"),
        "\n",
        Download("Download the journal", "journal.txt", journal)
    ])


@route
def record(state: State, entry: str) -> Page:
    state.entries.append(entry)
    return index(state)


start_server(State(["Day 1: Captain ignored me."]))

The three arguments are the link text, the filename to save as, and the contents. Rebuild the contents every time the page renders, as this example does, so the download always reflects the current state.

Recipe: read a bundled data file

Plain open() works for files that ship with your app:

with open("questions.txt") as data_file:
    lines = data_file.readlines()

Keep the file next to your program. When you deploy, the file must be deployed too: upload it to the repository, and if the build needs to be told about it, the --additional-paths flag lists extra files to bundle (see Command line). The helper get_drafter_path("questions.txt") resolves a name to wherever the app's files actually live, which helps in the rare case where plain open cannot find a file you bundled.

Variations

  • If uploaded text may contain unusual characters, annotate the parameter as bytes and decode it deliberately, or accept the friendly error; see Uploaded file couldn't be read as text.
  • To offer structured data as a download, build CSV text with a loop (row = name + "," + str(score)) and give the file a .csv name so spreadsheet programs can open it.

Common problems

  • Reading before uploading: the route runs only when the button is pressed, and the parameter holds the file chosen then. Nothing arrives ahead of time.
  • str upload errors on a binary file: a file that is not text cannot arrive as str. Annotate the parameter as bytes, or steer the picker toward text files with accept=".txt".
  • A bundled file 404s after deploy: it never made it into the repository, or the name differs in case. See works locally, 404s deployed.
  • Nothing survives reload: files a visitor uploads live in state, and state resets on reload as it always does. The download button is how visitors keep a permanent copy.

Understand it

How Drafter works explains that the app runs entirely inside the browser tab, which is why files must be explicitly handed in and out.

See another example

The photo editor runs the whole upload-and-download loop for images.

Look it up

FileUpload, Download, and File types.

Fix a problem

Uploaded file couldn't be read as text and works locally, 404s deployed.