diff --git a/CHANGES.rst b/CHANGES.rst index 7d8369f7..3d198aec 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -10,15 +10,19 @@ Version 1.1 Unreleased - :meth:`flask.RequestContext.copy` includes the current session - object in the request context copy. This prevents ``flask.session`` - pointing to an out-of-date object. (`#2935`) -- Using built-in RequestContext, unprintable Unicode characters in Host - header will result in a HTTP 400 response and not HTTP 500 as previously. - (`#2994`) -- :func:`send_file` supports ``PathLike`` objects as describe in - PEP 0519, to support ``pathlib`` in Python 3. (`#3059`_) + object in the request context copy. This prevents ``session`` + pointing to an out-of-date object. (`#2935`_) +- Using built-in RequestContext, unprintable Unicode characters in + Host header will result in a HTTP 400 response and not HTTP 500 as + previously. (`#2994`_) +- :func:`send_file` supports :class:`~os.PathLike` objects as + described in PEP 0519, to support :mod:`pathlib` in Python 3. + (`#3059`_) +- :func:`send_file` supports :class:`~io.BytesIO` partial content. + (`#2957`_) .. _#2935: https://github.com/pallets/flask/issues/2935 +.. _#2957: https://github.com/pallets/flask/issues/2957 .. _#2994: https://github.com/pallets/flask/pull/2994 .. _#3059: https://github.com/pallets/flask/pull/3059 diff --git a/flask/helpers.py b/flask/helpers.py index 8a65c2e9..5afba8d5 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 @@ -511,7 +511,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, compatibility with WSGI servers. .. versionchanged:: 1.1 - Filenames may be a `PathLike` object. + Filename may be a :class:`~os.PathLike` object. + + .. versionadded:: 1.1 + Partial content supports :class:`~io.BytesIO`. :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` @@ -600,6 +603,13 @@ 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: + # Python 2 doesn't have getbuffer + 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..921a9942 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,19 @@ class TestSendfile(object): assert rv.status_code == 200 rv.close() + 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"