diff --git a/tornado/test/web_test.py b/tornado/test/web_test.py index 08f4712d..6c60ab41 100644 --- a/tornado/test/web_test.py +++ b/tornado/test/web_test.py @@ -3,7 +3,7 @@ from tornado.iostream import IOStream from tornado.template import DictLoader from tornado.testing import LogTrapTestCase, AsyncHTTPTestCase 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 logging @@ -519,7 +519,13 @@ class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase): def get(self, 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')) def test_static_files(self): @@ -532,3 +538,35 @@ class StaticFileTest(AsyncHTTPTestCase, LogTrapTestCase): def test_static_url(self): response = self.fetch("/static_url/robots.txt") 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")) diff --git a/tornado/web.py b/tornado/web.py index eb344158..384b26ee 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -914,26 +914,13 @@ class RequestHandler(object): path names. """ self.require_setting("static_path", "static_url") - if not hasattr(RequestHandler, "_static_hashes"): - RequestHandler._static_hashes = {} - hashes = RequestHandler._static_hashes - abs_path = os.path.join(self.application.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 - 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] + static_handler_class = self.settings.get( + "static_handler_class", StaticFileHandler) + if getattr(self, "include_host", False): + base = self.request.protocol + "://" + self.request.host 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): """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 dictionary if it is present. That dictionary is passed as keyword 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([ (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 (this is configurable with the static_url_prefix setting), 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 @@ -1206,11 +1197,13 @@ class Application(object): handlers = list(handlers or []) static_url_prefix = settings.get("static_url_prefix", "/static/") + static_handler_class = settings.get("static_handler_class", + StaticFileHandler) handlers = [ - (re.escape(static_url_prefix) + r"(.*)", StaticFileHandler, + (re.escape(static_url_prefix) + r"(.*)", static_handler_class, dict(path=path)), - (r"/(favicon\.ico)", StaticFileHandler, dict(path=path)), - (r"/(robots\.txt)", StaticFileHandler, dict(path=path)), + (r"/(favicon\.ico)", static_handler_class, dict(path=path)), + (r"/(robots\.txt)", static_handler_class, dict(path=path)), ] + handlers if handlers: self.add_handlers(".*$", handlers) @@ -1469,6 +1462,8 @@ class StaticFileHandler(RequestHandler): """ CACHE_MAX_AGE = 86400*365*10 #10 years + _static_hashes = {} + def initialize(self, path, default_filename=None): self.root = os.path.abspath(path) + os.path.sep 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 + @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): """A RequestHandler that wraps another HTTP server callback. diff --git a/website/sphinx/releases/next.rst b/website/sphinx/releases/next.rst index 5df4f3ba..ef2e8c4f 100644 --- a/website/sphinx/releases/next.rst +++ b/website/sphinx/releases/next.rst @@ -71,6 +71,9 @@ New features appear multiple times in the response. * `IOLoop.add_timeout` now accepts `datetime.timedelta` objects in addition 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 ~~~~~~~~~