[Zope-Checkins] CVS: Zope/lib/python/OFS - Image.py:1.124.4.4

Martijn Pieters mj@zope.com
Sat, 13 Jul 2002 17:14:54 -0400


Update of /cvs-repository/Zope/lib/python/OFS
In directory cvs.zope.org:/tmp/cvs-serv9855/lib/python/OFS

Modified Files:
      Tag: zope-2_3-branch
	Image.py 
Log Message:
Backported HTTP Range support to Zope 2.3, for the benefit of Zope.org.


=== Zope/lib/python/OFS/Image.py 1.124.4.3 => 1.124.4.4 ===
 from Acquisition import Implicit
 from DateTime import DateTime
 from Cache import Cacheable
-
+from mimetools import choose_boundary
+from ZPublisher import HTTPRangeSupport
 
 StringType=type('')
 
@@ -135,6 +136,7 @@
            RoleManager, Item_w__name__, Cacheable):
     """A File object is a content object for arbitrary files."""
     
+    __implements__ = (HTTPRangeSupport.HTTPRangeInterface,)
     meta_type='File'
 
     
@@ -222,6 +224,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 ''
 
@@ -234,9 +237,185 @@
                 c(REQUEST['PARENTS'][1],REQUEST)
             else:
                 c()
+
+        # HTTP Range header handling
+        range = REQUEST.get_header('Range', None)
+        request_range = REQUEST.get_header('Request-Range', None)
+        if request_range is not None:
+            # Netscape 2 through 4 and MSIE 3 implement a draft version
+            # Later on, we need to serve a different mime-type as well.
+            range = request_range
+        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, not supported in Zope 2.3, so ignore
+                    pass
+                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:
+                    # 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)
+                            
+                    
+                    # Some clients implement an earlier draft of the spec, they
+                    # will only accept x-byteranges.
+                    draftprefix = (request_range is not None) and 'x-' or ''
+
+                    RESPONSE.setHeader('Content-Length', size)
+                    RESPONSE.setHeader('Accept-Ranges', 'bytes')
+                    RESPONSE.setHeader('Last-Modified',
+                        rfc1123_date(self._p_mtime))
+                    RESPONSE.setHeader('Content-Type',
+                        'multipart/%sbyteranges; boundary=%s' % (
+                            draftprefix, 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.