193
193
:param offsets: A list of (offset, size) tuples.
194
194
:param return: A list or generator of (offset, data) tuples
196
# Ideally we would pass one big request asking for all the ranges in
197
# one go; however then the server will give a multipart mime response
198
# back, and we can't parse them yet. So instead we just get one range
199
# per region, and try to coallesce the regions as much as possible.
201
# The read-coallescing code is not quite regular enough to have a
202
# single driver routine and
203
# helper method in Transport.
204
def do_combined_read(combined_offsets):
205
# read one coalesced block
207
for offset, size in combined_offsets:
209
mutter('readv coalesced %d reads.', len(combined_offsets))
210
offset = combined_offsets[0][0]
211
byte_range = (offset, offset + total_size - 1)
212
code, result_file = self._get(relpath, [byte_range])
214
for off, size in combined_offsets:
215
result_bytes = result_file.read(size)
216
assert len(result_bytes) == size
217
yield off, result_bytes
219
data = result_file.read(offset + total_size)[offset:offset + total_size]
221
for offset, size in combined_offsets:
222
yield offset, data[pos:pos + size]
227
pending_offsets = deque(offsets)
228
combined_offsets = []
229
while len(pending_offsets):
230
offset, size = pending_offsets.popleft()
231
if not combined_offsets:
232
combined_offsets = [[offset, size]]
234
if (len (combined_offsets) < 500 and
235
combined_offsets[-1][0] + combined_offsets[-1][1] == offset):
237
combined_offsets.append([offset, size])
239
# incompatible, or over the threshold issue a read and yield
240
pending_offsets.appendleft((offset, size))
241
for result in do_combined_read(combined_offsets):
243
combined_offsets = []
244
# whatever is left is a single coalesced request
245
if len(combined_offsets):
246
for result in do_combined_read(combined_offsets):
196
mutter('readv of %s [%s]', relpath, offsets)
197
ranges = self._offsets_to_ranges(offsets)
198
code, f = self._get(relpath, ranges)
199
for start, size in offsets:
202
assert len(data) == size
205
def _is_multipart(self, content_type):
206
return content_type.startswith('multipart/byteranges;')
208
def _handle_response(self, path, response):
209
"""Interpret the code & headers and return a HTTP response.
211
This is a factory method which returns an appropriate HTTP response
212
based on the code & headers it's given.
214
content_type = response.headers['Content-Type']
215
mutter('handling response code %s ctype %s', response.code,
218
if response.code == 206 and self._is_multipart(content_type):
219
# Full fledged multipart response
220
return HttpMultipartRangeResponse(path, content_type, response)
221
elif response.code == 206:
222
# A response to a range request, but not multipart
223
content_range = response.headers['Content-Range']
224
return HttpRangeResponse(path, content_range, response)
225
elif response.code == 200:
226
# A regular non-range response, unfortunately the result from
227
# urllib doesn't support seek, so we wrap it in a StringIO
228
return StringIO(response.read())
229
elif response.code == 404:
230
raise NoSuchFile(path)
232
raise BzrError("HTTP couldn't handle code %s", response.code)
249
234
def put(self, relpath, f, mode=None):
250
235
"""Copy the file-like or string object into the location.