clstaudt
clstaudt

Reputation: 22438

Downloading dynamically generated files from a Dash/Flask app

I tried to build a minimal example of a Dash app that illustrates the problem of dynamically generating a file that can then be downloaded via a download button.

If you run this example, you will see a text area where text can be entered. A click on the "enter" button will store the text to a file and create a download button for the file.

enter image description here

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

import uuid

stylesheets = [
    "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css", # Bulma
]

# create app
app = dash.Dash(
    __name__,
    external_stylesheets=stylesheets
)


app.layout = html.Div(
    className="section",
    children=[
        dcc.Textarea(
            id="text-area",
            className="textarea",
            placeholder='Enter a value...',
            style={'width': '300px'}
        ),
        html.Button(
            id="enter-button",
            className="button is-large is-outlined",
            children=["enter"]
        ),
        html.Div(
            id="download-area",
            className="block",
            children=[]
        )
    ]
)

def build_download_button(uri):
    """Generates a download button for the resource"""
    button = html.Form(
        action=uri,
        method="get",
        children=[
            html.Button(
                className="button",
                type="submit",
                children=[
                    "download"
                ]
            )
        ]
    )
    return button

@app.callback(
    Output("download-area", "children"),
    [
        Input("enter-button", "n_clicks")
    ],
    [
        State("text-area", "value")
    ]
)
def show_download_button(n_clicks, text):
    # turn text area content into file
    filename = f"{uuid.uuid1()}.txt"
    path = f"downloadable/{filename}"
    with open(path, "w") as file:
        file.write(text)
    uri = path
    return [build_download_button(uri)]


if __name__ == '__main__':
    app.run_server(debug=True)

However, the generated URI seems to be incorrect, because a click on the button just redirects to the index page. What would be needed to make it work?

Upvotes: 13

Views: 20134

Answers (3)

xhluca
xhluca

Reputation: 1036

With Dash 1.20.0, you now have a dcc.Download component for dynamic, user-based downloads. It doesn't require creating a custom button, uuid and flask.send_file.

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State
import uuid

stylesheets = [
    "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css", # Bulma
]

# create app
app = dash.Dash(
    __name__,
    external_stylesheets=stylesheets
)


app.layout = html.Div(
    className="section",
    children=[
        dcc.Textarea(
            id="text-area",
            className="textarea",
            placeholder='Enter a value...',
            style={'width': '300px'}
        ),
        html.Button("Enter", id="btn_txt"), 
        dcc.Download(id="download-text")
    ]
)

@app.callback(
    Output("download-text", "data"),
    Input("btn_txt", "n_clicks"),
    State("text-area", "value"),
    prevent_initial_call=True,
)
def create_download_file(n_clicks, text):
    filename = "file.txt"
    # Alternatively:
    # filename = f"{uuid.uuid1()}.txt"

    return dict(content=text, filename=filename)

Upvotes: 1

Lawliet
Lawliet

Reputation: 1592

Since Dash is built upon Flask, flask is not able to locate the URI for the text file that is generated.

The solution is to add the flask routes to redirect to download the resources, There is a simple example in the official plotly dash repository, https://github.com/plotly/dash-recipes/blob/master/dash-download-file-link-server.py

The modified code below solves your problem

import dash
import dash_core_components as dcc
import dash_html_components as html
from dash.dependencies import Input, Output, State

import uuid
import os
import flask
stylesheets = [
    "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css", # Bulma
]

# create app
app = dash.Dash(
    __name__,
    external_stylesheets=stylesheets
)


app.layout = html.Div(
    className="section",
    children=[
        dcc.Textarea(
            id="text-area",
            className="textarea",
            placeholder='Enter a value...',
            style={'width': '300px'}
        ),
        html.Button(
            id="enter-button",
            className="button is-large is-outlined",
            children=["enter"]
        ),
        html.Div(
            id="download-area",
            className="block",
            children=[]
        )
    ]
)

def build_download_button(uri):
    """Generates a download button for the resource"""
    button = html.Form(
        action=uri,
        method="get",
        children=[
            html.Button(
                className="button",
                type="submit",
                children=[
                    "download"
                ]
            )
        ]
    )
    return button

@app.callback(
    Output("download-area", "children"),
    [
        Input("enter-button", "n_clicks")
    ],
    [
        State("text-area", "value")
    ]
)
def show_download_button(n_clicks, text):
    if text == None:
        return
    # turn text area content into file
    filename = f"{uuid.uuid1()}.txt"
    path = f"downloadable/{filename}"
    with open(path, "w") as file:
        file.write(text)
    uri = path
    return [build_download_button(uri)]

@app.server.route('/downloadable/<path:path>')
def serve_static(path):
    root_dir = os.getcwd()
    return flask.send_from_directory(
        os.path.join(root_dir, 'downloadable'), path
    )

if __name__ == '__main__':
    app.run_server(debug=True)

Alternatively, you can use the static directory instead of the downloadable directory, It will work as well.

More information on flask static directory: http://flask.pocoo.org/docs/1.0/tutorial/static/

Here is the snippet,

#your code

def show_download_button(n_clicks, text):
    if text == None:
        return
    filename = f"{uuid.uuid1()}.txt"
    path = f"static/{filename}"      # =====> here change the name of the direcotry to point to the static directory
    with open(path, "w") as file:
        file.write(text)
    uri = path
    return [build_download_button(uri)]

#your code

Upvotes: 14

olinox14
olinox14

Reputation: 6653

Solution here:

import uuid

import dash
from dash.dependencies import Input, Output, State
import flask
from flask.helpers import send_file

import dash_core_components as dcc
import dash_html_components as html

stylesheets = [
    "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css", # Bulma
]

server = flask.Flask('app')

# create app
app = dash.Dash(
    __name__,
    external_stylesheets=stylesheets, 
    server=server                       # <-- do not forget this line
)

# (...) your code here

@server.route("/downloadable/<path>")
def download_file (path = None):
    return send_file("downloadable/" + path, as_attachment=True)

Upvotes: 2

Related Questions