blueprint Multi-range support implementation
This change adds multi range retrieval to OpenStack Swift. For non- segmented data object, a client can use HTTP Range header to specify multiple ranges to retrieve sections of the data object. This implementation currently does not support segmented data object multi range retrieval. When a client sends a multi range request against a segmented data object, Swift will return HTTP status code 200. Support for segmented data multi range retrieval will be added in near future. This implementation is to bring Swift closer to CDMI multi range data retrieval standard. Once support for segemented data multi range is added, Swift will be compliant with CDMI standard in this area. DocImpact Change-Id: I4ed1fb0a0a93c037ddb2f551ea62afe447945107
This commit is contained in:
parent
c7948ec5d9
commit
ce274b3532
227
swift/common/swob.py
Normal file → Executable file
227
swift/common/swob.py
Normal file → Executable file
@ -30,6 +30,7 @@ from email.utils import parsedate
|
||||
import urlparse
|
||||
import urllib2
|
||||
import re
|
||||
import random
|
||||
|
||||
from swift.common.utils import reiterate
|
||||
|
||||
@ -440,6 +441,22 @@ class Range(object):
|
||||
invalid byte-range-spec values MUST ignore the header field that includes
|
||||
that byte-range-set."
|
||||
|
||||
According to the RFC 2616 specification, the following cases will be all
|
||||
considered as syntactically invalid, thus, a ValueError is thrown so that
|
||||
the range header will be ignored. If the range value contains at least
|
||||
one of the following cases, the entire range is considered invalid,
|
||||
ValueError will be thrown so that the header will be ignored.
|
||||
|
||||
1. value not starts with bytes=
|
||||
2. range value start is greater than the end, eg. bytes=5-3
|
||||
3. range does not have start or end, eg. bytes=-
|
||||
4. range does not have hyphen, eg. bytes=45
|
||||
5. range value is non numeric
|
||||
6. any combination of the above
|
||||
|
||||
Every syntactically valid range will be added into the ranges list
|
||||
even when some of the ranges may not be satisfied by underlying content.
|
||||
|
||||
:param headerval: value of the header as a str
|
||||
"""
|
||||
def __init__(self, headerval):
|
||||
@ -448,22 +465,26 @@ class Range(object):
|
||||
raise ValueError('Invalid Range header: %s' % headerval)
|
||||
self.ranges = []
|
||||
for rng in headerval[6:].split(','):
|
||||
# Check if the range has required hyphen.
|
||||
if rng.find('-') == -1:
|
||||
raise ValueError('Invalid Range header: %s' % headerval)
|
||||
start, end = rng.split('-', 1)
|
||||
if start:
|
||||
# when start contains non numeric value, this also causes
|
||||
# ValueError
|
||||
start = int(start)
|
||||
else:
|
||||
start = None
|
||||
if end:
|
||||
# when end contains non numeric value, this also causes
|
||||
# ValueError
|
||||
end = int(end)
|
||||
if start is not None and not end >= start:
|
||||
# If the last-byte-pos value is present, it MUST be greater
|
||||
# than or equal to the first-byte-pos in that
|
||||
# byte-range-spec, or the byte- range-spec is syntactically
|
||||
# invalid. [which "MUST" be ignored]
|
||||
self.ranges = []
|
||||
break
|
||||
if start is not None and end < start:
|
||||
raise ValueError('Invalid Range header: %s' % headerval)
|
||||
else:
|
||||
end = None
|
||||
if start is None:
|
||||
raise ValueError('Invalid Range header: %s' % headerval)
|
||||
self.ranges.append((start, end))
|
||||
|
||||
def __str__(self):
|
||||
@ -477,38 +498,68 @@ class Range(object):
|
||||
string += ','
|
||||
return string.rstrip(',')
|
||||
|
||||
def range_for_length(self, length):
|
||||
def ranges_for_length(self, length):
|
||||
"""
|
||||
range_for_length is used to determine the correct range of bytes to
|
||||
serve from a body, given body length argument and the Range's ranges.
|
||||
This method is used to return multiple ranges for a given length
|
||||
which should represent the length of the underlying content.
|
||||
The constructor method __init__ made sure that any range in ranges
|
||||
list is syntactically valid. So if length is None or size of the
|
||||
ranges is zero, then the Range header should be ignored which will
|
||||
eventually make the response to be 200.
|
||||
|
||||
A limitation of this method is that it can't handle multiple ranges,
|
||||
for compatibility with webob. This should be fairly easy to extend.
|
||||
If an empty list is returned by this method, it indicates that there
|
||||
are unsatisfiable ranges found in the Range header, 416 will be
|
||||
returned.
|
||||
|
||||
:param length: length of the response body
|
||||
if a returned list has at least one element, the list indicates that
|
||||
there is at least one range valid and the server should serve the
|
||||
request with a 206 status code.
|
||||
|
||||
The start value of each range represents the starting position in
|
||||
the content, the end value represents the ending position. This
|
||||
method purposely adds 1 to the end number because the spec defines
|
||||
the Range to be inclusive.
|
||||
|
||||
The Range spec can be found at the following link:
|
||||
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
|
||||
|
||||
:param length: length of the underlying content
|
||||
"""
|
||||
if length is None or not self.ranges or len(self.ranges) != 1:
|
||||
# not syntactically valid ranges, must ignore
|
||||
if length is None or not self.ranges or self.ranges == []:
|
||||
return None
|
||||
begin, end = self.ranges[0]
|
||||
if begin is None:
|
||||
if end == 0:
|
||||
return None
|
||||
if end > length:
|
||||
return (0, length)
|
||||
return (length - end, length)
|
||||
if end is None:
|
||||
if begin < length:
|
||||
# If a syntactically valid byte-range-set includes at least one
|
||||
# byte-range-spec whose first-byte-pos is LESS THAN THE CURRENT
|
||||
# LENGTH OF THE ENTITY-BODY..., then the byte-range-set is
|
||||
# satisfiable.
|
||||
return (begin, length)
|
||||
else:
|
||||
# Otherwise, the byte-range-set is unsatisfiable.
|
||||
return None
|
||||
if begin > length:
|
||||
return None
|
||||
return (begin, min(end + 1, length))
|
||||
all_ranges = []
|
||||
for single_range in self.ranges:
|
||||
begin, end = single_range
|
||||
# The possible values for begin and end are
|
||||
# None, 0, or a positive numeric number
|
||||
if begin is None:
|
||||
if end == 0:
|
||||
# this is the bytes=-0 case
|
||||
continue
|
||||
elif end > length:
|
||||
# This is the case where the end is greater than the
|
||||
# content length, as the RFC 2616 stated, the entire
|
||||
# content should be returned.
|
||||
all_ranges.append((0, length))
|
||||
else:
|
||||
all_ranges.append((length - end, length))
|
||||
continue
|
||||
# begin can only be 0 and numeric value from this point on
|
||||
if end is None:
|
||||
if begin < length:
|
||||
all_ranges.append((begin, length))
|
||||
else:
|
||||
# the begin position is greater than or equal to the
|
||||
# content length; skip and move on to the next range
|
||||
continue
|
||||
# end can only be 0 or numeric value
|
||||
elif begin < length:
|
||||
# the begin position is valid, take the min of end + 1 or
|
||||
# the total length of the content
|
||||
all_ranges.append((begin, min(end + 1, length)))
|
||||
|
||||
return all_ranges
|
||||
|
||||
|
||||
class Match(object):
|
||||
@ -759,6 +810,25 @@ class Request(object):
|
||||
app_iter=app_iter, request=self)
|
||||
|
||||
|
||||
def content_range_header(start, stop, size, value_only=True):
|
||||
if value_only:
|
||||
range_str = 'bytes %s-%s/%s'
|
||||
else:
|
||||
range_str = 'Content-Range: bytes %s-%s/%s'
|
||||
return range_str % (start, (stop - 1), size)
|
||||
|
||||
|
||||
def multi_range_iterator(ranges, content_type, boundary, size, sub_iter_gen):
|
||||
for start, stop in ranges:
|
||||
yield ''.join(['\r\n--', boundary, '\r\n',
|
||||
'Content-Type: ', content_type, '\r\n'])
|
||||
yield content_range_header(start, stop, size, False) + '\r\n\r\n'
|
||||
sub_iter = sub_iter_gen(start, stop)
|
||||
for chunk in sub_iter:
|
||||
yield chunk
|
||||
yield '\r\n--' + boundary + '--\r\n'
|
||||
|
||||
|
||||
class Response(object):
|
||||
"""
|
||||
WSGI Response object.
|
||||
@ -783,6 +853,7 @@ class Response(object):
|
||||
self.body = body
|
||||
self.app_iter = app_iter
|
||||
self.status = status
|
||||
self.boundary = "%x" % random.randint(0, 256 ** 16)
|
||||
if request:
|
||||
self.environ = request.environ
|
||||
if request.range and self.status == 200:
|
||||
@ -793,6 +864,35 @@ class Response(object):
|
||||
for key, value in kw.iteritems():
|
||||
setattr(self, key, value)
|
||||
|
||||
def _prepare_for_ranges(self, ranges):
|
||||
"""
|
||||
Prepare the Response for multiple ranges.
|
||||
"""
|
||||
|
||||
content_size = self.content_length
|
||||
content_type = self.content_type
|
||||
self.content_type = ''.join(['multipart/byteranges;',
|
||||
'boundary=', self.boundary])
|
||||
|
||||
# This section calculate the total size of the targeted response
|
||||
# The value 12 is the length of total bytes of hyphen, new line
|
||||
# form feed for each section header. The value 8 is the length of
|
||||
# total bytes of hyphen, new line, form feed characters for the
|
||||
# closing boundary which appears only once
|
||||
section_header_fixed_len = 12 + (len(self.boundary) +
|
||||
len('Content-Type: ') +
|
||||
len(content_type) +
|
||||
len('Content-Range: bytes '))
|
||||
body_size = 0
|
||||
for start, end in ranges:
|
||||
body_size += section_header_fixed_len
|
||||
body_size += len(str(start) + '-' + str(end - 1) + '/' +
|
||||
str(content_size)) + (end - start)
|
||||
body_size += 8 + len(self.boundary)
|
||||
self.content_length = body_size
|
||||
self.content_range = None
|
||||
return content_size, content_type
|
||||
|
||||
def _response_iter(self, app_iter, body):
|
||||
if self.request and self.request.method == 'HEAD':
|
||||
# We explicitly do NOT want to set self.content_length to 0 here
|
||||
@ -800,23 +900,54 @@ class Response(object):
|
||||
if self.conditional_response and self.request and \
|
||||
self.request.range and self.request.range.ranges and \
|
||||
not self.content_range:
|
||||
args = self.request.range.range_for_length(self.content_length)
|
||||
if not args:
|
||||
ranges = self.request.range.ranges_for_length(self.content_length)
|
||||
if ranges == []:
|
||||
self.status = 416
|
||||
self.content_length = 0
|
||||
return ['']
|
||||
else:
|
||||
start, end = args
|
||||
self.status = 206
|
||||
self.content_range = self.request.range
|
||||
self.content_length = (end - start)
|
||||
if app_iter and hasattr(app_iter, 'app_iter_range'):
|
||||
return app_iter.app_iter_range(start, end)
|
||||
elif app_iter:
|
||||
# this could be improved, but we don't actually use it
|
||||
return [''.join(app_iter)[start:end]]
|
||||
elif body:
|
||||
return [body[start:end]]
|
||||
elif ranges:
|
||||
range_size = len(ranges)
|
||||
if range_size > 0:
|
||||
# There is at least one valid range in the request, so try
|
||||
# to satisfy the request
|
||||
if range_size == 1:
|
||||
start, end = ranges[0]
|
||||
if app_iter and hasattr(app_iter, 'app_iter_range'):
|
||||
self.status = 206
|
||||
self.content_range = \
|
||||
content_range_header(start, end,
|
||||
self.content_length,
|
||||
True)
|
||||
self.content_length = (end - start)
|
||||
return app_iter.app_iter_range(start, end)
|
||||
elif body:
|
||||
self.status = 206
|
||||
self.content_range = \
|
||||
content_range_header(start, end,
|
||||
self.content_length,
|
||||
True)
|
||||
self.content_length = (end - start)
|
||||
return [body[start:end]]
|
||||
elif range_size > 1:
|
||||
if app_iter and hasattr(app_iter, 'app_iter_ranges'):
|
||||
self.status = 206
|
||||
content_size, content_type = \
|
||||
self._prepare_for_ranges(ranges)
|
||||
return app_iter.app_iter_ranges(ranges,
|
||||
content_type,
|
||||
self.boundary,
|
||||
content_size)
|
||||
elif body:
|
||||
self.status = 206
|
||||
content_size, content_type, = \
|
||||
self._prepare_for_ranges(ranges)
|
||||
|
||||
def _body_slicer(start, stop):
|
||||
yield body[start:stop]
|
||||
return multi_range_iterator(ranges, content_type,
|
||||
self.boundary,
|
||||
content_size,
|
||||
_body_slicer)
|
||||
if app_iter:
|
||||
return app_iter
|
||||
if body:
|
||||
|
23
swift/obj/server.py
Normal file → Executable file
23
swift/obj/server.py
Normal file → Executable file
@ -46,7 +46,7 @@ from swift.common.swob import HTTPAccepted, HTTPBadRequest, HTTPCreated, \
|
||||
HTTPInternalServerError, HTTPNoContent, HTTPNotFound, HTTPNotModified, \
|
||||
HTTPPreconditionFailed, HTTPRequestTimeout, HTTPUnprocessableEntity, \
|
||||
HTTPClientDisconnect, HTTPMethodNotAllowed, Request, Response, UTC, \
|
||||
HTTPInsufficientStorage
|
||||
HTTPInsufficientStorage, multi_range_iterator
|
||||
|
||||
|
||||
DATADIR = 'objects'
|
||||
@ -128,6 +128,7 @@ class DiskFile(object):
|
||||
self.read_to_eof = False
|
||||
self.quarantined_dir = None
|
||||
self.keep_cache = False
|
||||
self.suppress_file_closing = False
|
||||
if not os.path.exists(self.datadir):
|
||||
return
|
||||
files = sorted(os.listdir(self.datadir), reverse=True)
|
||||
@ -183,11 +184,12 @@ class DiskFile(object):
|
||||
read - dropped_cache)
|
||||
break
|
||||
finally:
|
||||
self.close()
|
||||
if not self.suppress_file_closing:
|
||||
self.close()
|
||||
|
||||
def app_iter_range(self, start, stop):
|
||||
"""Returns an iterator over the data file for range (start, stop)"""
|
||||
if start:
|
||||
if start or start == 0:
|
||||
self.fp.seek(start)
|
||||
if stop is not None:
|
||||
length = stop - start
|
||||
@ -202,6 +204,21 @@ class DiskFile(object):
|
||||
break
|
||||
yield chunk
|
||||
|
||||
def app_iter_ranges(self, ranges, content_type, boundary, size):
|
||||
"""Returns an iterator over the data file for a set of ranges"""
|
||||
if (not ranges or len(ranges) == 0):
|
||||
yield ''
|
||||
else:
|
||||
try:
|
||||
self.suppress_file_closing = True
|
||||
for chunk in multi_range_iterator(
|
||||
ranges, content_type, boundary, size,
|
||||
self.app_iter_range):
|
||||
yield chunk
|
||||
finally:
|
||||
self.suppress_file_closing = False
|
||||
self.close()
|
||||
|
||||
def _handle_close_quarantine(self):
|
||||
"""Check if file needs to be quarantined"""
|
||||
try:
|
||||
|
208
test/unit/common/test_swob.py
Normal file → Executable file
208
test/unit/common/test_swob.py
Normal file → Executable file
@ -17,6 +17,7 @@
|
||||
|
||||
import unittest
|
||||
import datetime
|
||||
import re
|
||||
from StringIO import StringIO
|
||||
|
||||
import swift.common.swob
|
||||
@ -108,7 +109,7 @@ class TestRange(unittest.TestCase):
|
||||
|
||||
def test_upsidedown_range(self):
|
||||
range = swift.common.swob.Range('bytes=5-10')
|
||||
self.assertEquals(range.range_for_length(2), None)
|
||||
self.assertEquals(range.ranges_for_length(2), [])
|
||||
|
||||
def test_str(self):
|
||||
for range_str in ('bytes=1-7', 'bytes=1-', 'bytes=-1',
|
||||
@ -116,31 +117,94 @@ class TestRange(unittest.TestCase):
|
||||
range = swift.common.swob.Range(range_str)
|
||||
self.assertEquals(str(range), range_str)
|
||||
|
||||
def test_range_for_length(self):
|
||||
def test_ranges_for_length(self):
|
||||
range = swift.common.swob.Range('bytes=1-7')
|
||||
self.assertEquals(range.range_for_length(10), (1, 8))
|
||||
self.assertEquals(range.range_for_length(5), (1, 5))
|
||||
self.assertEquals(range.range_for_length(None), None)
|
||||
self.assertEquals(range.ranges_for_length(10), [(1, 8)])
|
||||
self.assertEquals(range.ranges_for_length(5), [(1, 5)])
|
||||
self.assertEquals(range.ranges_for_length(None), None)
|
||||
|
||||
def test_range_for_length_no_end(self):
|
||||
def test_ranges_for_large_length(self):
|
||||
range = swift.common.swob.Range('bytes=-1000000000000000000000000000')
|
||||
self.assertEquals(range.ranges_for_length(100), [(0, 100)])
|
||||
|
||||
def test_ranges_for_length_no_end(self):
|
||||
range = swift.common.swob.Range('bytes=1-')
|
||||
self.assertEquals(range.range_for_length(10), (1, 10))
|
||||
self.assertEquals(range.range_for_length(5), (1, 5))
|
||||
self.assertEquals(range.range_for_length(None), None)
|
||||
self.assertEquals(range.ranges_for_length(10), [(1, 10)])
|
||||
self.assertEquals(range.ranges_for_length(5), [(1, 5)])
|
||||
self.assertEquals(range.ranges_for_length(None), None)
|
||||
# This used to freak out:
|
||||
range = swift.common.swob.Range('bytes=100-')
|
||||
self.assertEquals(range.range_for_length(5), None)
|
||||
self.assertEquals(range.range_for_length(None), None)
|
||||
self.assertEquals(range.ranges_for_length(5), [])
|
||||
self.assertEquals(range.ranges_for_length(None), None)
|
||||
|
||||
def test_range_for_length_no_start(self):
|
||||
range = swift.common.swob.Range('bytes=4-6,100-')
|
||||
self.assertEquals(range.ranges_for_length(5), [(4, 5)])
|
||||
|
||||
def test_ranges_for_length_no_start(self):
|
||||
range = swift.common.swob.Range('bytes=-7')
|
||||
self.assertEquals(range.range_for_length(10), (3, 10))
|
||||
self.assertEquals(range.range_for_length(5), (0, 5))
|
||||
self.assertEquals(range.range_for_length(None), None)
|
||||
self.assertEquals(range.ranges_for_length(10), [(3, 10)])
|
||||
self.assertEquals(range.ranges_for_length(5), [(0, 5)])
|
||||
self.assertEquals(range.ranges_for_length(None), None)
|
||||
|
||||
range = swift.common.swob.Range('bytes=4-6,-100')
|
||||
self.assertEquals(range.ranges_for_length(5), [(4, 5), (0, 5)])
|
||||
|
||||
def test_ranges_for_length_multi(self):
|
||||
range = swift.common.swob.Range('bytes=-20,4-,30-150,-10')
|
||||
# the length of the ranges should be 4
|
||||
self.assertEquals(len(range.ranges_for_length(200)), 4)
|
||||
|
||||
# the actual length less than any of the range
|
||||
self.assertEquals(range.ranges_for_length(90),
|
||||
[(70, 90), (4, 90), (30, 90), (80, 90)])
|
||||
|
||||
# the actual length greater than any of the range
|
||||
self.assertEquals(range.ranges_for_length(200),
|
||||
[(180, 200), (4, 200), (30, 151), (190, 200)])
|
||||
|
||||
self.assertEquals(range.ranges_for_length(None), None)
|
||||
|
||||
def test_ranges_for_length_edges(self):
|
||||
range = swift.common.swob.Range('bytes=0-1, -7')
|
||||
self.assertEquals(range.ranges_for_length(10),
|
||||
[(0, 2), (3, 10)])
|
||||
|
||||
range = swift.common.swob.Range('bytes=-7, 0-1')
|
||||
self.assertEquals(range.ranges_for_length(10),
|
||||
[(3, 10), (0, 2)])
|
||||
|
||||
range = swift.common.swob.Range('bytes=-7, 0-1')
|
||||
self.assertEquals(range.ranges_for_length(5),
|
||||
[(0, 5), (0, 2)])
|
||||
|
||||
def test_range_invalid_syntax(self):
|
||||
range = swift.common.swob.Range('bytes=10-2')
|
||||
self.assertEquals(range.ranges, [])
|
||||
|
||||
def _check_invalid_range(range_value):
|
||||
try:
|
||||
swift.common.swob.Range(range_value)
|
||||
return False
|
||||
except ValueError:
|
||||
return True
|
||||
|
||||
"""
|
||||
All the following cases should result ValueError exception
|
||||
1. value not starts with bytes=
|
||||
2. range value start is greater than the end, eg. bytes=5-3
|
||||
3. range does not have start or end, eg. bytes=-
|
||||
4. range does not have hyphen, eg. bytes=45
|
||||
5. range value is non numeric
|
||||
6. any combination of the above
|
||||
"""
|
||||
|
||||
self.assert_(_check_invalid_range('nonbytes=foobar,10-2'))
|
||||
self.assert_(_check_invalid_range('bytes=5-3'))
|
||||
self.assert_(_check_invalid_range('bytes=-'))
|
||||
self.assert_(_check_invalid_range('bytes=45'))
|
||||
self.assert_(_check_invalid_range('bytes=foo-bar,3-5'))
|
||||
self.assert_(_check_invalid_range('bytes=4-10,45'))
|
||||
self.assert_(_check_invalid_range('bytes=foobar,3-5'))
|
||||
self.assert_(_check_invalid_range('bytes=nonumber-5'))
|
||||
self.assert_(_check_invalid_range('bytes=nonumber'))
|
||||
|
||||
|
||||
class TestMatch(unittest.TestCase):
|
||||
@ -387,6 +451,106 @@ class TestResponse(unittest.TestCase):
|
||||
body = ''.join(resp({}, start_response))
|
||||
self.assertEquals(body, 'abc')
|
||||
|
||||
def test_multi_ranges_wo_iter_ranges(self):
|
||||
def test_app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Length', '10')])
|
||||
return ['1234567890']
|
||||
|
||||
req = swift.common.swob.Request.blank(
|
||||
'/', headers={'Range': 'bytes=0-9,10-19,20-29'})
|
||||
|
||||
resp = req.get_response(test_app)
|
||||
resp.conditional_response = True
|
||||
resp.content_length = 10
|
||||
|
||||
content = ''.join(resp._response_iter(resp.app_iter, ''))
|
||||
|
||||
self.assertEquals(resp.status, '200 OK')
|
||||
self.assertEqual(10, resp.content_length)
|
||||
|
||||
def test_single_range_wo_iter_range(self):
|
||||
def test_app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Length', '10')])
|
||||
return ['1234567890']
|
||||
|
||||
req = swift.common.swob.Request.blank(
|
||||
'/', headers={'Range': 'bytes=0-9'})
|
||||
|
||||
resp = req.get_response(test_app)
|
||||
resp.conditional_response = True
|
||||
resp.content_length = 10
|
||||
|
||||
content = ''.join(resp._response_iter(resp.app_iter, ''))
|
||||
|
||||
self.assertEquals(resp.status, '200 OK')
|
||||
self.assertEqual(10, resp.content_length)
|
||||
|
||||
def test_multi_range_body(self):
|
||||
def test_app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Length', '4')])
|
||||
return ['abcd']
|
||||
|
||||
req = swift.common.swob.Request.blank(
|
||||
'/', headers={'Range': 'bytes=0-9,10-19,20-29'})
|
||||
|
||||
resp = req.get_response(test_app)
|
||||
resp.conditional_response = True
|
||||
resp.content_length = 100
|
||||
|
||||
resp.content_type = 'text/plain'
|
||||
content = ''.join(resp._response_iter(None,
|
||||
('0123456789112345678'
|
||||
'92123456789')))
|
||||
|
||||
self.assert_(re.match(('\r\n'
|
||||
'--[a-f0-9]{32}\r\n'
|
||||
'Content-Type: text/plain\r\n'
|
||||
'Content-Range: bytes '
|
||||
'0-9/100\r\n\r\n0123456789\r\n'
|
||||
'--[a-f0-9]{32}\r\n'
|
||||
'Content-Type: text/plain\r\n'
|
||||
'Content-Range: bytes '
|
||||
'10-19/100\r\n\r\n1123456789\r\n'
|
||||
'--[a-f0-9]{32}\r\n'
|
||||
'Content-Type: text/plain\r\n'
|
||||
'Content-Range: bytes '
|
||||
'20-29/100\r\n\r\n2123456789\r\n'
|
||||
'--[a-f0-9]{32}--\r\n'), content))
|
||||
|
||||
def test_multi_response_iter(self):
|
||||
def test_app(environ, start_response):
|
||||
start_response('200 OK', [('Content-Length', '10'),
|
||||
('Content-Type', 'application/xml')])
|
||||
return ['0123456789']
|
||||
|
||||
app_iter_ranges_args = []
|
||||
|
||||
class App_iter(object):
|
||||
def app_iter_ranges(self, ranges, content_type, boundary, size):
|
||||
app_iter_ranges_args.append((ranges, content_type, boundary,
|
||||
size))
|
||||
for i in xrange(3):
|
||||
yield str(i) + 'fun'
|
||||
yield boundary
|
||||
|
||||
def __iter__(self):
|
||||
for i in xrange(3):
|
||||
yield str(i) + 'fun'
|
||||
|
||||
req = swift.common.swob.Request.blank(
|
||||
'/', headers={'Range': 'bytes=1-5,8-11'})
|
||||
|
||||
resp = req.get_response(test_app)
|
||||
resp.conditional_response = True
|
||||
resp.content_length = 12
|
||||
|
||||
content = ''.join(resp._response_iter(App_iter(), ''))
|
||||
boundary = content[-32:]
|
||||
self.assertEqual(content[:-32], '0fun1fun2fun')
|
||||
self.assertEqual(app_iter_ranges_args,
|
||||
[([(1, 6), (8, 12)], 'application/xml',
|
||||
boundary, 12)])
|
||||
|
||||
def test_range_body(self):
|
||||
|
||||
def test_app(environ, start_response):
|
||||
@ -398,20 +562,17 @@ class TestResponse(unittest.TestCase):
|
||||
|
||||
req = swift.common.swob.Request.blank(
|
||||
'/', headers={'Range': 'bytes=1-3'})
|
||||
resp = req.get_response(test_app)
|
||||
resp.conditional_response = True
|
||||
body = ''.join(resp([], start_response))
|
||||
self.assertEquals(body, '234')
|
||||
self.assertEquals(resp.status, '206 Partial Content')
|
||||
|
||||
resp = swift.common.swob.Response(
|
||||
body='1234567890', request=req,
|
||||
conditional_response=True)
|
||||
body = ''.join(resp([], start_response))
|
||||
self.assertEquals(body, '234')
|
||||
self.assertEquals(resp.content_range, 'bytes 1-3/10')
|
||||
self.assertEquals(resp.status, '206 Partial Content')
|
||||
|
||||
# No body for 416
|
||||
# syntactically valid, but does not make sense, so returning 416
|
||||
# in next couple of cases.
|
||||
req = swift.common.swob.Request.blank(
|
||||
'/', headers={'Range': 'bytes=-0'})
|
||||
resp = req.get_response(test_app)
|
||||
@ -426,6 +587,7 @@ class TestResponse(unittest.TestCase):
|
||||
conditional_response=True)
|
||||
body = ''.join(resp([], start_response))
|
||||
self.assertEquals(body, '')
|
||||
self.assertEquals(resp.content_length, 0)
|
||||
self.assertEquals(resp.status, '416 Requested Range Not Satisfiable')
|
||||
|
||||
# Syntactically-invalid Range headers "MUST" be ignored
|
||||
|
85
test/unit/obj/test_server.py
Normal file → Executable file
85
test/unit/obj/test_server.py
Normal file → Executable file
@ -18,6 +18,7 @@
|
||||
import cPickle as pickle
|
||||
import os
|
||||
import unittest
|
||||
import email
|
||||
from shutil import rmtree
|
||||
from StringIO import StringIO
|
||||
from time import gmtime, sleep, strftime, time
|
||||
@ -55,31 +56,89 @@ class TestDiskFile(unittest.TestCase):
|
||||
""" Tear down for testing swift.object_server.ObjectController """
|
||||
rmtree(os.path.dirname(self.testdir))
|
||||
|
||||
def test_disk_file_app_iter_corners(self):
|
||||
def _create_test_file(self, data, keep_data_fp=True):
|
||||
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
|
||||
FakeLogger())
|
||||
mkdirs(df.datadir)
|
||||
f = open(os.path.join(df.datadir,
|
||||
normalize_timestamp(time()) + '.data'), 'wb')
|
||||
f.write('1234567890')
|
||||
f.write(data)
|
||||
setxattr(f.fileno(), object_server.METADATA_KEY,
|
||||
pickle.dumps({}, object_server.PICKLE_PROTOCOL))
|
||||
f.close()
|
||||
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
|
||||
FakeLogger(), keep_data_fp=True)
|
||||
it = df.app_iter_range(0, None)
|
||||
sio = StringIO()
|
||||
for chunk in it:
|
||||
sio.write(chunk)
|
||||
self.assertEquals(sio.getvalue(), '1234567890')
|
||||
FakeLogger(), keep_data_fp=keep_data_fp)
|
||||
return df
|
||||
|
||||
def test_disk_file_app_iter_corners(self):
|
||||
df = self._create_test_file('1234567890')
|
||||
self.assertEquals(''.join(df.app_iter_range(0, None)), '1234567890')
|
||||
|
||||
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
|
||||
FakeLogger(), keep_data_fp=True)
|
||||
it = df.app_iter_range(5, None)
|
||||
sio = StringIO()
|
||||
for chunk in it:
|
||||
sio.write(chunk)
|
||||
self.assertEquals(sio.getvalue(), '67890')
|
||||
self.assertEqual(''.join(df.app_iter_range(5, None)), '67890')
|
||||
|
||||
def test_disk_file_app_iter_ranges(self):
|
||||
df = self._create_test_file('012345678911234567892123456789')
|
||||
it = df.app_iter_ranges([(0, 10), (10, 20), (20, 30)], 'plain/text',
|
||||
'\r\n--someheader\r\n', 30)
|
||||
value = ''.join(it)
|
||||
self.assert_('0123456789' in value)
|
||||
self.assert_('1123456789' in value)
|
||||
self.assert_('2123456789' in value)
|
||||
|
||||
def test_disk_file_app_iter_ranges_edges(self):
|
||||
df = self._create_test_file('012345678911234567892123456789')
|
||||
it = df.app_iter_ranges([(3, 10), (0, 2)], 'application/whatever',
|
||||
'\r\n--someheader\r\n', 30)
|
||||
value = ''.join(it)
|
||||
self.assert_('3456789' in value)
|
||||
self.assert_('01' in value)
|
||||
|
||||
def test_disk_file_large_app_iter_ranges(self):
|
||||
"""
|
||||
This test case is to make sure that the disk file app_iter_ranges
|
||||
method all the paths being tested.
|
||||
"""
|
||||
long_str = '01234567890' * 65536
|
||||
target_strs = ['3456789', long_str[0:65590]]
|
||||
df = self._create_test_file(long_str)
|
||||
|
||||
it = df.app_iter_ranges([(3, 10), (0, 65590)], 'plain/text',
|
||||
'5e816ff8b8b8e9a5d355497e5d9e0301', 655360)
|
||||
|
||||
"""
|
||||
the produced string actually missing the MIME headers
|
||||
need to add these headers to make it as real MIME message.
|
||||
The body of the message is produced by method app_iter_ranges
|
||||
off of DiskFile object.
|
||||
"""
|
||||
header = ''.join(['Content-Type: multipart/byteranges;',
|
||||
'boundary=',
|
||||
'5e816ff8b8b8e9a5d355497e5d9e0301\r\n'])
|
||||
|
||||
value = header + ''.join(it)
|
||||
|
||||
parts = map(lambda p: p.get_payload(decode=True),
|
||||
email.message_from_string(value).walk())[1:3]
|
||||
self.assertEqual(parts, target_strs)
|
||||
|
||||
def test_disk_file_app_iter_ranges_empty(self):
|
||||
"""
|
||||
This test case tests when empty value passed into app_iter_ranges
|
||||
When ranges passed into the method is either empty array or None,
|
||||
this method will yield empty string
|
||||
"""
|
||||
df = self._create_test_file('012345678911234567892123456789')
|
||||
it = df.app_iter_ranges([], 'application/whatever',
|
||||
'\r\n--someheader\r\n', 100)
|
||||
self.assertEqual(''.join(it), '')
|
||||
|
||||
df = object_server.DiskFile(self.testdir, 'sda1', '0', 'a', 'c', 'o',
|
||||
FakeLogger(), keep_data_fp=True)
|
||||
it = df.app_iter_ranges(None, 'app/something',
|
||||
'\r\n--someheader\r\n', 150)
|
||||
self.assertEqual(''.join(it), '')
|
||||
|
||||
def test_disk_file_mkstemp_creates_dir(self):
|
||||
tmpdir = os.path.join(self.testdir, 'sda1', 'tmp')
|
||||
|
4
test/unit/proxy/test_server.py
Normal file → Executable file
4
test/unit/proxy/test_server.py
Normal file → Executable file
@ -4450,9 +4450,9 @@ class FakeObjectController(object):
|
||||
path = args[4]
|
||||
body = data = path[-1] * int(path[-1])
|
||||
if req.range:
|
||||
r = req.range.range_for_length(len(data))
|
||||
r = req.range.ranges_for_length(len(data))
|
||||
if r:
|
||||
(start, stop) = r
|
||||
(start, stop) = r[0]
|
||||
body = data[start:stop]
|
||||
resp = Response(app_iter=iter(body))
|
||||
return resp
|
||||
|
Loading…
Reference in New Issue
Block a user