Also set caching headers for downloads to discourage client-side caching

At least if necessary (added new boolean flag to the constructor of
LargeResponseHandler for that). While at it also rename UrlForwardHandler
to UrlProxyHandler (it does not forward, it proxies) and make it aware of
ETag and Expires headers.
This commit is contained in:
Gina Häußge 2015-10-07 12:25:25 +02:00
parent 637adc3095
commit 9cf21aa036
2 changed files with 31 additions and 8 deletions

View file

@ -330,13 +330,29 @@ class Server():
upload_suffixes = dict(name=s.get(["server", "uploads", "nameSuffix"]), path=s.get(["server", "uploads", "pathSuffix"]))
download_handler_kwargs = dict(
as_attachment=True,
allow_client_caching=False
)
admin_validator = dict(access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))
no_hidden_files_validator = dict(path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))
def joined_dict(*dicts):
if not len(dicts):
return dict()
joined = dict()
for d in dicts:
joined.update(d)
return joined
server_routes = self._router.urls + [
# various downloads
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("timelapse"), as_attachment=True)),
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))),
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))),
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("timelapse")), download_handler_kwargs, no_hidden_files_validator)),
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("uploads")), download_handler_kwargs, no_hidden_files_validator)),
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, joined_dict(dict(path=s.getBaseFolder("logs")), download_handler_kwargs, admin_validator)),
# camera snapshot
(r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
(r"/downloads/camera/current", util.tornado.UrlProxyHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
# generated webassets
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=os.path.join(s.getBaseFolder("generated"), "webassets")))
]

View file

@ -768,6 +768,8 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
:class:``~tornado.web.StaticFileHandler`` as the ``default_filename`` keyword parameter). Defaults to ``None``.
as_attachment (bool): Whether to serve requested files with ``Content-Disposition: attachment`` header (``True``)
or not. Defaults to ``False``.
allow_client_caching (bool): Whether to allow the client to cache (by not setting any ``Cache-Control`` or
``Expires`` headers on the response) or not.
access_validation (function): Callback to call in the ``get`` method to validate access to the resource. Will
be called with ``self.request`` as parameter which contains the full tornado request object. Should raise
a ``tornado.web.HTTPError`` if access is not allowed in which case the request will not be further processed.
@ -782,10 +784,11 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
by ``get_content_version``.
"""
def initialize(self, path, default_filename=None, as_attachment=False,
def initialize(self, path, default_filename=None, as_attachment=False, allow_client_caching=True,
access_validation=None, path_validation=None, etag_generator=None):
tornado.web.StaticFileHandler.initialize(self, os.path.abspath(path), default_filename)
self._as_attachment = as_attachment
self._allow_client_caching = allow_client_caching
self._access_validation = access_validation
self._path_validation = path_validation
self._etag_generator = etag_generator
@ -802,6 +805,10 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
if self._as_attachment:
self.set_header("Content-Disposition", "attachment")
if not self._allow_client_caching:
self.set_header("Cache-Control", "max-age=0, must-revalidate, private")
self.set_header("Expires", "-1")
def compute_etag(self):
if self._etag_generator is not None:
return self._etag_generator(self)
@ -817,7 +824,7 @@ class LargeResponseHandler(tornado.web.StaticFileHandler):
##~~ URL Forward Handler for forwarding requests to a preconfigured static URL
class UrlForwardHandler(tornado.web.RequestHandler):
class UrlProxyHandler(tornado.web.RequestHandler):
"""
`tornado.web.RequestHandler <http://tornado.readthedocs.org/en/branch4.0/web.html#request-handlers>`_ that proxies
requests to a preconfigured url and returns the response. Allows delivery of the requested content as attachment
@ -827,7 +834,7 @@ class UrlForwardHandler(tornado.web.RequestHandler):
for making the request to the configured endpoint and return the body of the client response with the status code
from the client response and the following headers:
* ``Date``, ``Cache-Control``, ``Server``, ``Content-Type`` and ``Location`` will be copied over.
* ``Date``, ``Cache-Control``, ``Expires``, ``ETag``, ``Server``, ``Content-Type`` and ``Location`` will be copied over.
* If ``as_attachment`` is set to True, ``Content-Disposition`` will be set to ``attachment``. If ``basename`` is
set including the attachement's ``filename`` attribute will be set to the base name followed by the extension
guessed based on the MIME type from the ``Content-Type`` header of the response. If no extension can be guessed
@ -877,7 +884,7 @@ class UrlForwardHandler(tornado.web.RequestHandler):
filename = None
self.set_status(response.code)
for name in ("Date", "Cache-Control", "Server", "Content-Type", "Location"):
for name in ("Date", "Cache-Control", "Server", "Content-Type", "Location", "Expires", "ETag"):
value = response.headers.get(name)
if value:
self.set_header(name, value)