1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139
|
# -*- test-case-name: txweb2.test.test_stream -*-
import os
import time
from txweb2 import http, http_headers, responsecode, stream
# Some starts at writing a response filter to handle request ranges.
class UnsatisfiableRangeRequest(Exception):
pass
def canonicalizeRange((start, end), size):
"""Return canonicalized (start, end) or raises UnsatisfiableRangeRequest
exception.
NOTE: end is the last byte *inclusive*, which is not the usual convention
in python! Be very careful! A range of 0,1 should return 2 bytes."""
# handle "-500" ranges
if start is None:
start = max(0, size - end)
end = None
if end is None or end >= size:
end = size - 1
if start >= size:
raise UnsatisfiableRangeRequest
return start, end
def makeUnsatisfiable(request, oldresponse):
if request.headers.hasHeader('if-range'):
return oldresponse # Return resource instead of error
response = http.Response(responsecode.REQUESTED_RANGE_NOT_SATISFIABLE)
response.headers.setHeader("content-range", ('bytes', None, None, oldresponse.stream.length))
return response
def makeSegment(inputStream, lastOffset, start, end):
offset = start - lastOffset
length = end + 1 - start
if offset != 0:
before, inputStream = inputStream.split(offset)
before.close()
return inputStream.split(length)
def rangefilter(request, oldresponse):
if oldresponse.stream is None:
return oldresponse
size = oldresponse.stream.length
if size is None:
# Does not deal with indeterminate length outputs
return oldresponse
oldresponse.headers.setHeader('accept-ranges', ('bytes',))
rangespec = request.headers.getHeader('range')
# If we've got a range header and the If-Range header check passes, and
# the range type is bytes, do a partial response.
if (
rangespec is not None and http.checkIfRange(request, oldresponse) and
rangespec[0] == 'bytes'
):
# If it's a single range, return a simple response
if len(rangespec[1]) == 1:
try:
start, end = canonicalizeRange(rangespec[1][0], size)
except UnsatisfiableRangeRequest:
return makeUnsatisfiable(request, oldresponse)
response = http.Response(responsecode.PARTIAL_CONTENT, oldresponse.headers)
response.headers.setHeader('content-range', ('bytes', start, end, size))
content, after = makeSegment(oldresponse.stream, 0, start, end)
after.close()
response.stream = content
return response
else:
# Return a multipart/byteranges response
lastOffset = -1
offsetList = []
for arange in rangespec[1]:
try:
start, end = canonicalizeRange(arange, size)
except UnsatisfiableRangeRequest:
continue
if start <= lastOffset:
# Stupid client asking for out-of-order or overlapping ranges, PUNT!
return oldresponse
offsetList.append((start, end))
lastOffset = end
if not offsetList:
return makeUnsatisfiable(request, oldresponse)
content_type = oldresponse.headers.getRawHeaders('content-type')
boundary = "%x%x" % (int(time.time() * 1000000), os.getpid())
response = http.Response(responsecode.PARTIAL_CONTENT, oldresponse.headers)
response.headers.setHeader(
'content-type',
http_headers.MimeType('multipart', 'byteranges',
[('boundary', boundary)])
)
response.stream = out = stream.CompoundStream()
lastOffset = 0
origStream = oldresponse.stream
headerString = "\r\n--%s" % boundary
if len(content_type) == 1:
headerString += '\r\nContent-Type: %s' % content_type[0]
headerString += "\r\nContent-Range: %s\r\n\r\n"
for start, end in offsetList:
out.addStream(
headerString %
http_headers.generateContentRange(('bytes', start, end, size))
)
content, origStream = makeSegment(origStream, lastOffset, start, end)
lastOffset = end + 1
out.addStream(content)
origStream.close()
out.addStream("\r\n--%s--\r\n" % boundary)
return response
else:
return oldresponse
__all__ = ['rangefilter']
|