Merge "Round s3api listing LastModified to integer resolution"

This commit is contained in:
Zuul 2022-05-10 17:30:48 +00:00 committed by Gerrit Code Review
commit b90338ff45
13 changed files with 379 additions and 211 deletions

View File

@ -34,7 +34,7 @@ from swift.common.middleware.s3api.s3response import \
MalformedXML, InvalidLocationConstraint, NoSuchBucket, \
BucketNotEmpty, VersionedBucketNotEmpty, InternalError, \
ServiceUnavailable, NoSuchKey
from swift.common.middleware.s3api.utils import MULTIUPLOAD_SUFFIX
from swift.common.middleware.s3api.utils import MULTIUPLOAD_SUFFIX, S3Timestamp
MAX_PUT_BUCKET_BODY_SIZE = 10240
@ -291,7 +291,7 @@ class BucketController(Controller):
contents = SubElement(elem, 'Contents')
SubElement(contents, 'Key').text = name
SubElement(contents, 'LastModified').text = \
o['last_modified'][:-3] + 'Z'
S3Timestamp.from_isoformat(o['last_modified']).s3xmlformat
if contents.tag != 'DeleteMarker':
if 's3_etag' in o:
# New-enough MUs are already in the right format

View File

@ -397,7 +397,7 @@ class UploadsController(Controller):
SubElement(owner_elem, 'DisplayName').text = req.user_id
SubElement(upload_elem, 'StorageClass').text = 'STANDARD'
SubElement(upload_elem, 'Initiated').text = \
u['last_modified'][:-3] + 'Z'
S3Timestamp.from_isoformat(u['last_modified']).s3xmlformat
for p in prefixes:
elem = SubElement(result_elem, 'CommonPrefixes')
@ -582,7 +582,7 @@ class UploadController(Controller):
part_elem = SubElement(result_elem, 'Part')
SubElement(part_elem, 'PartNumber').text = i['name'].split('/')[-1]
SubElement(part_elem, 'LastModified').text = \
i['last_modified'][:-3] + 'Z'
S3Timestamp.from_isoformat(i['last_modified']).s3xmlformat
SubElement(part_elem, 'ETag').text = '"%s"' % i['hash']
SubElement(part_elem, 'Size').text = str(i['bytes'])

View File

@ -15,6 +15,7 @@
import base64
import calendar
import datetime
import email.utils
import re
import six
@ -108,9 +109,19 @@ def validate_bucket_name(name, dns_compliant_bucket_names):
class S3Timestamp(utils.Timestamp):
S3_XML_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z"
@property
def s3xmlformat(self):
return self.isoformat[:-7] + '.000Z'
dt = datetime.datetime.utcfromtimestamp(self.ceil())
return dt.strftime(self.S3_XML_FORMAT)
@classmethod
def from_s3xmlformat(cls, date_string):
dt = datetime.datetime.strptime(date_string, cls.S3_XML_FORMAT)
dt = dt.replace(tzinfo=utils.UTC)
seconds = calendar.timegm(dt.timetuple())
return cls(seconds)
@property
def amz_date_format(self):

View File

@ -1324,6 +1324,15 @@ class Timestamp(object):
@property
def isoformat(self):
"""
Get an isoformat string representation of the 'normal' part of the
Timestamp with microsecond precision and no trailing timezone, for
example:
1970-01-01T00:00:00.000000
:return: an isoformat string
"""
t = float(self.normal)
if six.PY3:
# On Python 3, round manually using ROUND_HALF_EVEN rounding
@ -1350,6 +1359,21 @@ class Timestamp(object):
isoformat += ".000000"
return isoformat
@classmethod
def from_isoformat(cls, date_string):
"""
Parse an isoformat string representation of time to a Timestamp object.
:param date_string: a string formatted as per an Timestamp.isoformat
property.
:return: an instance of this class.
"""
start = datetime.datetime.strptime(date_string, "%Y-%m-%dT%H:%M:%S.%f")
delta = start - EPOCH
# This calculation is based on Python 2.7's Modules/datetimemodule.c,
# function delta_to_microseconds(), but written in Python.
return cls(delta.total_seconds())
def ceil(self):
"""
Return the 'normal' part of the timestamp rounded up to the nearest
@ -1506,13 +1530,7 @@ def last_modified_date_to_timestamp(last_modified_date_str):
Convert a last modified date (like you'd get from a container listing,
e.g. 2014-02-28T23:22:36.698390) to a float.
"""
start = datetime.datetime.strptime(last_modified_date_str,
'%Y-%m-%dT%H:%M:%S.%f')
delta = start - EPOCH
# This calculation is based on Python 2.7's Modules/datetimemodule.c,
# function delta_to_microseconds(), but written in Python.
return Timestamp(delta.total_seconds())
return Timestamp.from_isoformat(last_modified_date_str)
def normalize_delete_at_timestamp(timestamp, high_precision=False):

View File

@ -29,7 +29,8 @@ from six.moves import urllib, zip, zip_longest
import test.functional as tf
from swift.common.middleware.s3api.etree import fromstring, tostring, \
Element, SubElement
from swift.common.middleware.s3api.utils import MULTIUPLOAD_SUFFIX, mktime
from swift.common.middleware.s3api.utils import MULTIUPLOAD_SUFFIX, mktime, \
S3Timestamp
from swift.common.utils import md5
from test.functional.s3api import S3ApiBase
@ -213,7 +214,8 @@ class TestS3ApiMultiUpload(S3ApiBase):
self.assertEqual(headers['content-type'], 'text/html; charset=UTF-8')
self.assertTrue('content-length' in headers)
self.assertEqual(headers['content-length'], '0')
expected_parts_list = [(headers['etag'], mktime(headers['date']))]
expected_parts_list = [(headers['etag'],
mktime(headers['last-modified']))]
# Upload Part Copy
key, upload_id = uploads[1]
@ -242,8 +244,8 @@ class TestS3ApiMultiUpload(S3ApiBase):
self.assertTrue('etag' not in headers)
elem = fromstring(body, 'CopyPartResult')
last_modified = elem.find('LastModified').text
self.assertTrue(last_modified is not None)
copy_resp_last_modified = elem.find('LastModified').text
self.assertIsNotNone(copy_resp_last_modified)
self.assertEqual(resp_etag, etag)
@ -256,15 +258,10 @@ class TestS3ApiMultiUpload(S3ApiBase):
self.assertEqual(200, status)
elem = fromstring(body, 'ListPartsResult')
# FIXME: COPY result drops milli/microseconds but GET doesn't
last_modified_gets = [p.find('LastModified').text
listing_last_modified = [p.find('LastModified').text
for p in elem.iterfind('Part')]
self.assertEqual(
last_modified_gets[0].rsplit('.', 1)[0],
last_modified.rsplit('.', 1)[0],
'%r != %r' % (last_modified_gets[0], last_modified))
# There should be *exactly* two parts in the result
self.assertEqual(1, len(last_modified_gets))
# There should be *exactly* one parts in the result
self.assertEqual(listing_last_modified, [copy_resp_last_modified])
# List Parts
key, upload_id = uploads[0]
@ -299,15 +296,10 @@ class TestS3ApiMultiUpload(S3ApiBase):
for (expected_etag, expected_date), p in \
zip(expected_parts_list, elem.findall('Part')):
last_modified = p.find('LastModified').text
self.assertTrue(last_modified is not None)
# TODO: sanity check
# (kota_) How do we check the sanity?
# the last-modified header drops milli-seconds info
# by the constraint of the format.
# For now, we can do either the format check or round check
# last_modified_from_xml = mktime(last_modified)
# self.assertEqual(expected_date,
# last_modified_from_xml)
self.assertIsNotNone(last_modified)
last_modified_from_xml = S3Timestamp.from_s3xmlformat(
last_modified)
self.assertEqual(expected_date, float(last_modified_from_xml))
self.assertEqual(expected_etag, p.find('ETag').text)
self.assertEqual(self.min_segment_size, int(p.find('Size').text))
etags.append(p.find('ETag').text)
@ -496,7 +488,7 @@ class TestS3ApiMultiUpload(S3ApiBase):
self.assertIsNotNone(o.find('LastModified').text)
self.assertRegex(
o.find('LastModified').text,
r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$')
r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.000Z$')
self.assertEqual(o.find('ETag').text, exp_etag)
self.assertEqual(o.find('Size').text, str(exp_size))
self.assertIsNotNone(o.find('StorageClass').text)
@ -932,8 +924,8 @@ class TestS3ApiMultiUpload(S3ApiBase):
self.assertTrue('etag' not in headers)
elem = fromstring(body, 'CopyPartResult')
last_modified = elem.find('LastModified').text
self.assertTrue(last_modified is not None)
copy_resp_last_modified = elem.find('LastModified').text
self.assertIsNotNone(copy_resp_last_modified)
self.assertEqual(resp_etag, etag)
@ -945,16 +937,10 @@ class TestS3ApiMultiUpload(S3ApiBase):
elem = fromstring(body, 'ListPartsResult')
# FIXME: COPY result drops milli/microseconds but GET doesn't
last_modified_gets = [p.find('LastModified').text
listing_last_modified = [p.find('LastModified').text
for p in elem.iterfind('Part')]
self.assertEqual(
last_modified_gets[0].rsplit('.', 1)[0],
last_modified.rsplit('.', 1)[0],
'%r != %r' % (last_modified_gets[0], last_modified))
# There should be *exactly* one parts in the result
self.assertEqual(1, len(last_modified_gets))
self.assertEqual(listing_last_modified, [copy_resp_last_modified])
# Abort Multipart Upload
key, upload_id = uploads[0]
@ -1044,8 +1030,8 @@ class TestS3ApiMultiUpload(S3ApiBase):
self.assertTrue('etag' not in headers)
elem = fromstring(body, 'CopyPartResult')
last_modifieds = [elem.find('LastModified').text]
self.assertTrue(last_modifieds[0] is not None)
copy_resp_last_modifieds = [elem.find('LastModified').text]
self.assertTrue(copy_resp_last_modifieds[0] is not None)
self.assertEqual(resp_etag, etags[0])
@ -1062,8 +1048,8 @@ class TestS3ApiMultiUpload(S3ApiBase):
self.assertTrue('etag' not in headers)
elem = fromstring(body, 'CopyPartResult')
last_modifieds.append(elem.find('LastModified').text)
self.assertTrue(last_modifieds[1] is not None)
copy_resp_last_modifieds.append(elem.find('LastModified').text)
self.assertTrue(copy_resp_last_modifieds[1] is not None)
self.assertEqual(resp_etag, etags[1])
@ -1075,15 +1061,9 @@ class TestS3ApiMultiUpload(S3ApiBase):
elem = fromstring(body, 'ListPartsResult')
# FIXME: COPY result drops milli/microseconds but GET doesn't
last_modified_gets = [p.find('LastModified').text
listing_last_modified = [p.find('LastModified').text
for p in elem.iterfind('Part')]
self.assertEqual(
[lm.rsplit('.', 1)[0] for lm in last_modified_gets],
[lm.rsplit('.', 1)[0] for lm in last_modifieds])
# There should be *exactly* two parts in the result
self.assertEqual(2, len(last_modified_gets))
self.assertEqual(listing_last_modified, copy_resp_last_modifieds)
# Abort Multipart Upload
key, upload_id = uploads[0]

View File

@ -22,6 +22,7 @@ import boto
# pylint: disable-msg=E0611,F0401
from distutils.version import StrictVersion
import calendar
import email.parser
from email.utils import formatdate, parsedate
from time import mktime
@ -30,6 +31,7 @@ import six
import test.functional as tf
from swift.common.middleware.s3api.etree import fromstring
from swift.common.middleware.s3api.utils import S3Timestamp
from swift.common.utils import md5, quote
from test.functional.s3api import S3ApiBase
@ -98,21 +100,32 @@ class TestS3ApiObject(S3ApiBase):
elem = fromstring(body, 'CopyObjectResult')
self.assertTrue(elem.find('LastModified').text is not None)
last_modified_xml = elem.find('LastModified').text
copy_resp_last_modified_xml = elem.find('LastModified').text
self.assertTrue(elem.find('ETag').text is not None)
self.assertEqual(etag, elem.find('ETag').text.strip('"'))
self._assertObjectEtag(dst_bucket, dst_obj, etag)
# Check timestamp on Copy:
# Check timestamp on Copy in listing:
status, headers, body = \
self.conn.make_request('GET', dst_bucket)
self.assertEqual(status, 200)
elem = fromstring(body, 'ListBucketResult')
# FIXME: COPY result drops milli/microseconds but GET doesn't
self.assertEqual(
elem.find('Contents').find("LastModified").text.rsplit('.', 1)[0],
last_modified_xml.rsplit('.', 1)[0])
elem.find('Contents').find("LastModified").text,
copy_resp_last_modified_xml)
# GET Object copy
status, headers, body = \
self.conn.make_request('GET', dst_bucket, dst_obj)
self.assertEqual(status, 200)
self.assertCommonResponseHeaders(headers, etag)
self.assertTrue(headers['last-modified'] is not None)
self.assertEqual(
float(S3Timestamp.from_s3xmlformat(copy_resp_last_modified_xml)),
calendar.timegm(parsedate(headers['last-modified'])))
self.assertTrue(headers['content-type'] is not None)
self.assertEqual(headers['content-length'], str(len(content)))
# GET Object
status, headers, body = \
@ -770,6 +783,26 @@ class TestS3ApiObject(S3ApiBase):
self.assertEqual(status, 200)
self.assertCommonResponseHeaders(headers)
# check we can use the last modified time from the listing...
status, headers, body = \
self.conn.make_request('GET', self.bucket)
elem = fromstring(body, 'ListBucketResult')
last_modified = elem.find('./Contents/LastModified').text
listing_datetime = S3Timestamp.from_s3xmlformat(last_modified)
headers = \
{'If-Unmodified-Since': formatdate(listing_datetime)}
status, headers, body = \
self.conn.make_request('GET', self.bucket, obj, headers=headers)
self.assertEqual(status, 200)
self.assertCommonResponseHeaders(headers)
headers = \
{'If-Modified-Since': formatdate(listing_datetime)}
status, headers, body = \
self.conn.make_request('GET', self.bucket, obj, headers=headers)
self.assertEqual(status, 304)
self.assertCommonResponseHeaders(headers)
def test_get_object_if_match(self):
obj = 'object'
self.conn.make_request('PUT', self.bucket, obj)

View File

@ -92,7 +92,7 @@ class TestS3ApiPresignedUrls(S3ApiBase):
self.assertIsNotNone(o.find('LastModified').text)
self.assertRegex(
o.find('LastModified').text,
r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$')
r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.000Z$')
self.assertIsNotNone(o.find('ETag').text)
self.assertEqual(o.find('Size').text, '0')
self.assertIsNotNone(o.find('StorageClass').text is not None)

View File

@ -1042,9 +1042,9 @@ def make_timestamp_iter(offset=0):
@contextmanager
def mock_timestamp_now(now=None):
def mock_timestamp_now(now=None, klass=Timestamp):
if now is None:
now = Timestamp.now()
now = klass.now()
with mocklib.patch('swift.common.utils.Timestamp.now',
classmethod(lambda c: now)):
yield now

View File

@ -201,7 +201,7 @@ class TestS3ApiBucket(S3ApiTestCase):
items = []
for o in objects:
items.append((o.find('./Key').text, o.find('./ETag').text))
self.assertEqual('2011-01-05T02:19:14.275Z',
self.assertEqual('2011-01-05T02:19:15.000Z',
o.find('./LastModified').text)
expected = [
(i[0].encode('utf-8') if six.PY2 else i[0],
@ -211,6 +211,37 @@ class TestS3ApiBucket(S3ApiTestCase):
]
self.assertEqual(items, expected)
def test_bucket_GET_last_modified_rounding(self):
objects_list = [
{'name': 'a', 'last_modified': '2011-01-05T02:19:59.275290',
'content_type': 'application/octet-stream',
'hash': 'ahash', 'bytes': '12345'},
{'name': 'b', 'last_modified': '2011-01-05T02:19:59.000000',
'content_type': 'application/octet-stream',
'hash': 'ahash', 'bytes': '12345'},
]
self.swift.register(
'GET', '/v1/AUTH_test/junk',
swob.HTTPOk, {'Content-Type': 'application/json'},
json.dumps(objects_list))
req = Request.blank('/junk',
environ={'REQUEST_METHOD': 'GET'},
headers={'Authorization': 'AWS test:tester:hmac',
'Date': self.get_date_header()})
status, headers, body = self.call_s3api(req)
self.assertEqual(status.split()[0], '200')
elem = fromstring(body, 'ListBucketResult')
name = elem.find('./Name').text
self.assertEqual(name, 'junk')
objects = elem.iterchildren('Contents')
actual = [(obj.find('./Key').text, obj.find('./LastModified').text)
for obj in objects]
self.assertEqual(
[('a', '2011-01-05T02:20:00.000Z'),
('b', '2011-01-05T02:19:59.000Z')],
actual)
def test_bucket_GET_url_encoded(self):
bucket_name = 'junk'
req = Request.blank('/%s?encoding-type=url' % bucket_name,
@ -229,7 +260,7 @@ class TestS3ApiBucket(S3ApiTestCase):
items = []
for o in objects:
items.append((o.find('./Key').text, o.find('./ETag').text))
self.assertEqual('2011-01-05T02:19:14.275Z',
self.assertEqual('2011-01-05T02:19:15.000Z',
o.find('./LastModified').text)
self.assertEqual(items, [
@ -673,9 +704,9 @@ class TestS3ApiBucket(S3ApiTestCase):
self.assertEqual([v.find('./VersionId').text for v in versions],
['null' for v in objects])
# Last modified in self.objects is 2011-01-05T02:19:14.275290 but
# the returned value is 2011-01-05T02:19:14.275Z
# the returned value is rounded up to 2011-01-05T02:19:15Z
self.assertEqual([v.find('./LastModified').text for v in versions],
[v[1][:-3] + 'Z' for v in objects])
['2011-01-05T02:19:15.000Z'] * len(objects))
self.assertEqual([v.find('./ETag').text for v in versions],
[PFS_ETAG if v[0] == 'pfs-obj' else
'"0-N"' if v[0] == 'slo' else '"0"'

View File

@ -51,29 +51,41 @@ XML = '<CompleteMultipartUpload>' \
'</CompleteMultipartUpload>'
OBJECTS_TEMPLATE = \
(('object/X/1', '2014-05-07T19:47:51.592270', '0123456789abcdef', 100),
('object/X/2', '2014-05-07T19:47:52.592270', 'fedcba9876543210', 200))
(('object/X/1', '2014-05-07T19:47:51.592270', '0123456789abcdef', 100,
'2014-05-07T19:47:52.000Z'),
('object/X/2', '2014-05-07T19:47:52.592270', 'fedcba9876543210', 200,
'2014-05-07T19:47:53.000Z'))
MULTIPARTS_TEMPLATE = \
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1),
('object/X/1', '2014-05-07T19:47:51.592270', '0123456789abcdef', 11),
('object/X/2', '2014-05-07T19:47:52.592270', 'fedcba9876543210', 21),
('object/Y', '2014-05-07T19:47:53.592270', 'HASH', 2),
('object/Y/1', '2014-05-07T19:47:54.592270', '0123456789abcdef', 12),
('object/Y/2', '2014-05-07T19:47:55.592270', 'fedcba9876543210', 22),
('object/Z', '2014-05-07T19:47:56.592270', 'HASH', 3),
('object/Z/1', '2014-05-07T19:47:57.592270', '0123456789abcdef', 13),
('object/Z/2', '2014-05-07T19:47:58.592270', 'fedcba9876543210', 23),
('subdir/object/Z', '2014-05-07T19:47:58.592270', 'HASH', 4),
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1,
'2014-05-07T19:47:51.000Z'),
('object/X/1', '2014-05-07T19:47:51.592270', '0123456789abcdef', 11,
'2014-05-07T19:47:52.000Z'),
('object/X/2', '2014-05-07T19:47:52.592270', 'fedcba9876543210', 21,
'2014-05-07T19:47:53.000Z'),
('object/Y', '2014-05-07T19:47:53.592270', 'HASH', 2,
'2014-05-07T19:47:54.000Z'),
('object/Y/1', '2014-05-07T19:47:54.592270', '0123456789abcdef', 12,
'2014-05-07T19:47:55.000Z'),
('object/Y/2', '2014-05-07T19:47:55.592270', 'fedcba9876543210', 22,
'2014-05-07T19:47:56.000Z'),
('object/Z', '2014-05-07T19:47:56.592270', 'HASH', 3,
'2014-05-07T19:47:57.000Z'),
('object/Z/1', '2014-05-07T19:47:57.592270', '0123456789abcdef', 13,
'2014-05-07T19:47:58.000Z'),
('object/Z/2', '2014-05-07T19:47:58.592270', 'fedcba9876543210', 23,
'2014-05-07T19:47:59.000Z'),
('subdir/object/Z', '2014-05-07T19:47:58.592270', 'HASH', 4,
'2014-05-07T19:47:59.000Z'),
('subdir/object/Z/1', '2014-05-07T19:47:58.592270', '0123456789abcdef',
41),
41, '2014-05-07T19:47:59.000Z'),
('subdir/object/Z/2', '2014-05-07T19:47:58.592270', 'fedcba9876543210',
41),
41, '2014-05-07T19:47:59.000Z'),
# NB: wsgi strings
('subdir/object/completed\xe2\x98\x83/W/1', '2014-05-07T19:47:58.592270',
'0123456789abcdef', 41),
'0123456789abcdef', 41, '2014-05-07T19:47:59.000Z'),
('subdir/object/completed\xe2\x98\x83/W/2', '2014-05-07T19:47:58.592270',
'fedcba9876543210', 41))
'fedcba9876543210', 41, '2014-05-07T19:47:59'))
S3_ETAG = '"%s-2"' % md5(binascii.a2b_hex(
'0123456789abcdef0123456789abcdef'
@ -285,7 +297,7 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
self.assertEqual(elem.find('MaxUploads').text, '1000')
self.assertEqual(elem.find('IsTruncated').text, 'false')
self.assertEqual(len(elem.findall('Upload')), len(uploads))
expected_uploads = [(upload[0], '2014-05-07T19:47:50.592Z')
expected_uploads = [(upload[0], '2014-05-07T19:47:51.000Z')
for upload in uploads]
for u in elem.findall('Upload'):
name = u.find('Key').text + '/' + u.find('UploadId').text
@ -310,7 +322,7 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
self.assertEqual(elem.find('MaxUploads').text, '1000')
self.assertEqual(elem.find('IsTruncated').text, 'false')
self.assertEqual(len(elem.findall('Upload')), 4)
objects = [(o[0], o[1][:-3] + 'Z') for o in MULTIPARTS_TEMPLATE]
objects = [(o[0], o[4]) for o in MULTIPARTS_TEMPLATE]
for u in elem.findall('Upload'):
name = u.find('Key').text + '/' + u.find('UploadId').text
initiated = u.find('Initiated').text
@ -417,9 +429,12 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
def test_bucket_multipart_uploads_GET_with_id_and_key_marker(self):
query = 'upload-id-marker=Y&key-marker=object'
multiparts = \
(('object/Y', '2014-05-07T19:47:53.592270', 'HASH', 2),
('object/Y/1', '2014-05-07T19:47:54.592270', 'HASH', 12),
('object/Y/2', '2014-05-07T19:47:55.592270', 'HASH', 22))
(('object/Y', '2014-05-07T19:47:53.592270', 'HASH', 2,
'2014-05-07T19:47:54.000Z'),
('object/Y/1', '2014-05-07T19:47:54.592270', 'HASH', 12,
'2014-05-07T19:47:55.000Z'),
('object/Y/2', '2014-05-07T19:47:55.592270', 'HASH', 22,
'2014-05-07T19:47:56.000Z'))
status, headers, body = \
self._test_bucket_multipart_uploads_GET(query, multiparts)
@ -427,7 +442,7 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
self.assertEqual(elem.find('KeyMarker').text, 'object')
self.assertEqual(elem.find('UploadIdMarker').text, 'Y')
self.assertEqual(len(elem.findall('Upload')), 1)
objects = [(o[0], o[1][:-3] + 'Z') for o in multiparts]
objects = [(o[0], o[4]) for o in multiparts]
for u in elem.findall('Upload'):
name = u.find('Key').text + '/' + u.find('UploadId').text
initiated = u.find('Initiated').text
@ -447,12 +462,18 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
def test_bucket_multipart_uploads_GET_with_key_marker(self):
query = 'key-marker=object'
multiparts = \
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21),
('object/Y', '2014-05-07T19:47:53.592270', 'HASH', 2),
('object/Y/1', '2014-05-07T19:47:54.592270', 'HASH', 12),
('object/Y/2', '2014-05-07T19:47:55.592270', 'HASH', 22))
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1,
'2014-05-07T19:47:51.000Z'),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11,
'2014-05-07T19:47:52.000Z'),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21,
'2014-05-07T19:47:53.000Z'),
('object/Y', '2014-05-07T19:47:53.592270', 'HASH', 2,
'2014-05-07T19:47:54.000Z'),
('object/Y/1', '2014-05-07T19:47:54.592270', 'HASH', 12,
'2014-05-07T19:47:55.000Z'),
('object/Y/2', '2014-05-07T19:47:55.592270', 'HASH', 22,
'2014-05-07T19:47:56.000Z'))
status, headers, body = \
self._test_bucket_multipart_uploads_GET(query, multiparts)
elem = fromstring(body, 'ListMultipartUploadsResult')
@ -460,11 +481,11 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
self.assertEqual(elem.find('NextKeyMarker').text, 'object')
self.assertEqual(elem.find('NextUploadIdMarker').text, 'Y')
self.assertEqual(len(elem.findall('Upload')), 2)
objects = [(o[0], o[1][:-3] + 'Z') for o in multiparts]
objects = [(o[0], o[4]) for o in multiparts]
for u in elem.findall('Upload'):
name = u.find('Key').text + '/' + u.find('UploadId').text
initiated = u.find('Initiated').text
self.assertTrue((name, initiated) in objects)
self.assertIn((name, initiated), objects)
self.assertEqual(status.split()[0], '200')
_, path, _ = self.swift.calls_with_headers[-1]
@ -480,14 +501,17 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
def test_bucket_multipart_uploads_GET_with_prefix(self):
query = 'prefix=X'
multiparts = \
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21))
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1,
'2014-05-07T19:47:51.000Z'),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11,
'2014-05-07T19:47:52.000Z'),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21,
'2014-05-07T19:47:53.000Z'))
status, headers, body = \
self._test_bucket_multipart_uploads_GET(query, multiparts)
elem = fromstring(body, 'ListMultipartUploadsResult')
self.assertEqual(len(elem.findall('Upload')), 1)
objects = [(o[0], o[1][:-3] + 'Z') for o in multiparts]
objects = [(o[0], o[4]) for o in multiparts]
for u in elem.findall('Upload'):
name = u.find('Key').text + '/' + u.find('UploadId').text
initiated = u.find('Initiated').text
@ -507,38 +531,56 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
def test_bucket_multipart_uploads_GET_with_delimiter(self):
query = 'delimiter=/'
multiparts = \
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21),
('object/Y', '2014-05-07T19:47:50.592270', 'HASH', 2),
('object/Y/1', '2014-05-07T19:47:51.592270', 'HASH', 21),
('object/Y/2', '2014-05-07T19:47:52.592270', 'HASH', 22),
('object/Z', '2014-05-07T19:47:50.592270', 'HASH', 3),
('object/Z/1', '2014-05-07T19:47:51.592270', 'HASH', 31),
('object/Z/2', '2014-05-07T19:47:52.592270', 'HASH', 32),
('subdir/object/X', '2014-05-07T19:47:50.592270', 'HASH', 4),
('subdir/object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 41),
('subdir/object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 42),
('subdir/object/Y', '2014-05-07T19:47:50.592270', 'HASH', 5),
('subdir/object/Y/1', '2014-05-07T19:47:51.592270', 'HASH', 51),
('subdir/object/Y/2', '2014-05-07T19:47:52.592270', 'HASH', 52),
('subdir2/object/Z', '2014-05-07T19:47:50.592270', 'HASH', 6),
('subdir2/object/Z/1', '2014-05-07T19:47:51.592270', 'HASH', 61),
('subdir2/object/Z/2', '2014-05-07T19:47:52.592270', 'HASH', 62))
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1,
'2014-05-07T19:47:51.000Z'),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11,
'2014-05-07T19:47:52.000Z'),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21,
'2014-05-07T19:47:53.000Z'),
('object/Y', '2014-05-07T19:47:50.592270', 'HASH', 2,
'2014-05-07T19:47:51.000Z'),
('object/Y/1', '2014-05-07T19:47:51.592270', 'HASH', 21,
'2014-05-07T19:47:52.000Z'),
('object/Y/2', '2014-05-07T19:47:52.592270', 'HASH', 22,
'2014-05-07T19:47:53.000Z'),
('object/Z', '2014-05-07T19:47:50.592270', 'HASH', 3,
'2014-05-07T19:47:51.000Z'),
('object/Z/1', '2014-05-07T19:47:51.592270', 'HASH', 31,
'2014-05-07T19:47:52.000Z'),
('object/Z/2', '2014-05-07T19:47:52.592270', 'HASH', 32,
'2014-05-07T19:47:53.000Z'),
('subdir/object/X', '2014-05-07T19:47:50.592270', 'HASH', 4,
'2014-05-07T19:47:51.000Z'),
('subdir/object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 41,
'2014-05-07T19:47:52.000Z'),
('subdir/object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 42,
'2014-05-07T19:47:53.000Z'),
('subdir/object/Y', '2014-05-07T19:47:50.592270', 'HASH', 5,
'2014-05-07T19:47:51.000Z'),
('subdir/object/Y/1', '2014-05-07T19:47:51.592270', 'HASH', 51,
'2014-05-07T19:47:52.000Z'),
('subdir/object/Y/2', '2014-05-07T19:47:52.592270', 'HASH', 52,
'2014-05-07T19:47:53.000Z'),
('subdir2/object/Z', '2014-05-07T19:47:50.592270', 'HASH', 6,
'2014-05-07T19:47:51.000Z'),
('subdir2/object/Z/1', '2014-05-07T19:47:51.592270', 'HASH', 61,
'2014-05-07T19:47:52.000Z'),
('subdir2/object/Z/2', '2014-05-07T19:47:52.592270', 'HASH', 62,
'2014-05-07T19:47:53.000Z'))
status, headers, body = \
self._test_bucket_multipart_uploads_GET(query, multiparts)
elem = fromstring(body, 'ListMultipartUploadsResult')
self.assertEqual(len(elem.findall('Upload')), 3)
self.assertEqual(len(elem.findall('CommonPrefixes')), 2)
objects = [(o[0], o[1][:-3] + 'Z') for o in multiparts
objects = [(o[0], o[4]) for o in multiparts
if o[0].startswith('o')]
prefixes = set([o[0].split('/')[0] + '/' for o in multiparts
if o[0].startswith('s')])
for u in elem.findall('Upload'):
name = u.find('Key').text + '/' + u.find('UploadId').text
initiated = u.find('Initiated').text
self.assertTrue((name, initiated) in objects)
self.assertIn((name, initiated), objects)
for p in elem.findall('CommonPrefixes'):
prefix = p.find('Prefix').text
self.assertTrue(prefix in prefixes)
@ -557,31 +599,43 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
def test_bucket_multipart_uploads_GET_with_multi_chars_delimiter(self):
query = 'delimiter=subdir'
multiparts = \
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21),
(('object/X', '2014-05-07T19:47:50.592270', 'HASH', 1,
'2014-05-07T19:47:51.000Z'),
('object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 11,
'2014-05-07T19:47:52.000Z'),
('object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 21,
'2014-05-07T19:47:53.000Z'),
('dir/subdir/object/X', '2014-05-07T19:47:50.592270',
'HASH', 3),
'HASH', 3, '2014-05-07T19:47:51.000Z'),
('dir/subdir/object/X/1', '2014-05-07T19:47:51.592270',
'HASH', 31),
'HASH', 31, '2014-05-07T19:47:52.000Z'),
('dir/subdir/object/X/2', '2014-05-07T19:47:52.592270',
'HASH', 32),
('subdir/object/X', '2014-05-07T19:47:50.592270', 'HASH', 4),
('subdir/object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 41),
('subdir/object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 42),
('subdir/object/Y', '2014-05-07T19:47:50.592270', 'HASH', 5),
('subdir/object/Y/1', '2014-05-07T19:47:51.592270', 'HASH', 51),
('subdir/object/Y/2', '2014-05-07T19:47:52.592270', 'HASH', 52),
('subdir2/object/Z', '2014-05-07T19:47:50.592270', 'HASH', 6),
('subdir2/object/Z/1', '2014-05-07T19:47:51.592270', 'HASH', 61),
('subdir2/object/Z/2', '2014-05-07T19:47:52.592270', 'HASH', 62))
'HASH', 32, '2014-05-07T19:47:53.000Z'),
('subdir/object/X', '2014-05-07T19:47:50.592270', 'HASH', 4,
'2014-05-07T19:47:51.000Z'),
('subdir/object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 41,
'2014-05-07T19:47:52.000Z'),
('subdir/object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 42,
'2014-05-07T19:47:53.000Z'),
('subdir/object/Y', '2014-05-07T19:47:50.592270', 'HASH', 5,
'2014-05-07T19:47:51.000Z'),
('subdir/object/Y/1', '2014-05-07T19:47:51.592270', 'HASH', 51,
'2014-05-07T19:47:52.000Z'),
('subdir/object/Y/2', '2014-05-07T19:47:52.592270', 'HASH', 52,
'2014-05-07T19:47:53.000Z'),
('subdir2/object/Z', '2014-05-07T19:47:50.592270', 'HASH', 6,
'2014-05-07T19:47:51.000Z'),
('subdir2/object/Z/1', '2014-05-07T19:47:51.592270', 'HASH', 61,
'2014-05-07T19:47:52.000Z'),
('subdir2/object/Z/2', '2014-05-07T19:47:52.592270', 'HASH', 62,
'2014-05-07T19:47:53.000Z'))
status, headers, body = \
self._test_bucket_multipart_uploads_GET(query, multiparts)
elem = fromstring(body, 'ListMultipartUploadsResult')
self.assertEqual(len(elem.findall('Upload')), 1)
self.assertEqual(len(elem.findall('CommonPrefixes')), 2)
objects = [(o[0], o[1][:-3] + 'Z') for o in multiparts
objects = [(o[0], o[4]) for o in multiparts
if o[0].startswith('object')]
prefixes = ('dir/subdir', 'subdir')
for u in elem.findall('Upload'):
@ -607,27 +661,30 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
query = 'prefix=dir/&delimiter=/'
multiparts = \
(('dir/subdir/object/X', '2014-05-07T19:47:50.592270',
'HASH', 4),
'HASH', 4, '2014-05-07T19:47:51.000Z'),
('dir/subdir/object/X/1', '2014-05-07T19:47:51.592270',
'HASH', 41),
'HASH', 41, '2014-05-07T19:47:52.000Z'),
('dir/subdir/object/X/2', '2014-05-07T19:47:52.592270',
'HASH', 42),
('dir/object/X', '2014-05-07T19:47:50.592270', 'HASH', 5),
('dir/object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 51),
('dir/object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 52))
'HASH', 42, '2014-05-07T19:47:53.000Z'),
('dir/object/X', '2014-05-07T19:47:50.592270', 'HASH', 5,
'2014-05-07T19:47:51.000Z'),
('dir/object/X/1', '2014-05-07T19:47:51.592270', 'HASH', 51,
'2014-05-07T19:47:52.000Z'),
('dir/object/X/2', '2014-05-07T19:47:52.592270', 'HASH', 52,
'2014-05-07T19:47:53.000Z'))
status, headers, body = \
self._test_bucket_multipart_uploads_GET(query, multiparts)
elem = fromstring(body, 'ListMultipartUploadsResult')
self.assertEqual(len(elem.findall('Upload')), 1)
self.assertEqual(len(elem.findall('CommonPrefixes')), 1)
objects = [(o[0], o[1][:-3] + 'Z') for o in multiparts
objects = [(o[0], o[4]) for o in multiparts
if o[0].startswith('dir/o')]
prefixes = ['dir/subdir/']
for u in elem.findall('Upload'):
name = u.find('Key').text + '/' + u.find('UploadId').text
initiated = u.find('Initiated').text
self.assertTrue((name, initiated) in objects)
self.assertIn((name, initiated), objects)
for p in elem.findall('CommonPrefixes'):
prefix = p.find('Prefix').text
self.assertTrue(prefix in prefixes)
@ -1838,6 +1895,9 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
'hash': hex(i),
'bytes': 100 * i}
for i in range(1, 2000)]
ceil_last_modified = ['2014-05-07T19:%02d:%02d.000Z'
% (47 if (i + 1) % 60 else 48, (i + 1) % 60)
for i in range(1, 2000)]
swift_sorted = sorted(swift_parts, key=lambda part: part['name'])
self.swift.register('GET',
"%s?delimiter=/&format=json&marker=&"
@ -1872,7 +1932,7 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
s3_parts.append(partnum)
self.assertEqual(
p.find('LastModified').text,
swift_parts[partnum - 1]['last_modified'][:-3] + 'Z')
ceil_last_modified[partnum - 1])
self.assertEqual(p.find('ETag').text.strip(),
'"%s"' % swift_parts[partnum - 1]['hash'])
self.assertEqual(p.find('Size').text,
@ -1970,7 +2030,7 @@ class TestS3ApiMultiUpload(S3ApiTestCase):
for p in elem.findall('Part'):
partnum = int(p.find('PartNumber').text)
self.assertEqual(p.find('LastModified').text,
OBJECTS_TEMPLATE[partnum - 1][1][:-3] + 'Z')
OBJECTS_TEMPLATE[partnum - 1][4])
self.assertEqual(p.find('ETag').text,
'"%s"' % OBJECTS_TEMPLATE[partnum - 1][2])
self.assertEqual(p.find('Size').text,

View File

@ -28,6 +28,7 @@ import json
from swift.common import swob
from swift.common.swob import Request
from swift.common.middleware.proxy_logging import ProxyLoggingMiddleware
from test.unit import mock_timestamp_now
from test.unit.common.middleware.s3api import S3ApiTestCase
from test.unit.common.middleware.s3api.test_s3_acl import s3acl
@ -872,9 +873,7 @@ class TestS3ApiObj(S3ApiTestCase):
@s3acl
def test_object_PUT_copy_metadata_replace(self):
date_header = self.get_date_header()
timestamp = mktime(date_header)
allowed_last_modified = [S3Timestamp(timestamp).s3xmlformat]
with mock_timestamp_now(klass=S3Timestamp) as now:
status, headers, body = \
self._test_object_PUT_copy(
swob.HTTPOk,
@ -890,15 +889,13 @@ class TestS3ApiObj(S3ApiTestCase):
'content-type': 'so',
'expires': 'yeah',
'x-robots-tag': 'bye'})
date_header = self.get_date_header()
timestamp = mktime(date_header)
allowed_last_modified.append(S3Timestamp(timestamp).s3xmlformat)
self.assertEqual(status.split()[0], '200')
self.assertEqual(headers['Content-Type'], 'application/xml')
self.assertIsNone(headers.get('etag'))
elem = fromstring(body, 'CopyObjectResult')
self.assertIn(elem.find('LastModified').text, allowed_last_modified)
self.assertEqual(S3Timestamp(now.ceil()).s3xmlformat,
elem.find('LastModified').text)
self.assertEqual(elem.find('ETag').text, '"%s"' % self.etag)
_, _, headers = self.swift.calls_with_headers[-1]
@ -926,9 +923,7 @@ class TestS3ApiObj(S3ApiTestCase):
@s3acl
def test_object_PUT_copy_metadata_copy(self):
date_header = self.get_date_header()
timestamp = mktime(date_header)
allowed_last_modified = [S3Timestamp(timestamp).s3xmlformat]
with mock_timestamp_now(klass=S3Timestamp) as now:
status, headers, body = \
self._test_object_PUT_copy(
swob.HTTPOk,
@ -944,16 +939,14 @@ class TestS3ApiObj(S3ApiTestCase):
'content-type': 'so',
'expires': 'yeah',
'x-robots-tag': 'bye'})
date_header = self.get_date_header()
timestamp = mktime(date_header)
allowed_last_modified.append(S3Timestamp(timestamp).s3xmlformat)
self.assertEqual(status.split()[0], '200')
self.assertEqual(headers['Content-Type'], 'application/xml')
self.assertIsNone(headers.get('etag'))
elem = fromstring(body, 'CopyObjectResult')
self.assertIn(elem.find('LastModified').text, allowed_last_modified)
self.assertEqual(S3Timestamp(now.ceil()).s3xmlformat,
elem.find('LastModified').text)
self.assertEqual(elem.find('ETag').text, '"%s"' % self.etag)
_, _, headers = self.swift.calls_with_headers[-1]

View File

@ -81,21 +81,6 @@ class TestS3ApiUtils(unittest.TestCase):
self.assertFalse(utils.validate_bucket_name('bucket.', False))
self.assertFalse(utils.validate_bucket_name('a' * 256, False))
def test_s3timestamp(self):
expected = '1970-01-01T00:00:01.000Z'
# integer
ts = utils.S3Timestamp(1)
self.assertEqual(expected, ts.s3xmlformat)
# milliseconds unit should be floored
ts = utils.S3Timestamp(1.1)
self.assertEqual(expected, ts.s3xmlformat)
# float (microseconds) should be floored too
ts = utils.S3Timestamp(1.000001)
self.assertEqual(expected, ts.s3xmlformat)
# Bigger float (milliseconds) should be floored too
ts = utils.S3Timestamp(1.9)
self.assertEqual(expected, ts.s3xmlformat)
def test_mktime(self):
date_headers = [
'Thu, 01 Jan 1970 00:00:00 -0000',
@ -130,6 +115,48 @@ class TestS3ApiUtils(unittest.TestCase):
time.tzset()
class TestS3Timestamp(unittest.TestCase):
def test_s3xmlformat(self):
expected = '1970-01-01T00:00:01.000Z'
# integer
ts = utils.S3Timestamp(1)
self.assertEqual(expected, ts.s3xmlformat)
# milliseconds unit should be rounded up
expected = '1970-01-01T00:00:02.000Z'
ts = utils.S3Timestamp(1.1)
self.assertEqual(expected, ts.s3xmlformat)
# float (microseconds) should be floored too
ts = utils.S3Timestamp(1.000001)
self.assertEqual(expected, ts.s3xmlformat)
# Bigger float (milliseconds) should be floored too
ts = utils.S3Timestamp(1.9)
self.assertEqual(expected, ts.s3xmlformat)
def test_from_s3xmlformat(self):
ts = utils.S3Timestamp.from_s3xmlformat('2014-06-10T22:47:32.000Z')
self.assertIsInstance(ts, utils.S3Timestamp)
self.assertEqual(1402440452, float(ts))
self.assertEqual('2014-06-10T22:47:32.000000', ts.isoformat)
ts = utils.S3Timestamp.from_s3xmlformat('1970-01-01T00:00:00.000Z')
self.assertIsInstance(ts, utils.S3Timestamp)
self.assertEqual(0.0, float(ts))
self.assertEqual('1970-01-01T00:00:00.000000', ts.isoformat)
ts = utils.S3Timestamp(1402440452.0)
self.assertIsInstance(ts, utils.S3Timestamp)
ts1 = utils.S3Timestamp.from_s3xmlformat(ts.s3xmlformat)
self.assertIsInstance(ts1, utils.S3Timestamp)
self.assertEqual(ts, ts1)
def test_from_isoformat(self):
ts = utils.S3Timestamp.from_isoformat('2014-06-10T22:47:32.054580')
self.assertIsInstance(ts, utils.S3Timestamp)
self.assertEqual(1402440452.05458, float(ts))
self.assertEqual('2014-06-10T22:47:32.054580', ts.isoformat)
self.assertEqual('2014-06-10T22:47:33.000Z', ts.s3xmlformat)
class TestConfig(unittest.TestCase):
def _assert_defaults(self, conf):

View File

@ -305,6 +305,21 @@ class TestTimestamp(unittest.TestCase):
for value in test_values:
self.assertEqual(utils.Timestamp(value).isoformat, expected)
def test_from_isoformat(self):
ts = utils.Timestamp.from_isoformat('2014-06-10T22:47:32.054580')
self.assertIsInstance(ts, utils.Timestamp)
self.assertEqual(1402440452.05458, float(ts))
self.assertEqual('2014-06-10T22:47:32.054580', ts.isoformat)
ts = utils.Timestamp.from_isoformat('1970-01-01T00:00:00.000000')
self.assertIsInstance(ts, utils.Timestamp)
self.assertEqual(0.0, float(ts))
self.assertEqual('1970-01-01T00:00:00.000000', ts.isoformat)
ts = utils.Timestamp(1402440452.05458)
self.assertIsInstance(ts, utils.Timestamp)
self.assertEqual(ts, utils.Timestamp.from_isoformat(ts.isoformat))
def test_ceil(self):
self.assertEqual(0.0, utils.Timestamp(0).ceil())
self.assertEqual(1.0, utils.Timestamp(0.00001).ceil())