Solution for downloading a zipped folder/directory?

I’m looking to create a link to be able to download a zipped folder that already exists in the same directory as the dash application. I’ve seen solutions to download raw csv data (Download raw data), but am not quite sure how to change this to be able to download a .zip folder.

Here is the basic framework I’m trying to modify (from link above):

html.A(
        'Download Zipped Folder',
        id='download-link',
        download="folder.zip",
        href="",
        target="_blank"
    )

@app.callback(
    dash.dependencies.Output('download-link', 'href'),
    [dash.dependencies.Input('field-dropdown', 'value')])
def update_download_link(filter_value):
    dff = filter_data(filter_value)
    csv_string = dff.to_csv(index=False, encoding='utf-8')
    csv_string = "data:text/csv;charset=utf-8," + urllib.quote(csv_string)
    return csv_string

Thanks in advance!

What does the zip contain? One/many CSV files that you want to use in a dataframe?

The zip contains a variety of different files (.txt, .bedgraph, .bed) for the user to download after performing data analysis with the Dash App.

See Allowing users to download CSV on click for a more general solution for serving larger files and https://stackoverflow.com/a/27337047/4142536 to adapt that example to a zip file.

If you get this to work, please post your solution here too!

I was able to get this to work:

from flask import send_file
app = dash.Dash()

app.layout = html.Div([

	html.A(
        'Download Zip',
        id='download-zip',
        download = "example.zip",
        href="",
        target="_blank",
        n_clicks = 0
	    )
	])

@app.callback(
    Output('download-zip', 'href'),
    [Input('download-zip', 'n_clicks')])

def generate_report_url(n_clicks):

	return '/dash/urldownload'

@app.server.route('/dash/urldownload')

def generate_report_url():

	return send_file('example.zip', attachment_filename = 'example.zip', as_attachment = True)

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

I am dealing with similar problem.
Did someone find solution to this problem with the original framework (the one that in the post)?
Alternative, did someone try using the solution suggested above? because it doesn’t work for me.

Anyone? sorry for the bouncing

Here is the way I download my zip of excel files, with the zip file being created on-the-fly. In this example, the individual excel files reside in my AWS instance.

NOTE: This entire process must be achieved within 30 seconds if hosting on Heroku due to the web process time limit; unless you implement this asynchronously.

The href assoc. with the button to invoke the download looks similar to the following:

http://127.0.0.1:8050/download_zip/?filename=2020_06May_13May_Excel.zip&file_0=2020/May/06May_13May/20-05-06_Data.xlsx&file_1=2020/May/06May_13May/20-05-06_Data2.xlsx&file_2=2020/May/06May_13May/20-05-06_Data3.xlsx&num_files=3

Here is the code that handles the downloading of these files from my AWS instance, creating a zip to contain them, and then downloading to the user.

import flask
import io
import zipfile


@app.server.route('/download_zip/')
def download_zip_file():
    # Name of the zip file
    filename = flask.request.args.get('filename')
    # Num of files to process
    num_files = flask.request.args.get('num_files')

    # All files passed in via url string are of the format file_<filename>
    files = []
    for file in range(int(num_files)):
        files.append(flask.request.args.get('file_' + str(file)))

    # Loop thru files and add to zip
    zip_buffer = io.BytesIO()
    with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED, False) as zip_file:
        for file in files:
            # aws.download is an internal function to download the file from my AWS S3 instance
            file_obj = aws.download(file)
            zip_file.writestr(file.split('/')[-1], file_obj['Body'].read())

    return flask.Response(
        zip_buffer.getvalue(),
        mimetype='binary/octet-stream',
        headers={"Content-Disposition": "attachment;filename={}".format(filename)}
    )

Here is the aws.download() function:

import boto3

# Download a file from the bucket
def download(key):
    # Create connection to the bucket
    s3_resource = boto3.resource('s3')
    bucket = s3_resource.Bucket(S3_BUCKET)

    # Return file object
    return bucket.Object(key).get()

Hope this helps and you can adapt to your needs.

The above solutions did not work for me. But I finally found a way to do this using dash_core_components.Download and dash_html_components.Button along with the normal Dash callbacks

Here is a reproducible example that downloads a zip folder with some dummy csv files in it.

import dash
from dash.dependencies import Output, Input
import dash_html_components as html
import dash_core_components as dcc
import os
import zipfile
import tempfile

# helper function for closing temporary files
def close_tmp_file(tf):
    try:
        os.unlink(tf.name)
        tf.close()
    except:
        pass

# app layout
app = dash.Dash(prevent_initial_callbacks=True)

app.layout = html.Div(
    [
        html.Button("Download ZIP folder with dummy csv", id="btn_zip"),
        dcc.Download(id="download-dataframe-zip"),
    ]
)

# sample dataframes for zipping
import pandas as pd

df1 = pd.DataFrame({"a": [1, 2, 3, 4], "b": [2, 1, 5, 6], "c": ["x", "x", "y", "y"]})
df2 = pd.DataFrame({"d": [6, 2, 3, 9], "e": [3, 2, 7, 8], "f": ["t", "v", "s", "b"]})
df_dict = {"df1":df1,"df2":df2}

# app callbacks
@app.callback(

    Output("download-dataframe-zip", "data"),
    Input("btn_zip", "n_clicks"),
    prevent_initial_call=True,
)
def func(n_clicks):
    zip_dict = {}
    # add dataframes to zip file using temporary files
    for name,df in df_dict.items():
        df_temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.csv')
        df.to_csv(df_temp_file.name)
        df_temp_file.flush()
        zip_dict[name] = df_temp_file.name

    zip_tf = tempfile.NamedTemporaryFile(delete=False, suffix='.zip')
    zf = zipfile.ZipFile(zip_tf, mode='w', compression=zipfile.ZIP_DEFLATED)
    for name,fn in zip_dict.items():
        zf.write(fn,f"{name}.csv")

    

    # close uploaded temporary files
    zf.close()
    zip_tf.flush()
    zip_tf.seek(0)
    [close_tmp_file(_tf) for _tf in zip_dict]
    close_tmp_file(zip_tf)

    return dcc.send_file(zip_tf.name,filename="mydfs.zip")

if __name__ == "__main__":

    app.run_server(debug=True)