[Zope-Checkins] CVS: Zope2 - Image.py:1.129

Martijn Pieters mj@digicool.com
Mon, 23 Apr 2001 11:39:42 -0400 (EDT)


Update of /cvs-repository/Zope2/lib/python/OFS
In directory korak:/tmp/cvs-serv1360/lib/python/OFS

Modified Files:
	Image.py 
Log Message:
- Merge the mj-http_range_support-branch HTTP Range functionality.

- Update the CHANGES.txt file.



--- Updated File Image.py in package Zope2 --
--- Image.py	2001/04/12 15:55:43	1.128
+++ Image.py	2001/04/23 15:39:11	1.129
@@ -86,9 +86,9 @@
 
 __version__='$Revision$'[11:-2]
 
-import Globals, string, struct, content_types
+import Globals, string, struct
 from OFS.content_types import guess_content_type
-from Globals import DTMLFile, MessageDialog
+from Globals import DTMLFile
 from PropertyManager import PropertyManager
 from AccessControl.Role import RoleManager
 from webdav.common import rfc1123_date
@@ -100,10 +100,10 @@
 from Acquisition import Implicit
 from DateTime import DateTime
 from Cache import Cacheable
+from mimetools import choose_boundary
+from ZPublisher import HTTPRangeSupport
 
-
 StringType=type('')
-
 manage_addFileForm=DTMLFile('dtml/imageAdd', globals(),Kind='File',kind='file')
 def manage_addFile(self,id,file='',title='',precondition='', content_type='',
                    REQUEST=None):
@@ -137,7 +137,7 @@
            RoleManager, Item_w__name__, Cacheable):
     """A File object is a content object for arbitrary files."""
     
-    __implements__ = (WriteLockInterface,)
+    __implements__ = (WriteLockInterface, HTTPRangeSupport.HTTPRangeInterface)
     meta_type='File'
 
     
@@ -212,6 +212,8 @@
             # 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).
+            # This happens to be what RFC2616 tells us to do in the face of an
+            # invalid date.
             try:    mod_since=long(DateTime(header).timeTime())
             except: mod_since=None
             if mod_since is not None:
@@ -225,6 +227,7 @@
                     RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime))
                     RESPONSE.setHeader('Content-Type', self.content_type)
                     RESPONSE.setHeader('Content-Length', self.size)
+                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
                     RESPONSE.setStatus(304)
                     return ''
 
@@ -237,9 +240,181 @@
                 c(REQUEST['PARENTS'][1],REQUEST)
             else:
                 c()
+
+        # HTTP Range header handling
+        range = REQUEST.get_header('Range', None)
+        if_range = REQUEST.get_header('If-Range', None)
+        if range is not None:
+            ranges = HTTPRangeSupport.parseRange(range)
+
+            if if_range is not None:
+                # Only send ranges if the data isn't modified, otherwise send
+                # the whole object. Support both ETags and Last-Modified dates!
+                if len(if_range) > 1 and if_range[:2] == 'ts':
+                    # ETag:
+                    if if_range != self.http__etag():
+                        # Modified, so send a normal response. We delete
+                        # the ranges, which causes us to skip to the 200
+                        # response.
+                        ranges = None
+                else:
+                    # Date
+                    date = string.split(if_range, ';')[0]
+                    try: mod_since=long(DateTime(date).timeTime())
+                    except: mod_since=None
+                    if mod_since is not None:
+                        if self._p_mtime:
+                            last_mod = long(self._p_mtime)
+                        else:
+                            last_mod = long(0)
+                        if last_mod > mod_since:
+                            # Modified, so send a normal response. We delete
+                            # the ranges, which causes us to skip to the 200
+                            # response.
+                            ranges = None
+
+            if ranges:
+                # Search for satisfiable ranges.
+                satisfiable = 0
+                for start, end in ranges:
+                    if start < self.size:
+                        satisfiable = 1
+                        break
+
+                if not satisfiable:
+                    RESPONSE.setHeader('Content-Range', 
+                        'bytes */%d' % self.size)
+                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                    RESPONSE.setHeader('Last-Modified',
+                        rfc1123_date(self._p_mtime))
+                    RESPONSE.setHeader('Content-Type', self.content_type)
+                    RESPONSE.setHeader('Content-Length', self.size)
+                    RESPONSE.setStatus(416)
+                    return ''
+
+                # Can we optimize?
+                ranges = HTTPRangeSupport.optimizeRanges(ranges, self.size)
+                                
+                if len(ranges) == 1:
+                    # Easy case, set extra header and return partial set.
+                    start, end = ranges[0]
+                    size = end - start
+                    
+                    RESPONSE.setHeader('Last-Modified',
+                        rfc1123_date(self._p_mtime))
+                    RESPONSE.setHeader('Content-Type', self.content_type)
+                    RESPONSE.setHeader('Content-Length', size)
+                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                    RESPONSE.setHeader('Content-Range', 
+                        'bytes %d-%d/%d' % (start, end - 1, self.size))
+                    RESPONSE.setStatus(206) # Partial content
+
+                    data = self.data
+                    if type(data) is StringType:
+                        return data[start:end]
+
+                    # Linked Pdata objects. Urgh.
+                    pos = 0
+                    while data is not None:
+                        l = len(data.data)
+                        pos = pos + l
+                        if pos > start:
+                            # We are within the range
+                            lstart = l - (pos - start)
+
+                            if lstart < 0: lstart = 0
+                            
+                            # find the endpoint
+                            if end <= pos:
+                                lend = l - (pos - end)
+                                
+                                # Send and end transmission
+                                RESPONSE.write(data[lstart:lend])
+                                break
+
+                            # Not yet at the end, transmit what we have.
+                            RESPONSE.write(data[lstart:])
+
+                        data = data.next
+                    
+                    return ''
+                    
+                else:
+                    # Ignore multi-part ranges for now, pretend we don't know
+                    # about ranges at all.
+                    # When we get here, ranges have been optimized, so they are
+                    # in order, non-overlapping, and start and end values are
+                    # positive integers.
+                    boundary = choose_boundary()
+                    
+                    # Calculate the content length
+                    size = (8 + len(boundary) + # End marker length
+                        len(ranges) * (         # Constant lenght per set
+                            49 + len(boundary) + len(self.content_type) + 
+                            len('%d' % self.size)))
+                    for start, end in ranges:
+                        # Variable length per set
+                        size = (size + len('%d%d' % (start, end - 1)) + 
+                            end - start)
+                            
+                    
+                    RESPONSE.setHeader('Content-Length', size)
+                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                    RESPONSE.setHeader('Last-Modified',
+                        rfc1123_date(self._p_mtime))
+                    RESPONSE.setHeader('Content-Type',
+                        'multipart/byteranges; boundary=%s' % boundary)
+                    RESPONSE.setStatus(206) # Partial content
+
+                    pos = 0
+                    data = self.data
+
+                    for start, end in ranges:
+                        RESPONSE.write('\r\n--%s\r\n' % boundary)
+                        RESPONSE.write('Content-Type: %s\r\n' %
+                            self.content_type)
+                        RESPONSE.write(
+                            'Content-Range: bytes %d-%d/%d\r\n\r\n' % (
+                                start, end - 1, self.size)) 
+
+                        if type(data) is StringType:
+                            RESPONSE.write(data[start:end])
+
+                        else:
+                            # Yippee. Linked Pdata objects.
+                            while data is not None:
+                                l = len(data.data)
+                                pos = pos + l
+                                if pos > start:
+                                    # We are within the range
+                                    lstart = l - (pos - start)
+
+                                    if lstart < 0: lstart = 0
+                                    
+                                    # find the endpoint
+                                    if end <= pos:
+                                        lend = l - (pos - end)
+                                        
+                                        # Send and loop to next range
+                                        RESPONSE.write(data[lstart:lend])
+                                        # Back up the position marker, it will
+                                        # be incremented again for the next
+                                        # part.
+                                        pos = pos - l
+                                        break
+
+                                    # Not yet at the end, transmit what we have.
+                                    RESPONSE.write(data[lstart:])
+
+                                data = data.next
+
+                    RESPONSE.write('\r\n--%s--\r\n' % boundary)
+                    return ''
+
         RESPONSE.setHeader('Last-Modified', rfc1123_date(self._p_mtime))
         RESPONSE.setHeader('Content-Type', self.content_type)
         RESPONSE.setHeader('Content-Length', self.size)
+        RESPONSE.setHeader('Accept-Ranges', 'bytes')
 
         # Don't cache the data itself, but provide an opportunity
         # for a cache manager to set response headers.
@@ -268,6 +443,7 @@
         self.size=size
         self.data=data
         self.ZCacheable_invalidate()
+        self.http__refreshEtag()
 
     def manage_edit(self, title, content_type, precondition='', REQUEST=None):
         """