Sam Hooke

NiceGUI: File upload and download

If your NiceGUI application has some sort of state or configuration, it can be useful to provide a way to save and load that state. We can do that through serializing the state into JSON, and providing a way to download and upload that JSON file. Following is a simple example.

Download state §

To start with, let’s just implement download.

State §

In one file, create a class to contain our state, and serialize it as JSON with a to_json method.

my_state.py §
class MyState:
    def __init__(self):
        self.foo = 3
        self.bar = "hello"

    def to_json(self):
        return {
            "foo": self.foo,
            "bar": self.bar,
        }

GUI pages §

In another file, create two GUI pages:

  • An implicit1 / index page (the “auto-index page”):
    • This provides two fields for modifying the state of “Foo” and “Bar”. The bind_value means that the global CONFIG object is updated when these GUI fields are modified.
    • It also provides a link to the config.json page. Crucially, this uses .props("download=config.json") so that when the link is clicked the browser downloads the file, rather than opening it in a new tab2.
  • An explicit /config.json page which downloads the state as JSON.
my_gui.py §
from nicegui import app, ui
from my_state import MyState

CONFIG = MyState()

ui.number(label="Foo").bind_value(CONFIG, "foo")
ui.input(label="Bar").bind_value(CONFIG, "bar")
ui.link("Download config", target="/config.json").props("download=config.json")

@app.get("/config.json")
def gui_state_download():
    global CONFIG
    return CONFIG.to_json()

ui.run()

When we run python my_gui.py, NiceGUI will launch the GUI in our browser at <url>, which defaults to http://127.0.0.1:8080.

We can then open each page in a new tab:

<url>/
This displays the “Foo” and “Bar” input fields, which let us alter the state.
<url>/config.json
This displays a JSON file with the content {"foo":3.0,"bar":"hello"}. To pretty print the JSON, see these notes.

If we change the values in the “Foo” and “Bar” fields, then refresh the config.json page, we see the values update. This works because “[the] auto-index page is created once on startup and shared across all clients that might connect”3. This approach may get a little hairy if you have many clients using the GUI, since they all have read and write access to the same CONFIG object, but for a small application with one client it is Good Enough™.

For example, if we set “Foo” to 4 in the GUI, then refresh the config.json page, it will display {"foo":4.0,"bar":"hello"}.

Upload state §

To support upload, we need a way to populate our MyState object from JSON.

State §

Let’s add a from_json method:

my_state.py (updated) §
import json

class MyState:
    def __init__(self):
        self.foo = 3
        self.bar = "hello"

    def to_json(self):
        return {
            "foo": self.foo,
            "bar": self.bar,
        }

    def from_json(self, text):
        # NOTE: Error handling is omitted for simplicity.
        json_dict = json.loads(text)
        self.foo = json_dict["foo"]
        self.bar = json_dict["bar"]

GUI pages §

To upload we will use the ui.upload element4. When the upload completes, it calls the new state_uploaded function, which updates the config using CONFIG.from_json:

my_gui.py (updated) §
from nicegui import app, ui, events
from my_state import MyState

CONFIG = MyState()

def state_uploaded(e: events.UploadEventArguments):
    text = e.content.read().decode("utf-8")
    global CONFIG
    CONFIG.from_json(text)
    ui.notify(f"Loaded config: {e.name}")

ui.number(label="Foo").bind_value(CONFIG, "foo")
ui.input(label="Bar").bind_value(CONFIG, "bar")
ui.link("Download config", target="/config.json").props("download=config.json")
ui.upload(
    label="Upload state",
    on_upload=state_uploaded,
    max_files=1,
).props("accept=.json")

@app.get("/config.json")
def gui_state_download():
    global CONFIG
    return CONFIG.to_json()

ui.run()

We set max_files=1 because by default the ui.upload element accepts multiple files.

Testing §

To test, create a JSON file:

example.json §
{
  "foo": 123,
  "bar": "world"
}

Then run the GUI, click the plus icon to open the file picker, choose the example.json file, then click the upload icon to perform the upload. Once uploaded (which should be nearly instantaneous for such a small file), the GUI will update with the values from the JSON file, and the notification “Loaded config: example.json” will show at the bottom of the screen.

Alternative approaches §

If you are happy to let the browser manage the application state, and do not need to download it as JSON, then NiceGUI offers some built-in storage methods that use session cookies.


  1. To create an explicit index page, see these notes↩︎

  2. I looked at the documentation and source code to see if there is any way to specify the download property inside the ui.link arguments, but there is not. Instead you have to chain it with .props("download=<filename>"), which adds the download property to the HTML a element, telling the browser to download the file rather than open it in a new tab. Use <filename> to specify the name the browser should give to the downloaded file. ↩︎

  3. Quote from the NiceGUI documentation on “Auto-index page” found here↩︎

  4. For an even simpler upload example, see the official NiceGUI docs↩︎

See all notes.

← Previous Note: NiceGUI: Show a confirmation popup
Next Note: Django REST framework performance (part 1: profiling) →