static-file: properly set/use cache header for static file (closes #2255013)
authorPierre-Yves David <pierre-yves.david@logilab.fr>
Mon, 19 Mar 2012 14:37:43 +0100
changeset 8323 fe60a77ae4a7
parent 8322 cb838b126b07
child 8324 75694a61f089
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.
web/test/unittest_views_staticcontrollers.py
web/views/staticcontrollers.py
--- a/web/test/unittest_views_staticcontrollers.py	Tue Mar 20 18:25:06 2012 +0100
+++ b/web/test/unittest_views_staticcontrollers.py	Mon Mar 19 14:37:43 2012 +0100
@@ -1,5 +1,6 @@
 from __future__ import with_statement
 
+from logilab.common.testlib import tag, Tags
 from cubicweb.devtools.testlib import CubicWebTC
 
 import os
@@ -10,8 +11,31 @@
 from cubicweb.web import StatusResponse
 from cubicweb.web.views.staticcontrollers import ConcatFilesHandler
 
+class StaticControllerCacheTC(CubicWebTC):
+
+    tags = CubicWebTC.tags | Tags('static_controller', 'cache', 'http')
+
+
+    def _publish_static_files(self, url, header={}):
+        req = self.request(headers=header)
+        req._url = url
+        return self.app_handle_request(req, url), req
+
+    def test_static_file_are_cached(self):
+        _, req = self._publish_static_files('data/cubicweb.css')
+        self.assertEqual(200, req.status_out)
+        self.assertIn('last-modified', req.headers_out)
+        next_headers = {
+            'if-modified-since': req.get_response_header('last-modified', raw=True),
+        }
+        _, req = self._publish_static_files('data/cubicweb.css', next_headers)
+        self.assertEqual(304, req.status_out)
+
+
 class ConcatFilesTC(CubicWebTC):
 
+    tags = CubicWebTC.tags | Tags('static_controller', 'concat')
+
     def tearDown(self):
         super(ConcatFilesTC, self).tearDown()
         self._cleanup_concat_cache()
--- a/web/views/staticcontrollers.py	Tue Mar 20 18:25:06 2012 +0100
+++ b/web/views/staticcontrollers.py	Mon Mar 19 14:37:43 2012 +0100
@@ -67,6 +67,16 @@
             # the HTTP RFC recommands not going further than 1 year ahead
             expires = datetime.now() + timedelta(days=6*30)
             self._cw.set_header('Expires', generateDateTime(mktime(expires.timetuple())))
+
+        # XXX system call to os.stats could be cached once and for all in
+        # production mode (where static files are not expected to change)
+        #
+        # Note that: we do a osp.isdir + osp.isfile before and a potential
+        # os.read after. Improving this specific call will not help
+        #
+        # Real production environment should use dedicated static file serving.
+        self._cw.set_header('last-modified', generateDateTime(os.stat(path).st_mtime))
+        self._cw.validate_cache()
         # XXX elif uri.startswith('/https/'): uri = uri[6:]
         mimetype, encoding = mimetypes.guess_type(path)
         self._cw.set_content_type(mimetype, osp.basename(path), encoding)