diff --git a/flask/helpers.py b/flask/helpers.py index 8a65c2e9..6211b4aa 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -8,7 +8,7 @@ :copyright: © 2010 by the Pallets team. :license: BSD, see LICENSE for more details. """ - +import io import os import socket import sys @@ -512,6 +512,8 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionchanged:: 1.1 Filenames may be a `PathLike` object. + .. versionadded:: 1.1 + Partial content supports ``BytesIO``. :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` @@ -600,6 +602,12 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, mtime = os.path.getmtime(filename) fsize = os.path.getsize(filename) headers['Content-Length'] = fsize + elif isinstance(file, io.BytesIO): + try: + fsize = file.getbuffer().nbytes + except AttributeError: + fsize = len(file.getvalue()) + headers['Content-Length'] = fsize data = wrap_file(request.environ, file) rv = current_app.response_class(data, mimetype=mimetype, headers=headers, diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e36e0e64..70b7349c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -10,6 +10,7 @@ """ import datetime +import io import os import uuid @@ -608,6 +609,23 @@ class TestSendfile(object): assert rv.status_code == 200 rv.close() + @pytest.mark.skipif( + not callable(getattr(Range, 'to_content_range_header', None)), + reason="not implemented within werkzeug" + ) + def test_send_file_range_request_bytesio(self, app, client): + @app.route('/') + def index(): + file = io.BytesIO(b'somethingsomething') + return flask.send_file( + file, attachment_filename='filename', conditional=True + ) + + rv = client.get('/', headers={'Range': 'bytes=4-15'}) + assert rv.status_code == 206 + assert rv.data == b'somethingsomething'[4:16] + rv.close() + @pytest.mark.skipif( not callable(getattr(Range, 'to_content_range_header', None)), reason="not implemented within werkzeug"