Add static_handler_class application setting, and move most of static_url

into the handler class where it can be overridden.
This commit is contained in:
Ben Darnell 2011-08-14 14:38:42 -07:00
parent c14931142a
commit 00bb10c4e1
3 changed files with 88 additions and 25 deletions

View File

@ -3,7 +3,7 @@ from tornado.iostream import IOStream
from tornado.template import DictLoader from tornado.template import DictLoader
from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase
from tornado.util import b, bytes_type from tornado.util import b, bytes_type
from tornado.web import RequestHandler, _O, authenticated, Application, asynchronous, url, HTTPError from tornado.web import RequestHandler, _O, authenticated, Application, asynchronous, url, HTTPError, StaticFileHandler
import binascii import binascii
import logging import logging
@ -519,7 +519,13 @@ class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
def get(self, path): def get(self, path):
self.write(self.static_url(path)) self.write(self.static_url(path))
return Application([('/static_url/(.*)', StaticUrlHandler)], class AbsoluteStaticUrlHandler(RequestHandler):
include_host = True
def get(self, path):
self.write(self.static_url(path))
return Application([('/static_url/(.*)', StaticUrlHandler),
('/abs_static_url/(.*)', AbsoluteStaticUrlHandler)],
static_path=os.path.join(os.path.dirname(__file__), 'static')) static_path=os.path.join(os.path.dirname(__file__), 'static'))
def test_static_files(self): def test_static_files(self):
@ -532,3 +538,35 @@ class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
def test_static_url(self): def test_static_url(self):
response = self.fetch("/static_url/robots.txt") response = self.fetch("/static_url/robots.txt")
self.assertEqual(response.body, b("/static/robots.txt?v=f71d2")) self.assertEqual(response.body, b("/static/robots.txt?v=f71d2"))
def test_absolute_static_url(self):
response = self.fetch("/abs_static_url/robots.txt")
self.assertEqual(response.body,
utf8(self.get_url("/") + "static/robots.txt?v=f71d2"))
class CustomStaticFileTest(AsyncHTTPTestCase, LogTrapTestCase):
def get_app(self):
class MyStaticFileHandler(StaticFileHandler):
def get(self, path):
assert path == "foo.txt"
self.write("bar")
@classmethod
def make_static_url(cls, settings, path):
return "/static/%s?v=42" % path
class StaticUrlHandler(RequestHandler):
def get(self, path):
self.write(self.static_url(path))
return Application([("/static_url/(.*)", StaticUrlHandler)],
static_path="dummy",
static_handler_class=MyStaticFileHandler)
def test_serve(self):
response = self.fetch("/static/foo.txt")
self.assertEqual(response.body, b("bar"))
def test_static_url(self):
response = self.fetch("/static_url/foo.txt")
self.assertEqual(response.body, b("/static/foo.txt?v=42"))

View File

@ -914,26 +914,13 @@ class RequestHandler(object):
path names. path names.
""" """
self.require_setting("static_path", "static_url") self.require_setting("static_path", "static_url")
if not hasattr(RequestHandler, "_static_hashes"): static_handler_class = self.settings.get(
RequestHandler._static_hashes = {} "static_handler_class", StaticFileHandler)
hashes = RequestHandler._static_hashes if getattr(self, "include_host", False):
abs_path = os.path.join(self.application.settings["static_path"], base = self.request.protocol + "://" + self.request.host
path)
if abs_path not in hashes:
try:
f = open(abs_path, "rb")
hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
f.close()
except Exception:
logging.error("Could not open static file %r", path)
hashes[abs_path] = None
base = self.request.protocol + "://" + self.request.host \
if getattr(self, "include_host", False) else ""
static_url_prefix = self.settings.get('static_url_prefix', '/static/')
if hashes.get(abs_path):
return base + static_url_prefix + path + "?v=" + hashes[abs_path][:5]
else: else:
return base + static_url_prefix + path base = ""
return base + static_handler_class.make_static_url(self.settings, path)
def async_callback(self, callback, *args, **kwargs): def async_callback(self, callback, *args, **kwargs):
"""Obsolete - catches exceptions from the wrapped function. """Obsolete - catches exceptions from the wrapped function.
@ -1156,7 +1143,9 @@ class Application(object):
Each tuple can contain an optional third element, which should be a Each tuple can contain an optional third element, which should be a
dictionary if it is present. That dictionary is passed as keyword dictionary if it is present. That dictionary is passed as keyword
arguments to the contructor of the handler. This pattern is used arguments to the contructor of the handler. This pattern is used
for the StaticFileHandler below:: for the StaticFileHandler below (note that a StaticFileHandler
can be installed automatically with the static_path setting described
below)::
application = web.Application([ application = web.Application([
(r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}), (r"/static/(.*)", web.StaticFileHandler, {"path": "/var/www"}),
@ -1173,6 +1162,8 @@ class Application(object):
keyword argument. We will serve those files from the /static/ URI keyword argument. We will serve those files from the /static/ URI
(this is configurable with the static_url_prefix setting), (this is configurable with the static_url_prefix setting),
and we will serve /favicon.ico and /robots.txt from the same directory. and we will serve /favicon.ico and /robots.txt from the same directory.
A custom subclass of StaticFileHandler can be specified with the
static_handler_class setting.
.. attribute:: settings .. attribute:: settings
@ -1206,11 +1197,13 @@ class Application(object):
handlers = list(handlers or []) handlers = list(handlers or [])
static_url_prefix = settings.get("static_url_prefix", static_url_prefix = settings.get("static_url_prefix",
"/static/") "/static/")
static_handler_class = settings.get("static_handler_class",
StaticFileHandler)
handlers = [ handlers = [
(re.escape(static_url_prefix) + r"(.*)", StaticFileHandler, (re.escape(static_url_prefix) + r"(.*)", static_handler_class,
dict(path=path)), dict(path=path)),
(r"/(favicon\.ico)", StaticFileHandler, dict(path=path)), (r"/(favicon\.ico)", static_handler_class, dict(path=path)),
(r"/(robots\.txt)", StaticFileHandler, dict(path=path)), (r"/(robots\.txt)", static_handler_class, dict(path=path)),
] + handlers ] + handlers
if handlers: self.add_handlers(".*$", handlers) if handlers: self.add_handlers(".*$", handlers)
@ -1469,6 +1462,8 @@ class StaticFileHandler(RequestHandler):
""" """
CACHE_MAX_AGE = 86400*365*10 #10 years CACHE_MAX_AGE = 86400*365*10 #10 years
_static_hashes = {}
def initialize(self, path, default_filename=None): def initialize(self, path, default_filename=None):
self.root = os.path.abspath(path) + os.path.sep self.root = os.path.abspath(path) + os.path.sep
self.default_filename = default_filename self.default_filename = default_filename
@ -1550,6 +1545,33 @@ class StaticFileHandler(RequestHandler):
""" """
return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0 return self.CACHE_MAX_AGE if "v" in self.request.arguments else 0
@classmethod
def make_static_url(cls, settings, path):
"""Constructs a versioned url for the given path.
This method may be overridden in subclasses (but note that it is
a class method rather than an instance method).
``settings`` is the `Application.settings` dictionary. ``path``
is the static path being requested. The url returned should be
relative to the current host.
"""
hashes = cls._static_hashes
abs_path = os.path.join(settings["static_path"], path)
if abs_path not in hashes:
try:
f = open(abs_path, "rb")
hashes[abs_path] = hashlib.md5(f.read()).hexdigest()
f.close()
except Exception:
logging.error("Could not open static file %r", path)
hashes[abs_path] = None
static_url_prefix = settings.get('static_url_prefix', '/static/')
if hashes.get(abs_path):
return static_url_prefix + path + "?v=" + hashes[abs_path][:5]
else:
return static_url_prefix + path
class FallbackHandler(RequestHandler): class FallbackHandler(RequestHandler):
"""A RequestHandler that wraps another HTTP server callback. """A RequestHandler that wraps another HTTP server callback.

View File

@ -71,6 +71,9 @@ New features
appear multiple times in the response. appear multiple times in the response.
* `IOLoop.add_timeout` now accepts `datetime.timedelta` objects in addition * `IOLoop.add_timeout` now accepts `datetime.timedelta` objects in addition
to absolute timestamps. to absolute timestamps.
* It is now possible to use a custom subclass of ``StaticFileHandler``
with the ``static_handler_class`` application setting, and this subclass
can override the behavior of the ``static_url`` method.
Bug fixes Bug fixes
~~~~~~~~~ ~~~~~~~~~