mercurial/byterange.py
changeset 3673 eb0b4a2d70a9
parent 674 6513ba7d858a
equal deleted inserted replaced
3672:e8730b5b8a32 3673:eb0b4a2d70a9
   119         this object was created with a range tuple of (500,899),
   119         this object was created with a range tuple of (500,899),
   120         tell() will return 0 when at byte position 500 of the file.
   120         tell() will return 0 when at byte position 500 of the file.
   121         """
   121         """
   122         return (self.realpos - self.firstbyte)
   122         return (self.realpos - self.firstbyte)
   123 
   123 
   124     def seek(self,offset,whence=0):
   124     def seek(self, offset, whence=0):
   125         """Seek within the byte range.
   125         """Seek within the byte range.
   126         Positioning is identical to that described under tell().
   126         Positioning is identical to that described under tell().
   127         """
   127         """
   128         assert whence in (0, 1, 2)
   128         assert whence in (0, 1, 2)
   129         if whence == 0:   # absolute seek
   129         if whence == 0:   # absolute seek
   168                     size = (self.lastbyte - self.realpos)
   168                     size = (self.lastbyte - self.realpos)
   169             else:
   169             else:
   170                 size = (self.lastbyte - self.realpos)
   170                 size = (self.lastbyte - self.realpos)
   171         return size
   171         return size
   172 
   172 
   173     def _do_seek(self,offset):
   173     def _do_seek(self, offset):
   174         """Seek based on whether wrapped object supports seek().
   174         """Seek based on whether wrapped object supports seek().
   175         offset is relative to the current position (self.realpos).
   175         offset is relative to the current position (self.realpos).
   176         """
   176         """
   177         assert offset >= 0
   177         assert offset >= 0
   178         if not hasattr(self.fo, 'seek'):
   178         if not hasattr(self.fo, 'seek'):
   179             self._poor_mans_seek(offset)
   179             self._poor_mans_seek(offset)
   180         else:
   180         else:
   181             self.fo.seek(self.realpos + offset)
   181             self.fo.seek(self.realpos + offset)
   182         self.realpos+= offset
   182         self.realpos += offset
   183 
   183 
   184     def _poor_mans_seek(self,offset):
   184     def _poor_mans_seek(self, offset):
   185         """Seek by calling the wrapped file objects read() method.
   185         """Seek by calling the wrapped file objects read() method.
   186         This is used for file like objects that do not have native
   186         This is used for file like objects that do not have native
   187         seek support. The wrapped objects read() method is called
   187         seek support. The wrapped objects read() method is called
   188         to manually seek to the desired position.
   188         to manually seek to the desired position.
   189         offset -- read this number of bytes from the wrapped
   189         offset -- read this number of bytes from the wrapped
   197             if (pos + bufsize) > offset:
   197             if (pos + bufsize) > offset:
   198                 bufsize = offset - pos
   198                 bufsize = offset - pos
   199             buf = self.fo.read(bufsize)
   199             buf = self.fo.read(bufsize)
   200             if len(buf) != bufsize:
   200             if len(buf) != bufsize:
   201                 raise RangeError('Requested Range Not Satisfiable')
   201                 raise RangeError('Requested Range Not Satisfiable')
   202             pos+= bufsize
   202             pos += bufsize
   203 
   203 
   204 class FileRangeHandler(urllib2.FileHandler):
   204 class FileRangeHandler(urllib2.FileHandler):
   205     """FileHandler subclass that adds Range support.
   205     """FileHandler subclass that adds Range support.
   206     This class handles Range headers exactly like an HTTP
   206     This class handles Range headers exactly like an HTTP
   207     server would.
   207     server would.
   219         if host:
   219         if host:
   220             host, port = urllib.splitport(host)
   220             host, port = urllib.splitport(host)
   221             if port or socket.gethostbyname(host) not in self.get_names():
   221             if port or socket.gethostbyname(host) not in self.get_names():
   222                 raise urllib2.URLError('file not on local host')
   222                 raise urllib2.URLError('file not on local host')
   223         fo = open(localfile,'rb')
   223         fo = open(localfile,'rb')
   224         brange = req.headers.get('Range',None)
   224         brange = req.headers.get('Range', None)
   225         brange = range_header_to_tuple(brange)
   225         brange = range_header_to_tuple(brange)
   226         assert brange != ()
   226         assert brange != ()
   227         if brange:
   227         if brange:
   228             (fb,lb) = brange
   228             (fb, lb) = brange
   229             if lb == '': lb = size
   229             if lb == '':
       
   230                 lb = size
   230             if fb < 0 or fb > size or lb > size:
   231             if fb < 0 or fb > size or lb > size:
   231                 raise RangeError('Requested Range Not Satisfiable')
   232                 raise RangeError('Requested Range Not Satisfiable')
   232             size = (lb - fb)
   233             size = (lb - fb)
   233             fo = RangeableFileObject(fo, (fb,lb))
   234             fo = RangeableFileObject(fo, (fb, lb))
   234         headers = mimetools.Message(StringIO(
   235         headers = mimetools.Message(StringIO(
   235             'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' %
   236             'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' %
   236             (mtype or 'text/plain', size, modified)))
   237             (mtype or 'text/plain', size, modified)))
   237         return urllib.addinfourl(fo, headers, 'file:'+file)
   238         return urllib.addinfourl(fo, headers, 'file:'+file)
   238 
   239 
   290                    value in ('a', 'A', 'i', 'I', 'd', 'D'):
   291                    value in ('a', 'A', 'i', 'I', 'd', 'D'):
   291                     type = value.upper()
   292                     type = value.upper()
   292 
   293 
   293             # -- range support modifications start here
   294             # -- range support modifications start here
   294             rest = None
   295             rest = None
   295             range_tup = range_header_to_tuple(req.headers.get('Range',None))
   296             range_tup = range_header_to_tuple(req.headers.get('Range', None))
   296             assert range_tup != ()
   297             assert range_tup != ()
   297             if range_tup:
   298             if range_tup:
   298                 (fb,lb) = range_tup
   299                 (fb, lb) = range_tup
   299                 if fb > 0: rest = fb
   300                 if fb > 0:
       
   301                     rest = fb
   300             # -- range support modifications end here
   302             # -- range support modifications end here
   301 
   303 
   302             fp, retrlen = fw.retrfile(file, type, rest)
   304             fp, retrlen = fw.retrfile(file, type, rest)
   303 
   305 
   304             # -- range support modifications start here
   306             # -- range support modifications start here
   305             if range_tup:
   307             if range_tup:
   306                 (fb,lb) = range_tup
   308                 (fb, lb) = range_tup
   307                 if lb == '':
   309                 if lb == '':
   308                     if retrlen is None or retrlen == 0:
   310                     if retrlen is None or retrlen == 0:
   309                         raise RangeError('Requested Range Not Satisfiable due to unobtainable file length.')
   311                         raise RangeError('Requested Range Not Satisfiable due to unobtainable file length.')
   310                     lb = retrlen
   312                     lb = retrlen
   311                     retrlen = lb - fb
   313                     retrlen = lb - fb
   312                     if retrlen < 0:
   314                     if retrlen < 0:
   313                         # beginning of range is larger than file
   315                         # beginning of range is larger than file
   314                         raise RangeError('Requested Range Not Satisfiable')
   316                         raise RangeError('Requested Range Not Satisfiable')
   315                 else:
   317                 else:
   316                     retrlen = lb - fb
   318                     retrlen = lb - fb
   317                     fp = RangeableFileObject(fp, (0,retrlen))
   319                     fp = RangeableFileObject(fp, (0, retrlen))
   318             # -- range support modifications end here
   320             # -- range support modifications end here
   319 
   321 
   320             headers = ""
   322             headers = ""
   321             mtype = mimetypes.guess_type(req.get_full_url())[0]
   323             mtype = mimetypes.guess_type(req.get_full_url())[0]
   322             if mtype:
   324             if mtype:
   338     # this ftpwrapper code is copied directly from
   340     # this ftpwrapper code is copied directly from
   339     # urllib. The only enhancement is to add the rest
   341     # urllib. The only enhancement is to add the rest
   340     # argument and pass it on to ftp.ntransfercmd
   342     # argument and pass it on to ftp.ntransfercmd
   341     def retrfile(self, file, type, rest=None):
   343     def retrfile(self, file, type, rest=None):
   342         self.endtransfer()
   344         self.endtransfer()
   343         if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1
   345         if type in ('d', 'D'):
   344         else: cmd = 'TYPE ' + type; isdir = 0
   346             cmd = 'TYPE A'
       
   347             isdir = 1
       
   348         else:
       
   349             cmd = 'TYPE ' + type
       
   350             isdir = 0
   345         try:
   351         try:
   346             self.ftp.voidcmd(cmd)
   352             self.ftp.voidcmd(cmd)
   347         except ftplib.all_errors:
   353         except ftplib.all_errors:
   348             self.init()
   354             self.init()
   349             self.ftp.voidcmd(cmd)
   355             self.ftp.voidcmd(cmd)
   370                     raise IOError, ('ftp error', reason), sys.exc_info()[2]
   376                     raise IOError, ('ftp error', reason), sys.exc_info()[2]
   371         if not conn:
   377         if not conn:
   372             # Set transfer mode to ASCII!
   378             # Set transfer mode to ASCII!
   373             self.ftp.voidcmd('TYPE A')
   379             self.ftp.voidcmd('TYPE A')
   374             # Try a directory listing
   380             # Try a directory listing
   375             if file: cmd = 'LIST ' + file
   381             if file:
   376             else: cmd = 'LIST'
   382                 cmd = 'LIST ' + file
       
   383             else:
       
   384                 cmd = 'LIST'
   377             conn = self.ftp.ntransfercmd(cmd)
   385             conn = self.ftp.ntransfercmd(cmd)
   378         self.busy = 1
   386         self.busy = 1
   379         # Pass back both a suitably decorated object and a retrieval length
   387         # Pass back both a suitably decorated object and a retrieval length
   380         return (addclosehook(conn[0].makefile('rb'),
   388         return (addclosehook(conn[0].makefile('rb'),
   381                             self.endtransfer), conn[1])
   389                             self.endtransfer), conn[1])
   399     Return () if range_header does not conform to the range spec
   407     Return () if range_header does not conform to the range spec
   400     pattern.
   408     pattern.
   401 
   409 
   402     """
   410     """
   403     global _rangere
   411     global _rangere
   404     if range_header is None: return None
   412     if range_header is None:
       
   413         return None
   405     if _rangere is None:
   414     if _rangere is None:
   406         import re
   415         import re
   407         _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
   416         _rangere = re.compile(r'^bytes=(\d{1,})-(\d*)')
   408     match = _rangere.match(range_header)
   417     match = _rangere.match(range_header)
   409     if match:
   418     if match:
   410         tup = range_tuple_normalize(match.group(1,2))
   419         tup = range_tuple_normalize(match.group(1, 2))
   411         if tup and tup[1]:
   420         if tup and tup[1]:
   412             tup = (tup[0],tup[1]+1)
   421             tup = (tup[0], tup[1]+1)
   413         return tup
   422         return tup
   414     return ()
   423     return ()
   415 
   424 
   416 def range_tuple_to_header(range_tup):
   425 def range_tuple_to_header(range_tup):
   417     """Convert a range tuple to a Range header value.
   426     """Convert a range tuple to a Range header value.
   418     Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
   427     Return a string of the form "bytes=<firstbyte>-<lastbyte>" or None
   419     if no range is needed.
   428     if no range is needed.
   420     """
   429     """
   421     if range_tup is None: return None
   430     if range_tup is None:
       
   431         return None
   422     range_tup = range_tuple_normalize(range_tup)
   432     range_tup = range_tuple_normalize(range_tup)
   423     if range_tup:
   433     if range_tup:
   424         if range_tup[1]:
   434         if range_tup[1]:
   425             range_tup = (range_tup[0],range_tup[1] - 1)
   435             range_tup = (range_tup[0], range_tup[1] - 1)
   426         return 'bytes=%s-%s' % range_tup
   436         return 'bytes=%s-%s' % range_tup
   427 
   437 
   428 def range_tuple_normalize(range_tup):
   438 def range_tuple_normalize(range_tup):
   429     """Normalize a (first_byte,last_byte) range tuple.
   439     """Normalize a (first_byte,last_byte) range tuple.
   430     Return a tuple whose first element is guaranteed to be an int
   440     Return a tuple whose first element is guaranteed to be an int
   431     and whose second element will be '' (meaning: the last byte) or
   441     and whose second element will be '' (meaning: the last byte) or
   432     an int. Finally, return None if the normalized tuple == (0,'')
   442     an int. Finally, return None if the normalized tuple == (0,'')
   433     as that is equivelant to retrieving the entire file.
   443     as that is equivelant to retrieving the entire file.
   434     """
   444     """
   435     if range_tup is None: return None
   445     if range_tup is None:
       
   446         return None
   436     # handle first byte
   447     # handle first byte
   437     fb = range_tup[0]
   448     fb = range_tup[0]
   438     if fb in (None,''): fb = 0
   449     if fb in (None, ''):
   439     else: fb = int(fb)
   450         fb = 0
       
   451     else:
       
   452         fb = int(fb)
   440     # handle last byte
   453     # handle last byte
   441     try: lb = range_tup[1]
   454     try:
   442     except IndexError: lb = ''
   455         lb = range_tup[1]
       
   456     except IndexError:
       
   457         lb = ''
   443     else:
   458     else:
   444         if lb is None: lb = ''
   459         if lb is None:
   445         elif lb != '': lb = int(lb)
   460             lb = ''
       
   461         elif lb != '':
       
   462             lb = int(lb)
   446     # check if range is over the entire file
   463     # check if range is over the entire file
   447     if (fb,lb) == (0,''): return None
   464     if (fb, lb) == (0, ''):
       
   465         return None
   448     # check that the range is valid
   466     # check that the range is valid
   449     if lb < fb: raise RangeError('Invalid byte range: %s-%s' % (fb,lb))
   467     if lb < fb:
   450     return (fb,lb)
   468         raise RangeError('Invalid byte range: %s-%s' % (fb, lb))
       
   469     return (fb, lb)