I have an MWE of the problem in the following html template:
<block body>
<h2> Test Report </h2>
<form method="post">
<input type="submit" value="Generate Report">
</form>
</block body>
With the associated flask blueprint:
@bp.route('/reports2', methods=('GET', 'POST'))
@login_required
def reports2():
if request.method == 'POST':
from io import BytesIO
byte_obj = BytesIO()
byte_obj.write(b'Hello, I am a test file')
return send_file(byte_obj, as_attachment=True, attachment_filename='test-downloaded-report.txt', mimetype='text/plain')
return render_template('report/reports2.html')
The result is bewildering. There's no file created in my downloads directory called 'test-downloaded-report.txt'. Instead, it downloads reports2.html!
I also originally just had send_file without being returned, instead returning a redirect to reports2.html so that the form would reset. Nothing happened there, so I assume the return is necessary for anything to happen, but as an aside I also don't see how I should send a file for download and redirect to a different page.
Any idea why that file is getting sent, instead of the file I created?
CodePudding user response:
The file is not served as expected because the pointer is at the end of the stream. By putting the pointer at the beginning, the data can be read completely and should download as expected.
@bp.route('/reports2', methods=('GET', 'POST'))
@login_required
def reports2():
if request.method == 'POST':
from io import BytesIO
byte_obj = BytesIO()
byte_obj.write(b'Hello, I am a test file')
byte_obj.seek(0)
return send_file(
byte_obj,
as_attachment=True,
attachment_filename='test-downloaded-report.txt',
mimetype='text/plain'
)
return render_template('report/reports2.html')
Forwarding after downloading or resetting the form is a little more complicated. As you correctly noticed, send_file
is a response from the server. It cannot be used in conjunction with a redirect
.
However, the following suggestion shows you how to use JavaScript to send the form, download the file and then reset the form. With a little adjustment you should also be able to forward to another location.
from flask import (
Flask,
render_template,
request,
send_file
)
app = Flask(__name__)
@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'POST':
from io import BytesIO
byte_obj = BytesIO()
byte_obj.write(b'Hello, I am a test file')
byte_obj.seek(0)
return send_file(
byte_obj,
as_attachment=True,
attachment_filename='test-downloaded-report.txt',
mimetype='text/plain'
)
return render_template('index.html')
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Generate Report</title>
</head>
<body>
<h2>Test Report</h2>
<form name="my-form" action="{{ url_for('index') }}" method="post">
<input type="submit" value="Generate Report">
</form>
<script type="text/javascript">
(function() {
const download = (url, form, callback) => {
fetch(url, {
method: 'post',
body: new FormData(form)
}).then(resp => {
resp.ok && resp.blob().then(blob => {
callback(blob, resp.headers);
});
});
};
const save = (data, filename) => {
const elem = document.createElement('a');
elem.href = URL.createObjectURL(data);
elem.download = filename;
elem.click();
};
const form = document.querySelector('form[name="my-form"]');
form.addEventListener('submit', evt => {
evt.preventDefault();
download(evt.target.action, evt.target, (data, headers) => {
// Extract the filename from the "Content-Disposition" header.
const disposition = headers.get('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) {
const filename = matches[1].replace(/['"]/g, '');
save(data, filename);
}
}
});
// Reset the form or redirect to another location.
evt.target.reset();
})
})();
</script>
</body>
</html>