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) |