static-file: properly set/use cache header for static file (closes #2255013)

This changesets enables the standard http cache mechanism where the static controller may reply "304 Not modified" based on last-modified in HTTP response and if-modified-since in HTTP query. The last modified time is computed using the file-system information.

The pre-existing logic using an Expires header to prevent client from sending request stay in place. The new logic just prevents sending the file again if not necessary.

authorPierre-Yves David <pierre-yves.david@logilab.fr>
changesetfe60a77ae4a7
branchdefault
phasepublic
hiddenno
parent revision#cb838b126b07 [repository] move task manager instantiation outside repository.
child revision#75694a61f089 [login redirect] only add postlogin_path argument when meaningful
files modified by this revision
web/test/unittest_views_staticcontrollers.py
web/views/staticcontrollers.py
# HG changeset patch
# User Pierre-Yves David <pierre-yves.david@logilab.fr>
# Date 1332164263 -3600
# Mon Mar 19 14:37:43 2012 +0100
# Node ID fe60a77ae4a7c5b2af92dddbd1610d7ab07afbd5
# Parent cb838b126b0786b82da9fbeaf69683d63346c257
static-file: properly set/use cache header for static file (closes #2255013)

This changesets enables the standard http cache mechanism where the static
controller may reply "304 Not modified" based on `last-modified` in HTTP
response and `if-modified-since` in HTTP query. The last modified time is
computed using the file-system information.

The pre-existing logic using an `Expires` header to prevent client from sending
request stay in place. The new logic just prevents sending the file again if not
necessary.

diff --git a/web/test/unittest_views_staticcontrollers.py b/web/test/unittest_views_staticcontrollers.py
@@ -1,19 +1,43 @@
1  from __future__ import with_statement
2 
3 +from logilab.common.testlib import tag, Tags
4  from cubicweb.devtools.testlib import CubicWebTC
5 
6  import os
7  import os.path as osp
8  import glob
9 
10  from cubicweb.utils import HTMLHead
11  from cubicweb.web import StatusResponse
12  from cubicweb.web.views.staticcontrollers import ConcatFilesHandler
13 
14 +class StaticControllerCacheTC(CubicWebTC):
15 +
16 +    tags = CubicWebTC.tags | Tags('static_controller', 'cache', 'http')
17 +
18 +
19 +    def _publish_static_files(self, url, header={}):
20 +        req = self.request(headers=header)
21 +        req._url = url
22 +        return self.app_handle_request(req, url), req
23 +
24 +    def test_static_file_are_cached(self):
25 +        _, req = self._publish_static_files('data/cubicweb.css')
26 +        self.assertEqual(200, req.status_out)
27 +        self.assertIn('last-modified', req.headers_out)
28 +        next_headers = {
29 +            'if-modified-since': req.get_response_header('last-modified', raw=True),
30 +        }
31 +        _, req = self._publish_static_files('data/cubicweb.css', next_headers)
32 +        self.assertEqual(304, req.status_out)
33 +
34 +
35  class ConcatFilesTC(CubicWebTC):
36 
37 +    tags = CubicWebTC.tags | Tags('static_controller', 'concat')
38 +
39      def tearDown(self):
40          super(ConcatFilesTC, self).tearDown()
41          self._cleanup_concat_cache()
42 
43      def _cleanup_concat_cache(self):
diff --git a/web/views/staticcontrollers.py b/web/views/staticcontrollers.py
@@ -65,10 +65,20 @@
44              # XXX: Don't provide additional resource information to error responses
45              #
46              # the HTTP RFC recommands not going further than 1 year ahead
47              expires = datetime.now() + timedelta(days=6*30)
48              self._cw.set_header('Expires', generateDateTime(mktime(expires.timetuple())))
49 +
50 +        # XXX system call to os.stats could be cached once and for all in
51 +        # production mode (where static files are not expected to change)
52 +        #
53 +        # Note that: we do a osp.isdir + osp.isfile before and a potential
54 +        # os.read after. Improving this specific call will not help
55 +        #
56 +        # Real production environment should use dedicated static file serving.
57 +        self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime))
58 +        self._cw.validate_cache()
59          # XXX elif uri.startswith('/https/'): uri = uri[6:]
60          mimetype, encoding = mimetypes.guess_type(path)
61          self._cw.set_content_type(mimetype, osp.basename(path), encoding)
62          return file(path).read()
63