[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.