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

Martijn Pieters mj@digicool.com
Mon, 23 Apr 2001 10:28:58 -0400 (EDT)


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

Modified Files:
      Tag: mj-http_range_support-branch
	Image.py 
Log Message:
- Move HTTP Range methods to own module.

- Create a HTTPRangeInterface flag interface so we can test for support.

- Have WebDAV enabled objects set 'Accept-Ranges: bytes' for supporting
  objects and 'Accept-Ranges: none' for the rest.

- Add ETag support to If-Range (ETags were already generated for Image and
  File objects, just not updated when the contents of the resource changed).

- Removed some unused imports.



--- Updated File Image.py in package Zope2 --
--- Image.py	2001/04/20 15:03:52	1.128.6.4
+++ Image.py	2001/04/23 14:28:27	1.128.6.5
@@ -86,9 +86,9 @@
 
 __version__='$Revision$'[11:-2]
 
-import Globals, string, struct, content_types, re, sys
+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
@@ -101,119 +101,9 @@
 from DateTime import DateTime
 from Cache import Cacheable
 from mimetools import choose_boundary
+from ZPublisher import HTTPRangeSupport
 
-
 StringType=type('')
-WHITESPACE = re.compile('\s*', re.MULTILINE)
-
-# RFC 2616 (HTTP 1.1) Range header parsing
-# Convert a range header to a list of slice indexes, returned as (start, end)
-# tuples. If no end was given, end is None. Note that the RFC specifies the end
-# offset to be inclusive, we return python convention indexes, where the end is
-# exclusive. Syntactically incorrect headers are to be ignored, so if we
-# encounter one we return None.
-def parseRange(header):
-    ranges = []
-    add = ranges.append
-
-    # First, clean out *all* whitespace. This is slightly more tolerant
-    # than the spec asks for, but hey, it makes this function much easier.
-    header = WHITESPACE.sub('', header)
-
-    # A range header only can specify a byte range
-    try: spec, sets = string.split(header, '=')
-    except ValueError: return None
-    if spec != 'bytes':
-        return None
-
-    # The sets are delimited by commas.
-    sets = string.split(sets, ',')
-    # Filter out empty values, things like ',,' are allowed in the spec
-    sets = filter(None, sets)
-    # We need at least one set
-    if not sets:
-        return None
-
-    for set in sets:
-        try: start, end = string.split(set, '-')
-        except ValueError: return None
-
-        # Catch empty sets
-        if not start and not end:
-            return None
-
-        # Convert to integers or None (which will raise errors if
-        # non-integers were used (which is what we want)).
-        try:
-            if start == '': start = None
-            else: start = int(start)
-            if end == '': end = None
-            else: end = int(end)
-        except ValueError:
-            return None
-
-        # Special case: No start means the suffix format was used, which
-        # means the end value is actually a negative start value.
-        # Convert this by making it absolute.
-        # A -0 range is converted to sys.maxint, which will result in a
-        # Unsatisfiable response if no other ranges can by satisfied either.
-        if start is None:
-            start, end = -end, None
-            if not start:
-                start = sys.maxint
-        elif end is not None:
-            end = end + 1 # Make the end of the range exclusive
-
-        if end is not None and end <= start:
-            return None
-
-        # And store
-        add((start, end))
-
-    return ranges
-
-# Optimize Range sets, given those sets and the length of the resource
-# Optimisation is done by first expanding relative start values and open ends,
-# then sorting and combining overlapping or adjacent ranges. We also remove
-# unsatisfiable ranges (where the start lies beyond the size of the resource).
-def optimizeRanges(ranges, size):
-    expanded = []
-    add = expanded.append
-    for start, end in ranges:
-        if start < 0:
-            start = size + start
-        end = end or size
-        if end > size: end = size
-        # Only use satisfiable ranges
-        if start < size:
-            add((start, end))
-
-    ranges = expanded
-    ranges.sort()
-    ranges.reverse()
-    optimized = []
-    add = optimized.append
-    start, end = ranges.pop()
-    
-    while ranges:
-        nextstart, nextend = ranges.pop()
-        # If the next range overlaps or is adjacent
-        if nextstart <= end:
-            # If it falls within the current range, discard
-            if nextend <= end:
-                continue
-            
-            # Overlap, adjust end
-            end = nextend
-        else:
-            add((start, end))
-            start, end = nextstart, nextend
-
-    # Add the remaining optimized range
-    add((start, end))
-    
-    return optimized
-
 manage_addFileForm=DTMLFile('dtml/imageAdd', globals(),Kind='File',kind='file')
 def manage_addFile(self,id,file='',title='',precondition='', content_type='',
                    REQUEST=None):
@@ -247,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'
 
     
@@ -355,23 +245,33 @@
         range = REQUEST.get_header('Range', None)
         if_range = REQUEST.get_header('If-Range', None)
         if range is not None:
-            ranges = parseRange(range)
+            ranges = HTTPRangeSupport.parseRange(range)
 
             if if_range is not None:
                 # Only send ranges if the data isn't modified, otherwise send
-                # the whole object.
-                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.
+                # 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.
@@ -393,7 +293,7 @@
                     return ''
 
                 # Can we optimize?
-                ranges = optimizeRanges(ranges, self.size)
+                ranges = HTTPRangeSupport.optimizeRanges(ranges, self.size)
                                 
                 if len(ranges) == 1:
                     # Easy case, set extra header and return partial set.
@@ -543,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):
         """