Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Issue returning multiple downloads from one Flask route using MultipartEncoder

Tags:

python

flask

What I'm trying to do
I'm building a simple single-route Flask app that takes a value from a single-field form, creates multiple CSV files & automatically provides the files after the form is submitted.

Existing, Related Question
I stumbled upon the Download multiple CSVs using Flask? question that contains an answer that explains how to do exactly what I'm looking to do: return multiple downloads.

My Problem
I've implemented the MultipartEncoder from the requests_toolbelt as the answer shows, but when submitting the form it just downloads a single file (named after the route) with no extension instead of downloading all the files.

Things I tried to diagnose
If I open up the file in notepad++ I can see that all CSV files are included in the file separated by their Content-Type & Content-Disposition headers. So, the data is all present, but for some reason the files are not individually downloaded. That leads me to believe that my form is misconfigured or maybe I need to post to a different route.

What am I doing wrong? How can I accomplish downloading multiple files from a single route?

Minimal Working Example Code

app.py

from flask import Flask, Response, render_template, request
from requests_toolbelt import MultipartEncoder
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
from flask_bootstrap import Bootstrap
from flask_wtf import FlaskForm

app = Flask(__name__)
app.config['SECRET_KEY'] = 'n0T_a-R3a1_sEcR3t-KeY'
Bootstrap(app)

def build_files_for(term):
    # Create CSV files based on term
    # Return filenames
    return ['filename1.csv', 'filename2.csv', 'filename3.csv']

@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
def index():
    form = TermBuilderForm()
    if form.validate_on_submit():
        term_results = form.term.data
        downloads = build_files_for(term_results)
        me_dict = {}
        for i, download in enumerate(downloads, 1):
            me_dict['field' + str(i)] = (download, open(download, 'rb'), 'text/csv')
        m = MultipartEncoder(me_dict)
        return Response(m.to_string(), mimetype=m.content_type)
    return render_template('index.html', form=form)

class TermBuilderForm(FlaskForm):
    term = StringField('Term', validators=[DataRequired()], id='term')
    submit = SubmitField('Create')

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

index.html

{% extends 'bootstrap/base.html' %}
{% import 'bootstrap/wtf.html' as wtf %}

{% block title %}
    Term Builder
{% endblock %}

{% block scripts %}
    {{ super() }}
{% endblock %}
{% block content %}
    <div class="container" style="width:100%;padding-left:35px;">
        {% block app_content %}
        <h1>Term Builder</h1>
        {% if form %}
        <!--enctype="multipart/form-data"-->
        <form id="termbuilder" action="{{ url_for('index') }}" method="post" style="width:30%">
            <div style="display:none">{{ wtf.form_field(form.csrf_token) }}</div>
            <div class="row">
                {{ wtf.form_field(form.term) }}
            </div>
            <hr>
            <p>{{ wtf.form_field(form.submit) }}</p>
        </form>
        {% endif %}
        {% endblock %}
    </div>
{% endblock %}
like image 898
CaffeinatedMike Avatar asked Jan 19 '26 18:01

CaffeinatedMike


1 Answers

HTTP protocol was designed to send one file per one request.

it just downloads a single file (named after the route) with no extension instead of downloading all the files.

That's the default behaviour in browser, you can read about it here

The recommended way is to zip all the files and send it in one response.

One way to get the behaviour you are expecting:

In app.py return the list of files to be downloaded with any template (here index.html is used) and add a new route /files_download/<filename> to download files by filename

@app.route('/', methods=['GET', 'POST'])
@app.route('/index', methods=['GET', 'POST'])
def index():
    form = TermBuilderForm()
    if form.validate_on_submit():
        term_results = form.term.data
        downloads = build_files_for(term_results)
        return render_template('index.html', form=form, files=downloads)
    return render_template('index.html', form=form)

@app.route('/files_download/<filename>')
def files_download(filename):
    return send_file(filename, mimetype='text/csv')

In template where return render_template('index.html', form=form, files=downloads) (here index.html) add:

{% if files %}
<script>
    var urls = []
    {% for file in files %}
    urls.push("{{ url_for('files_download', filename=file) }}")
    {% endfor %}
    urls.forEach(url => {
        var iframe = document.createElement('iframe'); iframe.style.visibility = 'collapse';
        iframe.style.visibility = 'collapse';
        iframe.src = url;
        document.body.append(iframe);
        setTimeout(() => iframe.remove(), 2000);
    });
</script>
{% endif %}
like image 143
waynetech Avatar answered Jan 22 '26 09:01

waynetech