[CMF-checkins] SVN: CMF/branches/1.6/C - CMFCore.FSDTMLMethod/FSImage/FSFile: Added 304 Not Modified support

Jens Vagelpohl jens at dataflake.org
Sat Feb 18 08:57:53 EST 2006


Log message for revision 41663:
  - CMFCore.FSDTMLMethod/FSImage/FSFile: Added 304 Not Modified support
    analogous to the support added for FSPageTemplates earlier on. 
    (http://www.zope.org/Collectors/CMF/402)
  

Changed:
  U   CMF/branches/1.6/CHANGES.txt
  U   CMF/branches/1.6/CMFCore/FSDTMLMethod.py
  U   CMF/branches/1.6/CMFCore/FSFile.py
  U   CMF/branches/1.6/CMFCore/FSImage.py
  U   CMF/branches/1.6/CMFCore/tests/base/dummy.py
  U   CMF/branches/1.6/CMFCore/tests/test_FSDTMLMethod.py
  U   CMF/branches/1.6/CMFCore/tests/test_FSFile.py
  U   CMF/branches/1.6/CMFCore/tests/test_FSImage.py
  U   CMF/branches/1.6/CMFCore/utils.py

-=-
Modified: CMF/branches/1.6/CHANGES.txt
===================================================================
--- CMF/branches/1.6/CHANGES.txt	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CHANGES.txt	2006-02-18 13:57:52 UTC (rev 41663)
@@ -18,6 +18,10 @@
 
   Features
 
+    - CMFCore.FSDTMLMethod/FSImage/FSFile: Added 304 Not Modified support
+      analogous to the support added for FSPageTemplates earlier on.
+      (http://www.zope.org/Collectors/CMF/402)
+
     - Replaced Z2 interfaces w/ Z3 interfaces, dynamically creating Z2
       interfaces via bridge
 

Modified: CMF/branches/1.6/CMFCore/FSDTMLMethod.py
===================================================================
--- CMF/branches/1.6/CMFCore/FSDTMLMethod.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/FSDTMLMethod.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -29,7 +29,7 @@
 from permissions import View
 from permissions import ViewManagementScreens
 from utils import _dtmldir
-from utils import _setCacheHeaders
+from utils import _setCacheHeaders, _checkConditionalGET
 from utils import expandpath
 
 
@@ -112,15 +112,19 @@
 
         self._updateFromFS()
 
+        kw['document_id']   =self.getId()
+        kw['document_title']=self.title
+
+        if client is not None:
+            if _checkConditionalGET(self, kw):
+                return ''
+
         if not self._cache_namespace_keys:
             data = self.ZCacheable_get(default=_marker)
             if data is not _marker:
                 # Return cached results.
                 return data
 
-        kw['document_id']   =self.getId()
-        kw['document_title']=self.title
-
         __traceback_info__ = self._filepath
         security=getSecurityManager()
         security.addContext(self)

Modified: CMF/branches/1.6/CMFCore/FSFile.py
===================================================================
--- CMF/branches/1.6/CMFCore/FSFile.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/FSFile.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -22,7 +22,6 @@
 from OFS.Cache import Cacheable
 from OFS.content_types import guess_content_type
 from OFS.Image import File
-from webdav.common import rfc1123_date
 
 from DirectoryView import registerFileExtension
 from DirectoryView import registerMetaType
@@ -32,7 +31,7 @@
 from permissions import ViewManagementScreens
 from utils import _dtmldir
 from utils import _setCacheHeaders, _ViewEmulator
-from utils import expandpath
+from utils import expandpath, _FSCacheHeaders, _checkConditionalGET
 
 
 class FSFile(FSObject):
@@ -104,6 +103,9 @@
         self._updateFromFS()
         return str( self._readFile( 0 ) )
 
+    def modified(self):
+        return self.getModTime()
+
     security.declareProtected(View, 'index_html')
     def index_html(self, REQUEST, RESPONSE):
         """
@@ -113,51 +115,38 @@
         Content-Type HTTP header to the objects content type.
         """
         self._updateFromFS()
-        data = self._readFile(0)
-        data_len = len(data)
-        last_mod = self._file_mod_time
-        status = 200
-        # HTTP If-Modified-Since header handling.
-        header=REQUEST.get_header('If-Modified-Since', None)
-        if header is not None:
-            header = header.split(';')[0]
-            # Some proxies seem to send invalid date strings for this
-            # header. If the date string is not valid, we ignore it
-            # rather than raise an error to be generally consistent
-            # with common servers such as Apache (which can usually
-            # understand the screwy date string as a lucky side effect
-            # of the way they parse it).
-            try:
-                mod_since=long(DateTime(header).timeTime())
-            except:
-                mod_since=None
-                
-            if mod_since is not None:
-                if last_mod > 0 and last_mod <= mod_since:
-                    status = 304
-                    data = ''
+        view = _ViewEmulator().__of__(self)
 
-        #Last-Modified will get stomped on by a cache policy it there is
-        #one set....
-        RESPONSE.setStatus(status)
-        RESPONSE.setHeader('Last-Modified', rfc1123_date(last_mod))
+        # If we have a conditional get, set status 304 and return
+        # no content
+        if _checkConditionalGET(view, extra_context={}):
+            return ''
+
         RESPONSE.setHeader('Content-Type', self.content_type)
 
-        if status != 304:
-            # Avoid setting content-length for a 304. See RFC 2616.
-            # Zope might still, for better or for worse, set a 
-            # content-length header with value "0".
-            RESPONSE.setHeader('Content-Length', data_len)
+        # old-style If-Modified-Since header handling.
+        if self._setOldCacheHeaders():
+            # Make sure the CachingPolicyManager gets a go as well
+            _setCacheHeaders(view, extra_context={})
+            return ''
 
+        data = self._readFile(0)
+        data_len = len(data)
+        RESPONSE.setHeader('Content-Length', data_len)
+
         #There are 2 Cache Managers which can be in play....
         #need to decide which to use to determine where the cache headers
         #are decided on.
         if self.ZCacheable_getManager() is not None:
             self.ZCacheable_set(None)
         else:
-            _setCacheHeaders(_ViewEmulator().__of__(self), extra_context={})
+            _setCacheHeaders(view, extra_context={})
         return data
 
+    def _setOldCacheHeaders(self):
+        # return False to disable this simple caching behaviour
+        return _FSCacheHeaders(self)
+
     security.declareProtected(View, 'getContentType')
     def getContentType(self):
         """Get the content type of a file or image.

Modified: CMF/branches/1.6/CMFCore/FSImage.py
===================================================================
--- CMF/branches/1.6/CMFCore/FSImage.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/FSImage.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -18,7 +18,6 @@
 import Globals
 from DateTime import DateTime
 from AccessControl import ClassSecurityInfo
-from webdav.common import rfc1123_date
 from OFS.Cache import Cacheable
 from OFS.Image import Image, getImageInfo
 
@@ -30,7 +29,7 @@
 from FSObject import FSObject
 from utils import _dtmldir
 from utils import _setCacheHeaders, _ViewEmulator
-from utils import expandpath
+from utils import expandpath, _FSCacheHeaders, _checkConditionalGET
 
 
 class FSImage(FSObject):
@@ -94,52 +93,43 @@
         Returns the contents of the file or image.  Also, sets the
         Content-Type HTTP header to the objects content type.
         """
+
         self._updateFromFS()
-        data = self._data
-        data_len = len(data)
-        last_mod = self._file_mod_time
-        status = 200
-        # HTTP If-Modified-Since header handling.
-        header = REQUEST.get_header('If-Modified-Since', None)
-        if header is not None:
-            header = header.split(';')[0]
-            # Some proxies seem to send invalid date strings for this
-            # header. If the date string is not valid, we ignore it
-            # rather than raise an error to be generally consistent
-            # with common servers such as Apache (which can usually
-            # understand the screwy date string as a lucky side effect
-            # of the way they parse it).
-            try:
-                mod_since = long(DateTime(header).timeTime())
-            except:
-                mod_since = None
+        view = _ViewEmulator().__of__(self)
 
-            if mod_since is not None:
-                if last_mod > 0 and last_mod <= mod_since:
-                    status = 304
-                    data = ''
+        # If we have a conditional get, set status 304 and return
+        # no content
+        if _checkConditionalGET(view, extra_context={}):
+            return ''
 
-        #Last-Modified will get stomped on by a cache policy it there is
-        #one set....
-        RESPONSE.setStatus(status)
-        RESPONSE.setHeader('Last-Modified', rfc1123_date(last_mod))
         RESPONSE.setHeader('Content-Type', self.content_type)
 
-        if status != 304:
-            # Avoid setting content-length for a 304. See RFC 2616.
-            # Zope might still, for better or for worse, set a
-            # content-length header with value "0". 
-            RESPONSE.setHeader('Content-Length', data_len)
+        # old-style If-Modified-Since header handling.
+        if self._setOldCacheHeaders():
+            # Make sure the CachingPolicyManager gets a go as well
+            _setCacheHeaders(view, extra_context={})
+            return ''
 
+        data = self._readFile(0)
+        data_len = len(data)
+        RESPONSE.setHeader('Content-Length', data_len)
+
         #There are 2 Cache Managers which can be in play....
         #need to decide which to use to determine where the cache headers
         #are decided on.
         if self.ZCacheable_getManager() is not None:
             self.ZCacheable_set(None)
         else:
-            _setCacheHeaders(_ViewEmulator().__of__(self), extra_context={})
+            _setCacheHeaders(view, extra_context={})
         return data
 
+    def _setOldCacheHeaders(self):
+        # return False to disable this simple caching behaviour
+        return _FSCacheHeaders(self)
+
+    def modified(self):
+        return self.getModTime()
+
     security.declareProtected(View, 'getContentType')
     def getContentType(self):
         """Get the content type of a file or image.

Modified: CMF/branches/1.6/CMFCore/tests/base/dummy.py
===================================================================
--- CMF/branches/1.6/CMFCore/tests/base/dummy.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/tests/base/dummy.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -22,6 +22,8 @@
 from Products.CMFCore.PortalContent import PortalContent
 from security import OmnipotentUser
 
+from DateTime import DateTime
+from webdav.common import rfc1123_date
 
 class DummyObject(Implicit):
     """
@@ -350,7 +352,6 @@
     def notifyCreated(self, ob):
         self.test_notified = ob
 
-
 class DummyCachingManager:
 
     def getHTTPCachingHeaders( self, content, view_name, keywords, time=None ):
@@ -359,5 +360,32 @@
              ('test_path', '/'.join(content.getPhysicalPath())),
              )
 
+    def getModTimeAndETag(self, content, view_method, keywords, time=None ):
+         return (None, None, False)
+
     def getPhysicalPath(self):
         return ('baz',)
+
+
+FAKE_ETAG = None # '--FAKE ETAG--'
+
+class DummyCachingManagerWithPolicy(DummyCachingManager):
+
+    # dummy fixture implementing a single policy:
+    #  - always set the last-modified date if available
+    #  - calculate the date using the modified method on content
+
+    def getHTTPCachingHeaders( self, content, view_name, keywords, time=None ):
+
+         # if the object has a modified method, add it as last-modified
+         if hasattr(content, 'modified'):
+             headers = ( ('Last-modified', rfc1123_date(content.modified()) ), )
+         return headers
+
+    def getModTimeAndETag(self, content, view_method, keywords, time=None ):
+         modified_date = None
+         if hasattr(content, 'modified'):
+            modified_date = content.modified()
+         set_last_modified = (modified_date is not None)
+         return (modified_date, FAKE_ETAG, set_last_modified)
+

Modified: CMF/branches/1.6/CMFCore/tests/test_FSDTMLMethod.py
===================================================================
--- CMF/branches/1.6/CMFCore/tests/test_FSDTMLMethod.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/tests/test_FSDTMLMethod.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -31,10 +31,13 @@
 from Products.CMFCore.FSDTMLMethod import FSDTMLMethod
 from Products.CMFCore.FSMetadata import FSMetadata
 from Products.CMFCore.tests.base.dummy import DummyCachingManager
+from Products.CMFCore.tests.base.dummy import DummyCachingManagerWithPolicy
 from Products.CMFCore.tests.base.testcase import FSDVTest
 from Products.CMFCore.tests.base.testcase import RequestTest
 from Products.CMFCore.tests.base.testcase import SecurityTest
+from Products.CMFCore.tests.base.dummy import DummyContent
 
+from DateTime import DateTime
 
 class FSDTMLMaker(FSDVTest):
 
@@ -71,7 +74,25 @@
         self.failUnless( 'foo' in self.RESPONSE.headers.keys() )
         self.failUnless( 'bar' in self.RESPONSE.headers.keys() )
 
+    def test_304_response_from_cpm( self ):
+        # test that we get a 304 response from the cpm via this template
 
+        from webdav.common import rfc1123_date
+
+        mod_time = DateTime()
+        self.root.caching_policy_manager = DummyCachingManagerWithPolicy()
+        content = DummyContent(id='content')
+        content.modified_date = mod_time
+        content = content.__of__(self.root)
+        script = self._makeOne('testDTML', 'testDTML.dtml')
+        script = script.__of__(content)
+        self.REQUEST.environ[ 'IF_MODIFIED_SINCE'
+                            ] = '%s;' % rfc1123_date( mod_time+3600 )
+        data = script(content, self.REQUEST, self.RESPONSE)
+
+        self.assertEqual( data, '' )
+        self.assertEqual( self.RESPONSE.getStatus(), 304 )
+
 class FSDTMLMethodCustomizationTests( SecurityTest, FSDTMLMaker ):
 
     def setUp( self ):

Modified: CMF/branches/1.6/CMFCore/tests/test_FSFile.py
===================================================================
--- CMF/branches/1.6/CMFCore/tests/test_FSFile.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/tests/test_FSFile.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -24,6 +24,7 @@
 
 from os.path import join as path_join
 
+from Products.CMFCore.tests.base.dummy import DummyCachingManagerWithPolicy
 from Products.CMFCore.tests.base.dummy import DummyCachingManager
 from Products.CMFCore.tests.base.testcase import FSDVTest
 from Products.CMFCore.tests.base.testcase import RequestTest
@@ -145,6 +146,53 @@
         self.failUnless( data, '' )
         self.assertEqual( self.RESPONSE.getStatus(), 200 )
 
+    def test_index_html_with_304_from_cpm( self ):
+        self.root.caching_policy_manager = DummyCachingManagerWithPolicy()
+        path, ref = self._extractFile('test_file.swf')
+
+        import os
+        from webdav.common import rfc1123_date
+        from base.dummy import FAKE_ETAG
+        
+        file = self._makeOne( 'test_file', 'test_file.swf' )
+        file = file.__of__( self.root )
+
+        mod_time = os.stat( path )[ 8 ]
+
+        self.REQUEST.environ[ 'IF_MODIFIED_SINCE'
+                            ] = '%s;' % rfc1123_date( mod_time )
+        self.REQUEST.environ[ 'IF_NONE_MATCH'
+                            ] = '%s;' % FAKE_ETAG
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+        self.assertEqual( len(data), 0 )
+        self.assertEqual( self.RESPONSE.getStatus(), 304 )
+
+    def test_index_html_200_with_cpm( self ):
+        # should behave the same as without cpm installed
+        self.root.caching_policy_manager = DummyCachingManager()
+        path, ref = self._extractFile('test_file.swf')
+
+        import os
+        from webdav.common import rfc1123_date
+        
+        file = self._makeOne( 'test_file', 'test_file.swf' )
+        file = file.__of__( self.root )
+
+        mod_time = os.stat( path )[ 8 ]
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+
+        self.assertEqual( len( data ), len( ref ) )
+        self.assertEqual( data, ref )
+        # ICK!  'HTTPResponse.getHeader' doesn't case-flatten the key!
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Length'.lower() )
+                        , str(len(ref)) )
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Type'.lower() )
+                        , 'application/octet-stream' )
+        self.assertEqual( self.RESPONSE.getHeader( 'Last-Modified'.lower() )
+                        , rfc1123_date( mod_time ) )
+
     def test_caching( self ):
         self.root.caching_policy_manager = DummyCachingManager()
         original_len = len(self.RESPONSE.headers)

Modified: CMF/branches/1.6/CMFCore/tests/test_FSImage.py
===================================================================
--- CMF/branches/1.6/CMFCore/tests/test_FSImage.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/tests/test_FSImage.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -25,6 +25,7 @@
 from os.path import join as path_join
 
 from Products.CMFCore.tests.base.dummy import DummyCachingManager
+from Products.CMFCore.tests.base.dummy import DummyCachingManagerWithPolicy
 from Products.CMFCore.tests.base.testcase import FSDVTest
 from Products.CMFCore.tests.base.testcase import RequestTest
 
@@ -133,6 +134,28 @@
         self.failUnless( data, '' )
         self.assertEqual( self.RESPONSE.getStatus(), 200 )
 
+    def test_index_html_with_304_from_cpm( self ):
+        self.root.caching_policy_manager = DummyCachingManagerWithPolicy()
+        path, ref = self._extractFile()
+
+        import os
+        from webdav.common import rfc1123_date
+        from base.dummy import FAKE_ETAG
+        
+        file = self._makeOne( 'test_file', 'test_image.gif' )
+        file = file.__of__( self.root )
+
+        mod_time = os.stat( path )[ 8 ]
+
+        self.REQUEST.environ[ 'IF_MODIFIED_SINCE'
+                            ] = '%s;' % rfc1123_date( mod_time )
+        self.REQUEST.environ[ 'IF_NONE_MATCH'
+                            ] = '%s;' % FAKE_ETAG
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+        self.assertEqual( len(data), 0 )
+        self.assertEqual( self.RESPONSE.getStatus(), 304 )
+
     def test_caching( self ):
         self.root.caching_policy_manager = DummyCachingManager()
         original_len = len(self.RESPONSE.headers)
@@ -145,7 +168,31 @@
         self.failUnless('bar' in headers.keys())
         self.assertEqual(headers['test_path'], '/test_image')
 
+    def test_index_html_200_with_cpm( self ):
+        self.root.caching_policy_manager = DummyCachingManagerWithPolicy()
+        path, ref = self._extractFile()
 
+        import os
+        from webdav.common import rfc1123_date
+        
+        file = self._makeOne( 'test_file', 'test_image.gif' )
+        file = file.__of__( self.root )
+
+        mod_time = os.stat( path )[ 8 ]
+
+        data = file.index_html( self.REQUEST, self.RESPONSE )
+
+        # should behave the same as without cpm
+        self.assertEqual( len( data ), len( ref ) )
+        self.assertEqual( data, ref )
+        # ICK!  'HTTPResponse.getHeader' doesn't case-flatten the key!
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Length'.lower() )
+                        , str(len(ref)) )
+        self.assertEqual( self.RESPONSE.getHeader( 'Content-Type'.lower() )
+                        , 'image/gif' )
+        self.assertEqual( self.RESPONSE.getHeader( 'Last-Modified'.lower() )
+                        , rfc1123_date( mod_time ) )
+
     def test_index_html_with_304_and_caching( self ):
 
         # See collector #355

Modified: CMF/branches/1.6/CMFCore/utils.py
===================================================================
--- CMF/branches/1.6/CMFCore/utils.py	2006-02-18 13:57:37 UTC (rev 41662)
+++ CMF/branches/1.6/CMFCore/utils.py	2006-02-18 13:57:52 UTC (rev 41663)
@@ -48,7 +48,7 @@
 from Products.PageTemplates.Expressions import SecureModuleImporter
 from StructuredText.StructuredText import HTML
 from thread import allocate_lock
-
+from webdav.common import rfc1123_date
 from exceptions import AccessControl_Unauthorized
 from exceptions import NotFound
 
@@ -190,7 +190,39 @@
         raise NotFound('Cannot find default view for "%s"' %
                             '/'.join(obj.getPhysicalPath()))
 
+def _FSCacheHeaders(obj):
 
+    REQUEST = getattr(obj, 'REQUEST', None)
+    if REQUEST is None:
+        return False
+
+    RESPONSE = REQUEST.RESPONSE
+    header = REQUEST.get_header('If-Modified-Since', None)
+    last_mod = obj._file_mod_time
+
+    if header is not None:
+        header = header.split(';')[0]
+        # Some proxies seem to send invalid date strings for this
+        # header. If the date string is not valid, we ignore it
+        # rather than raise an error to be generally consistent
+        # with common servers such as Apache (which can usually
+        # understand the screwy date string as a lucky side effect
+        # of the way they parse it).
+        try:
+            mod_since=DateTime(header)
+            mod_since=long(mod_since.timeTime())
+        except TypeError:
+            mod_since=None
+               
+        if mod_since is not None:
+            if last_mod > 0 and last_mod <= mod_since:
+                RESPONSE.setStatus(304)
+                return True
+
+    #Last-Modified will get stomped on by a cache policy if there is
+    #one set....
+    RESPONSE.setHeader('Last-Modified', rfc1123_date(last_mod))
+
 # If Zope ever provides a call to getRolesInContext() through
 # the SecurityManager API, the method below needs to be updated.
 security.declarePrivate('_limitGrantedRoles')



More information about the CMF-checkins mailing list