From d94efc6db63516b7f72e58c34ae33700f3d9c4fb Mon Sep 17 00:00:00 2001 From: Ron DuPlain Date: Tue, 13 Mar 2012 16:34:16 -0700 Subject: [PATCH] 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. --- CHANGES | 12 ++++++++---- docs/config.rst | 9 ++++++++- flask/app.py | 7 +++++++ flask/helpers.py | 14 +++++++++----- flask/testsuite/blueprints.py | 14 ++++++++++++++ flask/testsuite/helpers.py | 13 +++++++++---- 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/CHANGES b/CHANGES index 6b96be9e..ee029adc 100644 --- a/CHANGES +++ b/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 instance of :class:`flask.Response`. This allows for returning ``jsonify(error="error msg"), 400`` from a view function. -- :class:`flask.Flask` now provides a `get_static_file_options` hook for - subclasses to override behavior of serving static files through Flask, - optionally by filename, which for example allows changing cache controls by - file extension. +- :class:`flask.Flask` now provides a `get_send_file_options` hook for + subclasses to override behavior of serving static files from Flask when using + :meth:`flask.Flask.send_static_file` based on keywords in + :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 diff --git a/docs/config.rst b/docs/config.rst index 2f9d8307..cf3c6a4a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -107,6 +107,13 @@ The following configuration values are used internally by Flask: reject incoming requests with a content length greater than this by 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 not execute the error handlers of HTTP exceptions but instead treat the @@ -267,7 +274,7 @@ configuration:: class ProductionConfig(Config): DATABASE_URI = 'mysql://user@localhost/foo' - + class DevelopmentConfig(Config): DEBUG = True diff --git a/flask/app.py b/flask/app.py index f3d7efcb..0876ac64 100644 --- a/flask/app.py +++ b/flask/app.py @@ -249,6 +249,7 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, + 'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False }) @@ -1020,6 +1021,12 @@ class Flask(_PackageBoundObject): self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ .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 def template_filter(self, name=None): """A decorator that is used to register custom template filter. diff --git a/flask/helpers.py b/flask/helpers.py index 9964792b..52b0cebc 100644 --- a/flask/helpers.py +++ b/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 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 checking them first. Something like this is usually sufficient to avoid security problems:: @@ -652,7 +656,7 @@ class _PackageBoundObject(object): return FileSystemLoader(os.path.join(self.root_path, 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`. 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`):: class MyFlask(flask.Flask): - def get_static_file_options(self, filename): - options = super(MyFlask, self).get_static_file_options(filename) + def get_send_file_options(self, filename): + options = super(MyFlask, self).get_send_file_options(filename) if filename.lower().endswith('.js'): options['cache_timeout'] = 60 options['conditional'] = True return options - .. versionaded:: 0.9 + .. versionadded:: 0.9 """ return {} @@ -680,7 +684,7 @@ class _PackageBoundObject(object): if not self.has_static_folder: raise RuntimeError('No static folder for this object') 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'): """Opens a resource from the application's resource folder. To see diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 5bf81d92..5f3d3ab3 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -16,6 +16,7 @@ import unittest import warnings from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning from werkzeug.exceptions import NotFound +from werkzeug.http import parse_cache_control_header from jinja2 import TemplateNotFound @@ -357,6 +358,19 @@ class BlueprintTestCase(FlaskTestCase): rv = c.get('/admin/static/css/test.css') 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(): self.assert_equal(flask.url_for('admin.static', filename='test.txt'), '/admin/static/test.txt') diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 42331993..b4dd00ea 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -206,15 +206,20 @@ class SendfileTestCase(FlaskTestCase): def test_static_file(self): app = flask.Flask(__name__) - # default cache timeout is 12 hours (hard-coded) + # default cache timeout is 12 hours 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, 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): - def get_static_file_options(self, filename): - opts = super(StaticFileApp, self).get_static_file_options(filename) + def get_send_file_options(self, filename): + opts = super(StaticFileApp, self).get_send_file_options(filename) opts['cache_timeout'] = 10 # this test catches explicit inclusion of the conditional # keyword arg in the guts