Automatic Download / Select and Download File with Single Button Click

Hello,

Is there way to automatically start a file download?

My use case is a form where a user selects options and clicks a “submit” button. The program creates a CSV based on the selected options and initiates a download. Seems like a very common application.

I’ve seen the great posts about creating a download link (e.g. https://discuss.streamlit.io/t/heres-a-download-function-that-works-for-dataframes-and-txt/4052). However, this creates a two-step process for the user (click to submit options, click to download). It also uses a hyperlink in HTML rather than a button object. I tried adding a second button on a page with a form, but there are conflicts and form widgets are cleared.

Has any created a one-step operation for this use case?

Thanks,

Ryan

1 Like

Hi @RyanMaley, welcome to Streamlit community!! :wave: :partying_face:

Thanks for describing your issue. I’ve extended the example you’ve linked and combined it with callbacks to create a one-step operation. A csv file download is triggered when the form submit button is pressed.The only limitation (due to my very limited knowledge of javascript) is that filenames are random.

In the below example, users provide a column name and entries. Clicking on the form submit button creates a dataframe with the user specified attributes and auto downloads the corresponding csv:

# uses https://discuss.streamlit.io/t/heres-a-download-function-that-works-for-dataframes-and-txt/4052
import streamlit as st
import streamlit.components.v1 as components
import pandas as pd
import base64
import os
import json
import pickle

def download_button(
    object_to_download, download_filename, button_text, pickle_it=False
):
    """
    Generates a link to download the given object_to_download.
    Params:
    ------
    object_to_download:  The object to be downloaded.
    download_filename (str): filename and extension of file. e.g. mydata.csv,
    some_txt_output.txt download_link_text (str): Text to display for download
    link.
    button_text (str): Text to display on download button (e.g. 'click here to download file')
    pickle_it (bool): If True, pickle file.
    Returns:
    -------
    (str): the anchor tag to download object_to_download
    Examples:
    --------
    download_link(your_df, 'YOUR_DF.csv', 'Click to download data!')
    download_link(your_str, 'YOUR_STRING.txt', 'Click to download text!')
    """
    if pickle_it:
        try:
            object_to_download = pickle.dumps(object_to_download)
        except pickle.PicklingError as e:
            st.write(e)
            return None

    else:
        if isinstance(object_to_download, bytes):
            pass

        elif isinstance(object_to_download, pd.DataFrame):
            object_to_download = object_to_download.to_csv(index=False)

        # Try JSON encode for everything else
        else:
            object_to_download = json.dumps(object_to_download)

    try:
        # some strings <-> bytes conversions necessary here
        b64 = base64.b64encode(object_to_download.encode()).decode()

    except AttributeError as e:
        b64 = base64.b64encode(object_to_download).decode()

    dl_link = f"""
        <html>
        <head>
        <title>Start Auto Download file</title>
        <script src="http://code.jquery.com/jquery-3.2.1.min.js"></script>
        <script>
        $(function() {{
        $('a[data-auto-download]').each(function(){{
        var $this = $(this);
        setTimeout(function() {{
        window.location = $this.attr('href');
        }}, 500);
        }});
        }});
        </script>
        </head>
        <body>
        <div class="wrapper">
        <a data-auto-download href="data:text/csv;base64,{b64}"></a>
        </div>
        </body>
        </html>"""

    return dl_link

def download_df():
    df = pd.DataFrame(st.session_state.col_values, columns=[st.session_state.col_name])
    filename = "my-dataframe.csv"
    components.html(
        download_button(
            df, filename, f"Click here to download {filename}", pickle_it=False
        ),
        height=0,
    )

with st.form("my_form", clear_on_submit=False):
    st.text_input("Column name", help="Name of column", key="col_name")
    st.multiselect(
        "Entries", options=["A", "B", "C"], help="Entries in column", key="col_values"
    )
    submit = st.form_submit_button("Download dataframe", on_click=download_df)

autodownload-csv

You’ll have to fix the JS in the dl_link object to specify a filename.
Source: html - How can I download a file automatically without click on button? - Stack Overflow

Happy Streamlit’ing! :balloon:
Snehan

2 Likes

Shehan (@snehankekre),

Thanks for this great bit of code. Works as advertised. I have zero knowledge of javascript so haven’t figured out how to name the download file. The HTML code would be something like:

<a data-auto-download href=“data:text/csv;base64,{b64}” download=“dataframe.csv”>

However, didn’t get this to work nor did my attempts to add a “download” attr. I’ll do some more research and if I get a solution, will post here.

Thanks again,

Ryan

1 Like

Hey @RyanMaley :wave:

I figured it out! I simplified the JS to a one liner based on your reply. You can find it on here or modify the code below:

import streamlit as st
import streamlit.components.v1 as components
import pandas as pd
import base64
import json


def download_button(object_to_download, download_filename):
    """
    Generates a link to download the given object_to_download.
    Params:
    ------
    object_to_download:  The object to be downloaded.
    download_filename (str): filename and extension of file. e.g. mydata.csv,
    Returns:
    -------
    (str): the anchor tag to download object_to_download
    """
    if isinstance(object_to_download, pd.DataFrame):
        object_to_download = object_to_download.to_csv(index=False)

    # Try JSON encode for everything else
    else:
        object_to_download = json.dumps(object_to_download)

    try:
        # some strings <-> bytes conversions necessary here
        b64 = base64.b64encode(object_to_download.encode()).decode()

    except AttributeError as e:
        b64 = base64.b64encode(object_to_download).decode()

    dl_link = f"""
    <html>
    <head>
    <title>Start Auto Download file</title>
    <script src="http://code.jquery.com/jquery-3.2.1.min.js"></script>
    <script>
    $('<a href="data:text/csv;base64,{b64}" download="{download_filename}">')[0].click()
    </script>
    </head>
    </html>
    """
    return dl_link


def download_df():
    df = pd.DataFrame(st.session_state.col_values, columns=[st.session_state.col_name])
    components.html(
        download_button(df, st.session_state.filename),
        height=0,
    )


with st.form("my_form", clear_on_submit=False):
    st.text_input("Column name", help="Name of column", key="col_name")
    st.multiselect(
        "Entries", options=["A", "B", "C"], help="Entries in column", key="col_values"
    )
    st.text_input("Filename (must include .csv)", key="filename")
    submit = st.form_submit_button("Download dataframe", on_click=download_df)

download-csv-compressed

On our roadmap, you’ll see that a Download button feature is in development :grinning_face_with_smiling_eyes: Stay tuned!

Happy Streamlit’ing! :balloon:
Snehan

3 Likes

Excellent, Snehan. You’ve saved me some time. And, I think this is a good feature for Streamlit.

Thanks for your responsiveness.

1 Like

Starting with v0.88, we now have st.download_button natively built into Streamlit. Check out the release notes and demo: 0.88.0 Release Notes

3 Likes

@snehankekre Can you please update your solution using st.download_button?

@Cerri The one-click auto-download solution via the form’s submit button doesn’t work with st.download_button.

@snehankekre I thought so. Thanks anyway!

Is it possible to have this solution but spawning an interaction window prompting the user to select the save directory/file ?