Browse Source

Expose send_file max-age as config value, #433.

Need to add the same hook in a Blueprint, but this is the first such
case where we need app.config in the Blueprint.
pull/440/merge
Ron DuPlain 13 years ago
parent
commit
d94efc6db6
  1. 12
      CHANGES
  2. 9
      docs/config.rst
  3. 7
      flask/app.py
  4. 14
      flask/helpers.py
  5. 14
      flask/testsuite/blueprints.py
  6. 13
      flask/testsuite/helpers.py

12
CHANGES

@ -48,10 +48,14 @@ Relase date to be decided, codename to be chosen.
- View functions can now return a tuple with the first instance being an - View functions can now return a tuple with the first instance being an
instance of :class:`flask.Response`. This allows for returning instance of :class:`flask.Response`. This allows for returning
``jsonify(error="error msg"), 400`` from a view function. ``jsonify(error="error msg"), 400`` from a view function.
- :class:`flask.Flask` now provides a `get_static_file_options` hook for - :class:`flask.Flask` now provides a `get_send_file_options` hook for
subclasses to override behavior of serving static files through Flask, subclasses to override behavior of serving static files from Flask when using
optionally by filename, which for example allows changing cache controls by :meth:`flask.Flask.send_static_file` based on keywords in
file extension. :func:`flask.helpers.send_file`. This hook is provided a filename, which for
example allows changing cache controls by file extension. The default
max-age for `send_static_file` can be configured through a new
``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, regardless of whether
the `get_send_file_options` hook is used.
Version 0.8.1 Version 0.8.1

9
docs/config.rst

@ -107,6 +107,13 @@ The following configuration values are used internally by Flask:
reject incoming requests with a reject incoming requests with a
content length greater than this by content length greater than this by
returning a 413 status code. returning a 413 status code.
``SEND_FILE_MAX_AGE_DEFAULT``: Default cache control max age to use with
:meth:`flask.Flask.send_static_file`, in
seconds. Override this value on a per-file
basis using the
:meth:`flask.Flask.get_send_file_options` and
:meth:`flask.Blueprint.get_send_file_options`
hooks. Defaults to 43200 (12 hours).
``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will ``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will
not execute the error handlers of HTTP not execute the error handlers of HTTP
exceptions but instead treat the exceptions but instead treat the
@ -267,7 +274,7 @@ configuration::
class ProductionConfig(Config): class ProductionConfig(Config):
DATABASE_URI = 'mysql://user@localhost/foo' DATABASE_URI = 'mysql://user@localhost/foo'
class DevelopmentConfig(Config): class DevelopmentConfig(Config):
DEBUG = True DEBUG = True

7
flask/app.py

@ -249,6 +249,7 @@ class Flask(_PackageBoundObject):
'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_SECURE': False, 'SESSION_COOKIE_SECURE': False,
'MAX_CONTENT_LENGTH': None, 'MAX_CONTENT_LENGTH': None,
'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours
'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_BAD_REQUEST_ERRORS': False,
'TRAP_HTTP_EXCEPTIONS': False 'TRAP_HTTP_EXCEPTIONS': False
}) })
@ -1020,6 +1021,12 @@ class Flask(_PackageBoundObject):
self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \
.append((code_or_exception, f)) .append((code_or_exception, f))
def get_send_file_options(self, filename):
# Override: Hooks in SEND_FILE_MAX_AGE_DEFAULT config.
options = super(Flask, self).get_send_file_options(filename)
options['cache_timeout'] = self.config['SEND_FILE_MAX_AGE_DEFAULT']
return options
@setupmethod @setupmethod
def template_filter(self, name=None): def template_filter(self, name=None):
"""A decorator that is used to register custom template filter. """A decorator that is used to register custom template filter.

14
flask/helpers.py

@ -319,6 +319,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
guessing requires a `filename` or an `attachment_filename` to be guessing requires a `filename` or an `attachment_filename` to be
provided. provided.
Note `get_send_file_options` in :class:`flask.Flask` hooks the
``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable to set the default
cache_timeout.
Please never pass filenames to this function from user sources without Please never pass filenames to this function from user sources without
checking them first. Something like this is usually sufficient to checking them first. Something like this is usually sufficient to
avoid security problems:: avoid security problems::
@ -652,7 +656,7 @@ class _PackageBoundObject(object):
return FileSystemLoader(os.path.join(self.root_path, return FileSystemLoader(os.path.join(self.root_path,
self.template_folder)) self.template_folder))
def get_static_file_options(self, filename): def get_send_file_options(self, filename):
"""Provides keyword arguments to send to :func:`send_from_directory`. """Provides keyword arguments to send to :func:`send_from_directory`.
This allows subclasses to change the behavior when sending files based This allows subclasses to change the behavior when sending files based
@ -660,14 +664,14 @@ class _PackageBoundObject(object):
to 60 seconds (note the options are keywords for :func:`send_file`):: to 60 seconds (note the options are keywords for :func:`send_file`)::
class MyFlask(flask.Flask): class MyFlask(flask.Flask):
def get_static_file_options(self, filename): def get_send_file_options(self, filename):
options = super(MyFlask, self).get_static_file_options(filename) options = super(MyFlask, self).get_send_file_options(filename)
if filename.lower().endswith('.js'): if filename.lower().endswith('.js'):
options['cache_timeout'] = 60 options['cache_timeout'] = 60
options['conditional'] = True options['conditional'] = True
return options return options
.. versionaded:: 0.9 .. versionadded:: 0.9
""" """
return {} return {}
@ -680,7 +684,7 @@ class _PackageBoundObject(object):
if not self.has_static_folder: if not self.has_static_folder:
raise RuntimeError('No static folder for this object') raise RuntimeError('No static folder for this object')
return send_from_directory(self.static_folder, filename, return send_from_directory(self.static_folder, filename,
**self.get_static_file_options(filename)) **self.get_send_file_options(filename))
def open_resource(self, resource, mode='rb'): def open_resource(self, resource, mode='rb'):
"""Opens a resource from the application's resource folder. To see """Opens a resource from the application's resource folder. To see

14
flask/testsuite/blueprints.py

@ -16,6 +16,7 @@ import unittest
import warnings import warnings
from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning
from werkzeug.exceptions import NotFound from werkzeug.exceptions import NotFound
from werkzeug.http import parse_cache_control_header
from jinja2 import TemplateNotFound from jinja2 import TemplateNotFound
@ -357,6 +358,19 @@ class BlueprintTestCase(FlaskTestCase):
rv = c.get('/admin/static/css/test.css') rv = c.get('/admin/static/css/test.css')
self.assert_equal(rv.data.strip(), '/* nested file */') self.assert_equal(rv.data.strip(), '/* nested file */')
# try/finally, in case other tests use this app for Blueprint tests.
max_age_default = app.config['SEND_FILE_MAX_AGE_DEFAULT']
try:
expected_max_age = 3600
if app.config['SEND_FILE_MAX_AGE_DEFAULT'] == expected_max_age:
expected_max_age = 7200
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = expected_max_age
rv = c.get('/admin/static/css/test.css')
cc = parse_cache_control_header(rv.headers['Cache-Control'])
self.assert_equal(cc.max_age, expected_max_age)
finally:
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default
with app.test_request_context(): with app.test_request_context():
self.assert_equal(flask.url_for('admin.static', filename='test.txt'), self.assert_equal(flask.url_for('admin.static', filename='test.txt'),
'/admin/static/test.txt') '/admin/static/test.txt')

13
flask/testsuite/helpers.py

@ -206,15 +206,20 @@ class SendfileTestCase(FlaskTestCase):
def test_static_file(self): def test_static_file(self):
app = flask.Flask(__name__) app = flask.Flask(__name__)
# default cache timeout is 12 hours (hard-coded) # default cache timeout is 12 hours
with app.test_request_context(): with app.test_request_context():
rv = app.send_static_file('index.html') rv = app.send_static_file('index.html')
cc = parse_cache_control_header(rv.headers['Cache-Control']) cc = parse_cache_control_header(rv.headers['Cache-Control'])
self.assert_equal(cc.max_age, 12 * 60 * 60) self.assert_equal(cc.max_age, 12 * 60 * 60)
# override get_static_file_options with some new values and check them app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3600
with app.test_request_context():
rv = app.send_static_file('index.html')
cc = parse_cache_control_header(rv.headers['Cache-Control'])
self.assert_equal(cc.max_age, 3600)
# override get_send_file_options with some new values and check them
class StaticFileApp(flask.Flask): class StaticFileApp(flask.Flask):
def get_static_file_options(self, filename): def get_send_file_options(self, filename):
opts = super(StaticFileApp, self).get_static_file_options(filename) opts = super(StaticFileApp, self).get_send_file_options(filename)
opts['cache_timeout'] = 10 opts['cache_timeout'] = 10
# this test catches explicit inclusion of the conditional # this test catches explicit inclusion of the conditional
# keyword arg in the guts # keyword arg in the guts

Loading…
Cancel
Save