Browse Source

make use of range requests if available in werkzeug (#2031)

* make use of range requests if available in werkzeug

* different logic for testing werkzeug functionality
pull/2046/head
Joël Charles 8 years ago committed by Markus Unterwaditzer
parent
commit
7186a5aaf5
  1. 1
      CHANGES
  2. 36
      flask/helpers.py
  3. 65
      tests/test_helpers.py

1
CHANGES

@ -18,6 +18,7 @@ Version 0.12
- Correctly invoke response handlers for both regular request dispatching as - Correctly invoke response handlers for both regular request dispatching as
well as error handlers. well as error handlers.
- Disable logger propagation by default for the app logger. - Disable logger propagation by default for the app logger.
- Add support for range requests in ``send_file``.
Version 0.11.2 Version 0.11.2
-------------- --------------

36
flask/helpers.py

@ -25,8 +25,9 @@ try:
except ImportError: except ImportError:
from urlparse import quote as url_quote from urlparse import quote as url_quote
from werkzeug.datastructures import Headers from werkzeug.datastructures import Headers, Range
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound, \
RequestedRangeNotSatisfiable
# this was moved in 0.7 # this was moved in 0.7
try: try:
@ -446,6 +447,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
ETags will also be attached automatically if a `filename` is provided. You ETags will also be attached automatically if a `filename` is provided. You
can turn this off by setting `add_etags=False`. can turn this off by setting `add_etags=False`.
If `conditional=True` and `filename` is provided, this method will try to
upgrade the response stream to support range requests. This will allow
the request to be answered with partial content response.
Please never pass filenames to this function from user sources; Please never pass filenames to this function from user sources;
you should use :func:`send_from_directory` instead. you should use :func:`send_from_directory` instead.
@ -500,6 +505,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
If a file was passed, this overrides its mtime. If a file was passed, this overrides its mtime.
""" """
mtime = None mtime = None
fsize = None
if isinstance(filename_or_fp, string_types): if isinstance(filename_or_fp, string_types):
filename = filename_or_fp filename = filename_or_fp
if not os.path.isabs(filename): if not os.path.isabs(filename):
@ -535,13 +541,15 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
if file is not None: if file is not None:
file.close() file.close()
headers['X-Sendfile'] = filename headers['X-Sendfile'] = filename
headers['Content-Length'] = os.path.getsize(filename) fsize = os.path.getsize(filename)
headers['Content-Length'] = fsize
data = None data = None
else: else:
if file is None: if file is None:
file = open(filename, 'rb') file = open(filename, 'rb')
mtime = os.path.getmtime(filename) mtime = os.path.getmtime(filename)
headers['Content-Length'] = os.path.getsize(filename) fsize = os.path.getsize(filename)
headers['Content-Length'] = fsize
data = wrap_file(request.environ, file) data = wrap_file(request.environ, file)
rv = current_app.response_class(data, mimetype=mimetype, headers=headers, rv = current_app.response_class(data, mimetype=mimetype, headers=headers,
@ -575,12 +583,22 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
warn('Access %s failed, maybe it does not exist, so ignore etags in ' warn('Access %s failed, maybe it does not exist, so ignore etags in '
'headers' % filename, stacklevel=2) 'headers' % filename, stacklevel=2)
if conditional: if conditional:
if callable(getattr(Range, 'to_content_range_header', None)):
# Werkzeug supports Range Requests
# Remove this test when support for Werkzeug <0.12 is dropped
try:
rv = rv.make_conditional(request, accept_ranges=True,
complete_length=fsize)
except RequestedRangeNotSatisfiable:
file.close()
raise
else:
rv = rv.make_conditional(request) rv = rv.make_conditional(request)
# make sure we don't send x-sendfile for servers that # make sure we don't send x-sendfile for servers that
# ignore the 304 status code for x-sendfile. # ignore the 304 status code for x-sendfile.
if rv.status_code == 304: if rv.status_code == 304:
rv.headers.pop('x-sendfile', None) rv.headers.pop('x-sendfile', None)
return rv return rv

65
tests/test_helpers.py

@ -14,8 +14,10 @@ import pytest
import os import os
import uuid import uuid
import datetime import datetime
import flask import flask
from logging import StreamHandler from logging import StreamHandler
from werkzeug.datastructures import Range
from werkzeug.exceptions import BadRequest, NotFound from werkzeug.exceptions import BadRequest, NotFound
from werkzeug.http import parse_cache_control_header, parse_options_header from werkzeug.http import parse_cache_control_header, parse_options_header
from werkzeug.http import http_date from werkzeug.http import http_date
@ -462,6 +464,69 @@ class TestSendfile(object):
assert 'x-sendfile' not in rv.headers assert 'x-sendfile' not in rv.headers
rv.close() rv.close()
@pytest.mark.skipif(
not callable(getattr(Range, 'to_content_range_header', None)),
reason="not implement within werkzeug"
)
def test_send_file_range_request(self):
app = flask.Flask(__name__)
@app.route('/')
def index():
return flask.send_file('static/index.html', conditional=True)
c = app.test_client()
rv = c.get('/', headers={'Range': 'bytes=4-15'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[4:16]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=4-'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[4:]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=4-1000'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[4:]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=-10'})
assert rv.status_code == 206
with app.open_resource('static/index.html') as f:
assert rv.data == f.read()[-10:]
rv.close()
rv = c.get('/', headers={'Range': 'bytes=1000-'})
assert rv.status_code == 416
rv.close()
rv = c.get('/', headers={'Range': 'bytes=-'})
assert rv.status_code == 416
rv.close()
rv = c.get('/', headers={'Range': 'somethingsomething'})
assert rv.status_code == 416
rv.close()
last_modified = datetime.datetime.fromtimestamp(os.path.getmtime(
os.path.join(app.root_path, 'static/index.html'))).replace(
microsecond=0)
rv = c.get('/', headers={'Range': 'bytes=4-15',
'If-Range': http_date(last_modified)})
assert rv.status_code == 206
rv.close()
rv = c.get('/', headers={'Range': 'bytes=4-15', 'If-Range': http_date(
datetime.datetime(1999, 1, 1))})
assert rv.status_code == 200
rv.close()
def test_attachment(self): def test_attachment(self):
app = flask.Flask(__name__) app = flask.Flask(__name__)
with app.test_request_context(): with app.test_request_context():

Loading…
Cancel
Save