118cf2ba8a
We've known this would eventually be necessary for a while [1], and way back in 2017 we started seeing SHA-1 collisions [2]. [1] https://www.schneier.com/blog/archives/2012/10/when_will_we_se.html [2] https://security.googleblog.com/2017/02/announcing-first-sha1-collision.html UpgradeImpact: ============== "sha1" has been removed from the default set of `allowed_digests` in the tempurl middleware config. If your cluster still has clients requiring the use of SHA-1, - explicitly configure `allowed_digests` to include "sha1" and - encourage your clients to move to more-secure algorithms. Depends-On: https://review.opendev.org/c/openstack/tempest/+/832771 Change-Id: I6e6fa76671c860191a2ce921cb6caddc859b1066 Related-Change: Ia9dd1a91cc3c9c946f5f029cdefc9e66bcf01046 Closes-Bug: #1733634
2775 lines
107 KiB
Python
2775 lines
107 KiB
Python
#!/usr/bin/python -u
|
|
# Copyright (c) 2010-2012 OpenStack Foundation
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import hashlib
|
|
import hmac
|
|
import json
|
|
import time
|
|
import six
|
|
|
|
from copy import deepcopy
|
|
from six.moves.urllib.parse import quote, unquote
|
|
|
|
import test.functional as tf
|
|
|
|
from swift.common.swob import normalize_etag
|
|
from swift.common.utils import MD5_OF_EMPTY_STRING, config_true_value, md5
|
|
from swift.common.middleware.versioned_writes.object_versioning import \
|
|
DELETE_MARKER_CONTENT_TYPE
|
|
|
|
from test.functional.tests import Base, Base2, BaseEnv, Utils
|
|
from test.functional import cluster_info, SkipTest
|
|
from test.functional.swift_test_client import Connection, \
|
|
ResponseError
|
|
from test.functional.test_tempurl import TestContainerTempurlEnv, \
|
|
TestTempurlEnv
|
|
|
|
|
|
def setUpModule():
|
|
tf.setup_package()
|
|
|
|
|
|
def tearDownModule():
|
|
tf.teardown_package()
|
|
|
|
|
|
class TestObjectVersioningEnv(BaseEnv):
|
|
account2 = None
|
|
versions_header_key = 'X-Versions-Enabled'
|
|
|
|
@classmethod
|
|
def setUp(cls):
|
|
super(TestObjectVersioningEnv, cls).setUp()
|
|
|
|
if not tf.skip2:
|
|
# Second connection for ACL tests
|
|
config2 = deepcopy(tf.config)
|
|
config2['account'] = tf.config['account2']
|
|
config2['username'] = tf.config['username2']
|
|
config2['password'] = tf.config['password2']
|
|
cls.conn2 = Connection(config2)
|
|
cls.conn2.authenticate()
|
|
|
|
if six.PY2:
|
|
# avoid getting a prefix that stops halfway through an encoded
|
|
# character
|
|
prefix = Utils.create_name().decode("utf-8")[:10].encode("utf-8")
|
|
else:
|
|
prefix = Utils.create_name()[:10]
|
|
|
|
cls.container = cls.account.container(prefix + "-objs")
|
|
container_headers = {cls.versions_header_key: 'True'}
|
|
if not cls.container.create(hdrs=container_headers):
|
|
raise ResponseError(cls.conn.response)
|
|
|
|
cls.unversioned_container = cls.account.container(
|
|
prefix + "-unversioned")
|
|
if not cls.unversioned_container.create():
|
|
raise ResponseError(cls.conn.response)
|
|
|
|
if not tf.skip2:
|
|
# setup another account to test ACLs
|
|
config2 = deepcopy(tf.config)
|
|
config2['account'] = tf.config['account2']
|
|
config2['username'] = tf.config['username2']
|
|
config2['password'] = tf.config['password2']
|
|
cls.conn2 = Connection(config2)
|
|
cls.storage_url2, cls.storage_token2 = cls.conn2.authenticate()
|
|
cls.account2 = cls.conn2.get_account()
|
|
cls.account2.delete_containers()
|
|
|
|
if not tf.skip3:
|
|
# setup another account with no access to anything to test ACLs
|
|
config3 = deepcopy(tf.config)
|
|
config3['account'] = tf.config['account']
|
|
config3['username'] = tf.config['username3']
|
|
config3['password'] = tf.config['password3']
|
|
cls.conn3 = Connection(config3)
|
|
cls.storage_url3, cls.storage_token3 = cls.conn3.authenticate()
|
|
cls.account3 = cls.conn3.get_account()
|
|
|
|
# the allowed headers are configurable in object server, so we cannot
|
|
# assert that content-encoding or content-disposition get *copied* to
|
|
# the object version unless they were set on the original PUT, so
|
|
# populate expected_headers by making a HEAD on the original object
|
|
precheck_container = cls.account.container('header-precheck-cont')
|
|
if not precheck_container.create():
|
|
raise ResponseError(cls.conn.response)
|
|
test_obj = precheck_container.file('test_allowed_headers')
|
|
put_headers = {'Content-Type': 'text/jibberish01',
|
|
'Content-Encoding': 'gzip',
|
|
'Content-Disposition': 'attachment; filename=myfile'}
|
|
test_obj.write(b"aaaaa", hdrs=put_headers)
|
|
test_obj.initialize()
|
|
resp_headers = {
|
|
h.lower(): v for h, v in test_obj.conn.response.getheaders()}
|
|
cls.expected_headers = {}
|
|
for k, v in put_headers.items():
|
|
if k.lower() in resp_headers:
|
|
cls.expected_headers[k] = v
|
|
precheck_container.delete_recursive()
|
|
|
|
@classmethod
|
|
def tearDown(cls):
|
|
if cls.account:
|
|
cls.account.delete_containers()
|
|
if cls.account2:
|
|
cls.account2.delete_containers()
|
|
|
|
|
|
class TestObjectVersioningBase(Base):
|
|
env = TestObjectVersioningEnv
|
|
|
|
def setUp(self):
|
|
super(TestObjectVersioningBase, self).setUp()
|
|
if 'object_versioning' not in tf.cluster_info:
|
|
raise SkipTest("Object Versioning not enabled")
|
|
|
|
self._account_name = None
|
|
|
|
# make sure versioning is enabled,
|
|
# since it gets disabled in tearDown
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'True'})
|
|
|
|
def _tear_down_files(self, container):
|
|
try:
|
|
# only delete files and not containers
|
|
# as they were configured in self.env
|
|
# get rid of any versions so they aren't restored
|
|
|
|
container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
# get rid of originals
|
|
container.delete_files()
|
|
|
|
# delete older versions
|
|
listing_parms = {'versions': None, 'format': 'json'}
|
|
for obj_info in container.files(parms=listing_parms):
|
|
prev_version = container.file(obj_info['name'])
|
|
prev_version.delete(
|
|
parms={'version-id': obj_info['version_id']})
|
|
|
|
except ResponseError:
|
|
pass
|
|
|
|
def tearDown(self):
|
|
super(TestObjectVersioningBase, self).tearDown()
|
|
self._tear_down_files(self.env.container)
|
|
|
|
def assertTotalVersions(self, container, count):
|
|
listing_parms = {'versions': None}
|
|
self.assertEqual(count, len(container.files(parms=listing_parms)))
|
|
|
|
def assertContentTypes(self, container, expected_content_types):
|
|
listing_parms = {'versions': None,
|
|
'format': 'json',
|
|
'reverse': 'true'}
|
|
self.assertEqual(expected_content_types, [
|
|
o['content_type']
|
|
for o in container.files(parms=listing_parms)])
|
|
|
|
|
|
class TestObjectVersioning(TestObjectVersioningBase):
|
|
|
|
@property
|
|
def account_name(self):
|
|
if not self._account_name:
|
|
self._account_name = self.env.conn.storage_path.rsplit('/', 1)[-1]
|
|
return self._account_name
|
|
|
|
def test_disable_version(self):
|
|
# sanity
|
|
self.assertTrue(
|
|
config_true_value(self.env.container.info()['versions_enabled']))
|
|
|
|
# disable it
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
self.assertFalse(
|
|
config_true_value(self.env.container.info()['versions_enabled']))
|
|
|
|
# enabled it back
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'True'})
|
|
self.assertTrue(
|
|
config_true_value(self.env.container.info()['versions_enabled']))
|
|
|
|
def test_account_list_containers(self):
|
|
cont_listing = self.env.account.containers()
|
|
self.assertEqual(cont_listing, [self.env.container.name,
|
|
self.env.unversioned_container.name])
|
|
self.env.account.delete_containers()
|
|
prefix = Utils.create_name()
|
|
|
|
def get_name(i):
|
|
return prefix + '-%02d' % i
|
|
|
|
num_container = [15, 20]
|
|
for i in range(num_container[1]):
|
|
name = get_name(i)
|
|
container = self.env.account.container(name)
|
|
container.create()
|
|
|
|
limit = 5
|
|
cont_listing = self.env.account.containers(parms={'limit': limit})
|
|
self.assertEqual(cont_listing, [get_name(i) for i in range(limit)])
|
|
|
|
for i in range(num_container[0], num_container[1]):
|
|
name = get_name(i)
|
|
container = self.env.account.container(name)
|
|
container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'True'})
|
|
|
|
cont_listing = self.env.account.containers(parms={'limit': limit})
|
|
self.assertEqual(cont_listing, [get_name(i) for i in range(limit)])
|
|
|
|
# we're in charge of getting everything back to normal
|
|
self.env.account.delete_containers()
|
|
self.env.container.create()
|
|
self.env.unversioned_container.create()
|
|
|
|
def assert_previous_version(self, object_name, version_id, content,
|
|
content_type, expected_headers={},
|
|
not_expected_header_keys=[],
|
|
check_env_expected_headers=False):
|
|
'''
|
|
Find previous version of an object using the ?versions API
|
|
then, assert object data and metadata using ?version-id API
|
|
'''
|
|
prev_version = self.env.container.file(object_name)
|
|
prev_version.initialize(parms={'version-id': version_id})
|
|
self.assertEqual(content, prev_version.read(
|
|
parms={'version-id': version_id}))
|
|
self.assertEqual(content_type, prev_version.content_type)
|
|
# make sure the new obj metadata did not leak to the prev. version
|
|
resp_headers = {
|
|
h.lower(): v for h, v in prev_version.conn.response.getheaders()}
|
|
|
|
for k in not_expected_header_keys:
|
|
self.assertNotIn(k, resp_headers)
|
|
|
|
for k, v in expected_headers.items():
|
|
self.assertIn(k.lower(), resp_headers)
|
|
self.assertEqual(v, resp_headers[k.lower()])
|
|
|
|
# also check env expected_headers
|
|
if check_env_expected_headers:
|
|
for k, v in self.env.expected_headers.items():
|
|
self.assertIn(k.lower(), resp_headers)
|
|
self.assertEqual(v, resp_headers[k.lower()])
|
|
|
|
def test_expiry(self):
|
|
# sanity
|
|
container = self.env.container
|
|
self.assertTrue(
|
|
config_true_value(self.env.container.info()['versions_enabled']))
|
|
|
|
versioned_obj1 = container.file(Utils.create_name())
|
|
put_headers = {'Content-Type': 'text/blah-blah-blah',
|
|
'X-Delete-After': '1',
|
|
'X-Object-Meta-Color': 'blue'}
|
|
resp = versioned_obj1.write(b"aaaaa", hdrs=put_headers,
|
|
return_resp=True)
|
|
version_id1 = resp.getheader('x-object-version-id')
|
|
|
|
versioned_obj2 = container.file(Utils.create_name())
|
|
resp = versioned_obj2.write(b"aaaaa", hdrs={}, return_resp=True)
|
|
version_id2 = resp.getheader('x-object-version-id')
|
|
|
|
# swift_test_client's File API doesn't really allow for POSTing
|
|
# arbitrary headers, so...
|
|
def post(url, token, parsed, conn):
|
|
conn.request('POST', '%s/%s/%s' % (parsed.path, container,
|
|
versioned_obj2.name),
|
|
'', {'X-Auth-Token': token,
|
|
'Content-Length': '0',
|
|
'X-Object-Meta-Color': 'red',
|
|
'X-Delete-After': '1'})
|
|
return tf.check_response(conn)
|
|
resp = tf.retry(post)
|
|
resp.read()
|
|
self.assertEqual(resp.status, 202)
|
|
|
|
time.sleep(1)
|
|
|
|
# Links have expired
|
|
with self.assertRaises(ResponseError) as cm:
|
|
versioned_obj1.info()
|
|
self.assertEqual(404, cm.exception.status)
|
|
|
|
with self.assertRaises(ResponseError) as cm:
|
|
versioned_obj2.info()
|
|
self.assertEqual(404, cm.exception.status)
|
|
|
|
# But data are still there
|
|
versioned_obj1.initialize(parms={'version-id': version_id1})
|
|
self.assertEqual('text/blah-blah-blah', versioned_obj1.content_type)
|
|
self.assertEqual('blue', versioned_obj1.metadata['color'])
|
|
|
|
versioned_obj2.initialize(parms={'version-id': version_id2})
|
|
self.assertEqual('application/octet-stream',
|
|
versioned_obj2.content_type)
|
|
self.assertEqual('red', versioned_obj2.metadata['color'])
|
|
|
|
# Note that links may still show up in listings, depending on how
|
|
# aggressive the object-expirer is. When doing a version-aware
|
|
# listing, though, we'll only ever have the two entries.
|
|
self.assertTotalVersions(container, 2)
|
|
|
|
def test_get_if_match(self):
|
|
body = b'data'
|
|
oname = Utils.create_name()
|
|
obj = self.env.unversioned_container.file(oname)
|
|
resp = obj.write(body, return_resp=True)
|
|
etag = resp.getheader('etag')
|
|
self.assertEqual(
|
|
md5(body, usedforsecurity=False).hexdigest(),
|
|
normalize_etag(etag))
|
|
|
|
# un-versioned object is cool with with if-match
|
|
self.assertEqual(body, obj.read(hdrs={'if-match': etag}))
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.read(hdrs={'if-match': 'not-the-etag'})
|
|
self.assertEqual(412, cm.exception.status)
|
|
|
|
v_obj = self.env.container.file(oname)
|
|
resp = v_obj.write(body, return_resp=True)
|
|
self.assertEqual(resp.getheader('etag'), etag)
|
|
|
|
# versioned object is too with with if-match
|
|
self.assertEqual(body, v_obj.read(hdrs={
|
|
'if-match': normalize_etag(etag)}))
|
|
# works quoted, too
|
|
self.assertEqual(body, v_obj.read(hdrs={
|
|
'if-match': '"%s"' % normalize_etag(etag)}))
|
|
with self.assertRaises(ResponseError) as cm:
|
|
v_obj.read(hdrs={'if-match': 'not-the-etag'})
|
|
self.assertEqual(412, cm.exception.status)
|
|
|
|
def test_container_acls(self):
|
|
if tf.skip3:
|
|
raise SkipTest('Username3 not set')
|
|
|
|
obj = self.env.container.file(Utils.create_name())
|
|
resp = obj.write(b"data", return_resp=True)
|
|
version_id = resp.getheader('x-object-version-id')
|
|
self.assertIsNotNone(version_id)
|
|
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.read(hdrs={'X-Auth-Token': self.env.conn3.storage_token})
|
|
self.assertEqual(403, cm.exception.status)
|
|
|
|
# Container ACLs work more or less like they always have
|
|
self.env.container.update_metadata(
|
|
hdrs={'X-Container-Read': self.env.conn3.user_acl})
|
|
self.assertEqual(b"data", obj.read(hdrs={
|
|
'X-Auth-Token': self.env.conn3.storage_token}))
|
|
|
|
# But the version-specifc GET still requires a swift owner
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.read(hdrs={'X-Auth-Token': self.env.conn3.storage_token},
|
|
parms={'version-id': version_id})
|
|
self.assertEqual(403, cm.exception.status)
|
|
|
|
# If it's pointing to a symlink that points elsewhere, that still needs
|
|
# to be authed
|
|
tgt_name = Utils.create_name()
|
|
self.env.unversioned_container.file(tgt_name).write(b'link')
|
|
sym_tgt_header = quote(unquote('%s/%s' % (
|
|
self.env.unversioned_container.name, tgt_name)))
|
|
obj.write(hdrs={'X-Symlink-Target': sym_tgt_header})
|
|
|
|
# So, user1's good...
|
|
self.assertEqual(b'link', obj.read())
|
|
# ...but user3 can't
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.read(hdrs={'X-Auth-Token': self.env.conn3.storage_token})
|
|
self.assertEqual(403, cm.exception.status)
|
|
|
|
# unless we add the acl to the unversioned_container
|
|
self.env.unversioned_container.update_metadata(
|
|
hdrs={'X-Container-Read': self.env.conn3.user_acl})
|
|
self.assertEqual(b'link', obj.read(
|
|
hdrs={'X-Auth-Token': self.env.conn3.storage_token}))
|
|
|
|
def _test_overwriting_setup(self, obj_name=None):
|
|
# sanity
|
|
container = self.env.container
|
|
self.assertTrue(
|
|
config_true_value(self.env.container.info()['versions_enabled']))
|
|
|
|
expected_content_types = []
|
|
self.assertTotalVersions(container, 0)
|
|
obj_name = obj_name or Utils.create_name()
|
|
|
|
versioned_obj = container.file(obj_name)
|
|
put_headers = {'Content-Type': 'text/jibberish01',
|
|
'Content-Encoding': 'gzip',
|
|
'Content-Disposition': 'attachment; filename=myfile'}
|
|
resp = versioned_obj.write(b"aaaaa", hdrs=put_headers,
|
|
return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
expected_content_types.append('text/jibberish01')
|
|
self.assertContentTypes(container, expected_content_types)
|
|
obj_info = versioned_obj.info()
|
|
self.assertEqual('text/jibberish01', obj_info['content_type'])
|
|
|
|
self.assertTotalVersions(container, 1)
|
|
resp = versioned_obj.write(
|
|
b"bbbbb",
|
|
hdrs={'Content-Type': 'text/jibberish02',
|
|
'X-Object-Meta-Foo': 'Bar'},
|
|
return_resp=True)
|
|
v2_version_id = resp.getheader('x-object-version-id')
|
|
versioned_obj.initialize()
|
|
self.assertEqual(versioned_obj.content_type, 'text/jibberish02')
|
|
self.assertEqual(versioned_obj.metadata['foo'], 'Bar')
|
|
resp_headers = {
|
|
h.lower(): v for h, v in versioned_obj.conn.response.getheaders()}
|
|
content_location = quote('/v1/%s/%s/%s' % (
|
|
self.account_name, container.name, obj_name
|
|
)) + '?version-id=%s' % (v2_version_id,)
|
|
self.assertEqual(content_location, resp_headers['content-location'])
|
|
expected_content_types.append('text/jibberish02')
|
|
self.assertContentTypes(container, expected_content_types)
|
|
|
|
# the old version got saved off
|
|
self.assertTotalVersions(container, 2)
|
|
|
|
self.assert_previous_version(
|
|
obj_name, v1_version_id, b'aaaaa', 'text/jibberish01',
|
|
not_expected_header_keys=['X-Object-Meta-Foo'],
|
|
check_env_expected_headers=True)
|
|
|
|
# check that POST does not create a new version
|
|
versioned_obj.sync_metadata(metadata={'fu': 'baz'})
|
|
self.assertTotalVersions(container, 2)
|
|
|
|
self.assert_previous_version(
|
|
obj_name, v2_version_id, b'bbbbb', 'text/jibberish02',
|
|
expected_headers={'X-Object-Meta-Fu': 'baz'})
|
|
|
|
# if we overwrite it again, there are three versions
|
|
resp = versioned_obj.write(b"ccccc", return_resp=True)
|
|
v3_version_id = resp.getheader('x-object-version-id')
|
|
expected_content_types.append('text/jibberish02')
|
|
self.assertContentTypes(container, expected_content_types)
|
|
self.assertTotalVersions(self.env.container, 3)
|
|
|
|
# versioned_obj keeps the newest content
|
|
self.assertEqual(b"ccccc", versioned_obj.read())
|
|
|
|
# test copy from a different container
|
|
src_container = self.env.account.container(Utils.create_name())
|
|
self.assertTrue(src_container.create())
|
|
src_name = Utils.create_name()
|
|
src_obj = src_container.file(src_name)
|
|
src_obj.write(b"ddddd", hdrs={'Content-Type': 'text/jibberish04'})
|
|
src_obj.copy(container.name, obj_name)
|
|
expected_content_types.append('text/jibberish04')
|
|
self.assertContentTypes(container, expected_content_types)
|
|
|
|
self.assertEqual(b"ddddd", versioned_obj.read())
|
|
versioned_obj.initialize()
|
|
self.assertEqual(versioned_obj.content_type, 'text/jibberish04')
|
|
|
|
# make sure versions container has the previous version
|
|
self.assertTotalVersions(self.env.container, 4)
|
|
self.assert_previous_version(
|
|
obj_name, v3_version_id, b'ccccc', 'text/jibberish02')
|
|
|
|
# test delete
|
|
# at first, delete will succeed with 204
|
|
versioned_obj.delete()
|
|
expected_content_types.append(
|
|
'application/x-deleted;swift_versions_deleted=1')
|
|
|
|
# after that, any time the delete doesn't restore the old version
|
|
# and we will get 404 NotFound
|
|
for x in range(3):
|
|
with self.assertRaises(ResponseError) as cm:
|
|
versioned_obj.delete()
|
|
self.assertEqual(404, cm.exception.status)
|
|
expected_content_types.append(
|
|
'application/x-deleted;swift_versions_deleted=1')
|
|
|
|
# finally, we have 4 versioned items and 4 delete markers total in
|
|
# the versions container
|
|
self.assertTotalVersions(self.env.container, 8)
|
|
self.assertContentTypes(self.env.container, expected_content_types)
|
|
|
|
# update versioned_obj
|
|
versioned_obj.write(b"eeee", hdrs={'Content-Type': 'text/thanksgiving',
|
|
'X-Object-Meta-Bar': 'foo'})
|
|
|
|
# verify the PUT object is kept successfully
|
|
obj_info = versioned_obj.info()
|
|
self.assertEqual('text/thanksgiving', obj_info['content_type'])
|
|
|
|
# 8 plus one more write
|
|
self.assertTotalVersions(self.env.container, 9)
|
|
|
|
# update versioned_obj
|
|
versioned_obj.write(b"ffff", hdrs={'Content-Type': 'text/teriyaki',
|
|
'X-Object-Meta-Food': 'chickin'})
|
|
|
|
# verify the PUT object is kept successfully
|
|
obj_info = versioned_obj.info()
|
|
self.assertEqual('text/teriyaki', obj_info['content_type'])
|
|
|
|
# 9 plus one more write
|
|
self.assertTotalVersions(self.env.container, 10)
|
|
|
|
versioned_obj.delete()
|
|
with self.assertRaises(ResponseError) as cm:
|
|
versioned_obj.read()
|
|
self.assertEqual(404, cm.exception.status)
|
|
|
|
# 10 plus delete marker
|
|
self.assertTotalVersions(self.env.container, 11)
|
|
|
|
return (versioned_obj, expected_content_types)
|
|
|
|
def test_overwriting(self):
|
|
versioned_obj, expected_content_types = \
|
|
self._test_overwriting_setup()
|
|
|
|
def test_make_old_version_latest(self):
|
|
obj_name = Utils.create_name()
|
|
versioned_obj = self.env.container.file(obj_name)
|
|
versions = [{
|
|
'content_type': 'text/jibberish01',
|
|
'body': b'aaaaa',
|
|
}, {
|
|
'content_type': 'text/jibberish02',
|
|
'body': b'bbbbbb',
|
|
}, {
|
|
'content_type': 'text/jibberish03',
|
|
'body': b'ccccccc',
|
|
}]
|
|
for version in versions:
|
|
resp = versioned_obj.write(version['body'], hdrs={
|
|
'Content-Type': version['content_type']}, return_resp=True)
|
|
version['version_id'] = resp.getheader('x-object-version-id')
|
|
expected = [{
|
|
'name': obj_name,
|
|
'content_type': version['content_type'],
|
|
'version_id': version['version_id'],
|
|
'hash': md5(version['body'], usedforsecurity=False).hexdigest(),
|
|
'bytes': len(version['body'],)
|
|
} for version in reversed(versions)]
|
|
for item, is_latest in zip(expected, (True, False, False)):
|
|
item['is_latest'] = is_latest
|
|
versions_listing = self.env.container.files(parms={
|
|
'versions': 'true', 'format': 'json'})
|
|
for item in versions_listing:
|
|
item.pop('last_modified')
|
|
self.assertEqual(expected, versions_listing)
|
|
|
|
versioned_obj.write(b'', parms={
|
|
'version-id': versions[1]['version_id']})
|
|
self.assertEqual(b'bbbbbb', versioned_obj.read())
|
|
for item, is_latest in zip(expected, (False, True, False)):
|
|
item['is_latest'] = is_latest
|
|
versions_listing = self.env.container.files(parms={
|
|
'versions': 'true', 'format': 'json'})
|
|
for item in versions_listing:
|
|
item.pop('last_modified')
|
|
self.assertEqual(expected, versions_listing)
|
|
|
|
def test_overwriting_with_url_encoded_object_name(self):
|
|
obj_name = Utils.create_name() + '%25ff'
|
|
versioned_obj, expected_content_types = \
|
|
self._test_overwriting_setup(obj_name)
|
|
|
|
def _test_versioning_dlo_setup(self):
|
|
if tf.in_process:
|
|
tf.skip_if_no_xattrs()
|
|
|
|
container = self.env.container
|
|
obj_name = Utils.create_name()
|
|
|
|
for i in ('1', '2', '3'):
|
|
time.sleep(.01) # guarantee that the timestamp changes
|
|
obj_name_seg = 'segs_' + obj_name + '/' + i
|
|
versioned_obj = container.file(obj_name_seg)
|
|
versioned_obj.write(i.encode('ascii'))
|
|
# immediately overwrite
|
|
versioned_obj.write((i + i).encode('ascii'))
|
|
|
|
# three objects 2 versions each
|
|
self.assertTotalVersions(self.env.container, 6)
|
|
|
|
man_file = container.file(obj_name)
|
|
|
|
# write a normal file first
|
|
resp = man_file.write(
|
|
b'old content', hdrs={'Content-Type': 'text/jibberish01'},
|
|
return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
self.assertEqual(b'old content', man_file.read())
|
|
|
|
# guarantee that the timestamp changes
|
|
time.sleep(.01)
|
|
|
|
# overwrite with a dlo manifest
|
|
dlo_prefix = quote(unquote('%s/segs_%s/' % (
|
|
self.env.container.name, obj_name)))
|
|
resp = man_file.write(
|
|
b'', hdrs={'Content-Type': 'text/jibberish02',
|
|
'X-Object-Manifest': dlo_prefix},
|
|
return_resp=True)
|
|
v2_version_id = resp.getheader('x-object-version-id')
|
|
|
|
self.assertTotalVersions(self.env.container, 8)
|
|
self.assertEqual(b'112233', man_file.read())
|
|
|
|
self.assert_previous_version(
|
|
obj_name, v1_version_id, b'old content', 'text/jibberish01')
|
|
|
|
# overwrite the manifest with a normal file
|
|
man_file.write(b'new content')
|
|
self.assertTotalVersions(self.env.container, 9)
|
|
self.assertEqual(b'new content', man_file.read())
|
|
|
|
# new most-recent archive is the dlo
|
|
self.assert_previous_version(
|
|
obj_name, v2_version_id, b'112233', 'text/jibberish02',
|
|
expected_headers={'X-Object-Manifest': dlo_prefix})
|
|
return obj_name, man_file
|
|
|
|
def test_versioning_dlo(self):
|
|
obj_name, man_file = \
|
|
self._test_versioning_dlo_setup()
|
|
|
|
man_file.delete()
|
|
with self.assertRaises(ResponseError) as cm:
|
|
man_file.read()
|
|
self.assertEqual(404, cm.exception.status)
|
|
|
|
# 9 plus one more write
|
|
self.assertTotalVersions(self.env.container, 10)
|
|
|
|
expected = [b'old content', b'112233', b'new content']
|
|
|
|
bodies = []
|
|
listing_parms = {'versions': None, 'format': 'json',
|
|
'reverse': 'true', 'prefix': obj_name}
|
|
for obj_info in self.env.container.files(parms=listing_parms)[:3]:
|
|
bodies.append(man_file.read(
|
|
parms={'version-id': obj_info['version_id']}))
|
|
self.assertEqual(expected, bodies)
|
|
|
|
def _check_overwriting_symlink(self):
|
|
# sanity
|
|
container = self.env.container
|
|
self.assertTrue(
|
|
config_true_value(self.env.container.info()['versions_enabled']))
|
|
|
|
tgt_a_name = Utils.create_name()
|
|
tgt_b_name = Utils.create_name()
|
|
expected_count = 0
|
|
|
|
tgt_a = container.file(tgt_a_name)
|
|
tgt_a.write(b'aaaaa', hdrs={'Content-Type': 'text/jibberish01'})
|
|
expected_count += 1
|
|
|
|
tgt_b = container.file(tgt_b_name)
|
|
tgt_b.write(b"bbbbb")
|
|
expected_count += 1
|
|
|
|
symlink_name = Utils.create_name()
|
|
sym_tgt_header = quote(unquote('%s/%s' % (container.name, tgt_a_name)))
|
|
sym_headers_a = {'X-Symlink-Target': sym_tgt_header}
|
|
symlink = container.file(symlink_name)
|
|
resp = symlink.write(b'', hdrs=sym_headers_a, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
expected_count += 1
|
|
self.assertEqual(b"aaaaa", symlink.read())
|
|
|
|
sym_headers_b = {'X-Symlink-Target': '%s/%s' % (container.name,
|
|
tgt_b_name)}
|
|
symlink.write(b"", hdrs=sym_headers_b)
|
|
expected_count += 1
|
|
self.assertEqual(b"bbbbb", symlink.read())
|
|
|
|
self.assertTotalVersions(container, expected_count)
|
|
self.assert_previous_version(
|
|
symlink_name, v1_version_id, b'aaaaa', 'text/jibberish01')
|
|
return symlink, tgt_a
|
|
|
|
def test_overwriting_symlink(self):
|
|
if 'symlink' not in cluster_info:
|
|
raise SkipTest("Symlinks not enabled")
|
|
|
|
symlink, target = self._check_overwriting_symlink()
|
|
# test delete
|
|
symlink.delete()
|
|
with self.assertRaises(ResponseError) as cm:
|
|
symlink.read()
|
|
self.assertEqual(404, cm.exception.status)
|
|
|
|
def _setup_symlink(self):
|
|
tgt_name = 'target-' + Utils.create_name()
|
|
target = self.env.container.file(tgt_name)
|
|
target.write(b'target object data',
|
|
hdrs={'Content-Type': 'text/jibberish01'})
|
|
symlink = self.env.container.file('symlink')
|
|
resp = symlink.write(b'', hdrs={
|
|
'Content-Type': 'application/symlink',
|
|
'X-Symlink-Target': '%s/%s' % (
|
|
self.env.container.name, target.name)},
|
|
return_resp=True)
|
|
symlink_version_id = resp.getheader('x-object-version-id')
|
|
return symlink, symlink_version_id, target
|
|
|
|
def _check_copy_destination_symlink(self):
|
|
symlink, sym_version_id, target = self._setup_symlink()
|
|
self.assertEqual(b'target object data', symlink.read())
|
|
symlink.write(b'this is not a symlink')
|
|
|
|
# target, symlink, and new 'not a symlink' overwritten by write
|
|
self.assertTotalVersions(self.env.container, 3)
|
|
self.assert_previous_version(
|
|
symlink.name, sym_version_id,
|
|
b'target object data', 'text/jibberish01')
|
|
|
|
# the symlink is still a symlink
|
|
prev_version = self.env.container.file(symlink.name)
|
|
prev_version.initialize(parms={'version-id': sym_version_id})
|
|
self.assertEqual('application/symlink',
|
|
prev_version.info(parms={
|
|
'version-id': sym_version_id,
|
|
'symlink': 'get'})['content_type'])
|
|
prev_version.copy(self.env.container.name, symlink.name,
|
|
parms={'version-id': sym_version_id,
|
|
'symlink': 'get'})
|
|
self.assertEqual(b'target object data', symlink.read())
|
|
self.assertTotalVersions(self.env.container, 4)
|
|
|
|
return symlink, target
|
|
|
|
def test_copy_destination_restore_symlink(self):
|
|
if 'symlink' not in cluster_info:
|
|
raise SkipTest("Symlinks not enabled")
|
|
|
|
symlink, target = self._check_copy_destination_symlink()
|
|
symlink.delete()
|
|
with self.assertRaises(ResponseError) as cm:
|
|
symlink.read()
|
|
self.assertEqual(404, cm.exception.status)
|
|
# symlink & target, plus overwrite and restore, then delete marker
|
|
self.assertTotalVersions(self.env.container, 5)
|
|
|
|
def test_versioned_staticlink(self):
|
|
tgt_name = 'target-' + Utils.create_name()
|
|
link_name = 'staticlink-' + Utils.create_name()
|
|
target = self.env.container.file(tgt_name)
|
|
staticlink = self.env.container.file(link_name)
|
|
|
|
target_resp = target.write(b'target object data', hdrs={
|
|
'Content-Type': 'text/jibberish01'}, return_resp=True)
|
|
staticlink.write(b'', hdrs={
|
|
'X-Symlink-Target': '%s/%s' % (
|
|
self.env.container.name, target.name),
|
|
'X-Symlink-Target-Etag': target_resp.getheader('etag'),
|
|
}, cfg={'no_content_type': True})
|
|
self.assertEqual(b'target object data', staticlink.read())
|
|
|
|
listing_parms = {'format': 'json', 'versions': 'true'}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
expected = [{
|
|
'name': link_name,
|
|
'bytes': 0,
|
|
'content_type': 'text/jibberish01',
|
|
'is_latest': True,
|
|
}, {
|
|
'name': tgt_name,
|
|
'bytes': 18,
|
|
'content_type': 'text/jibberish01',
|
|
'is_latest': True,
|
|
}]
|
|
self.assertEqual(expected, [{
|
|
k: i[k] for k in (
|
|
'name', 'bytes', 'content_type', 'is_latest',
|
|
)} for i in prev_versions])
|
|
|
|
target_resp = target.write(b'updated target data', hdrs={
|
|
'Content-Type': 'text/jibberish02'}, return_resp=True)
|
|
with self.assertRaises(ResponseError) as caught:
|
|
staticlink.read()
|
|
self.assertEqual(409, caught.exception.status)
|
|
staticlink.write(b'', hdrs={
|
|
'X-Symlink-Target': '%s/%s' % (
|
|
self.env.container.name, target.name),
|
|
'X-Symlink-Target-Etag': target_resp.getheader('etag'),
|
|
}, cfg={'no_content_type': True})
|
|
self.assertEqual(b'updated target data', staticlink.read())
|
|
|
|
listing_parms = {'format': 'json', 'versions': 'true'}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
expected = [{
|
|
'name': link_name,
|
|
'bytes': 0,
|
|
'content_type': 'text/jibberish02',
|
|
'is_latest': True,
|
|
}, {
|
|
'name': link_name,
|
|
'bytes': 0,
|
|
'content_type': 'text/jibberish01',
|
|
'is_latest': False,
|
|
}, {
|
|
'name': tgt_name,
|
|
'bytes': 19,
|
|
'content_type': 'text/jibberish02',
|
|
'is_latest': True,
|
|
}, {
|
|
'name': tgt_name,
|
|
'bytes': 18,
|
|
'content_type': 'text/jibberish01',
|
|
'is_latest': False,
|
|
}]
|
|
self.assertEqual(expected, [{
|
|
k: i[k] for k in (
|
|
'name', 'bytes', 'content_type', 'is_latest',
|
|
)} for i in prev_versions])
|
|
|
|
def test_link_to_versioned_object(self):
|
|
|
|
# setup target object
|
|
tgt_name = 'target-' + Utils.create_name()
|
|
target = self.env.container.file(tgt_name)
|
|
target_resp = target.write(b'target object data', hdrs={
|
|
'Content-Type': 'text/jibberish01'}, return_resp=True)
|
|
|
|
# setup dynamic link object from a non-versioned container
|
|
link_container_name = 'link-container-' + Utils.create_name()
|
|
link_name = 'link-' + Utils.create_name()
|
|
link_cont = self.env.account.container(link_container_name)
|
|
self.assertTrue(link_cont.create())
|
|
link = link_cont.file(link_name)
|
|
self.assertTrue(link.write(b'', hdrs={
|
|
'X-Symlink-Target': '%s/%s' % (
|
|
self.env.container.name, tgt_name),
|
|
}, cfg={'no_content_type': True}))
|
|
self.assertEqual(b'target object data', link.read())
|
|
|
|
# setup static link object from a non-versioned container
|
|
staticlink_name = 'staticlink-' + Utils.create_name()
|
|
staticlink = link_cont.file(staticlink_name)
|
|
self.assertTrue(staticlink.write(b'', hdrs={
|
|
'X-Symlink-Target': '%s/%s' % (
|
|
self.env.container.name, tgt_name),
|
|
'X-Symlink-Target-Etag': target_resp.getheader('etag'),
|
|
}, cfg={'no_content_type': True}))
|
|
self.assertEqual(b'target object data', link.read())
|
|
|
|
def test_versioned_post(self):
|
|
# first we'll create a versioned object
|
|
obj_name = Utils.create_name()
|
|
obj = self.env.container.file(obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish10'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
|
|
# send post request
|
|
obj.post(hdrs={'Content-Type': 'text/updated20'})
|
|
|
|
# head request should show updated content-type
|
|
obj_info = obj.info()
|
|
self.assertEqual(obj_info['content_type'], 'text/updated20')
|
|
|
|
listing_parms = {'format': 'json', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(1, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/updated20',
|
|
'hash': '966634ebf2fc135707d6753692bf4b1e',
|
|
'version_id': v1_version_id,
|
|
'is_latest': True,
|
|
}])
|
|
|
|
def test_unversioned_post(self):
|
|
# first we'll create a versioned object
|
|
obj_name = Utils.create_name()
|
|
obj = self.env.container.file(obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish10'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
|
|
# now, turn off versioning
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
obj.post(hdrs={'Content-Type': 'text/updated20'})
|
|
|
|
# head request should show updated content-type
|
|
obj_info = obj.info()
|
|
self.assertEqual(obj_info['content_type'], 'text/updated20')
|
|
|
|
listing_parms = {'format': 'json', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(1, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/updated20',
|
|
'hash': '966634ebf2fc135707d6753692bf4b1e',
|
|
'is_latest': True,
|
|
'version_id': v1_version_id,
|
|
'is_latest': True,
|
|
}])
|
|
|
|
def test_unversioned_overwrite_and_delete(self):
|
|
# first we'll create a versioned object
|
|
obj_name = Utils.create_name()
|
|
obj = self.env.container.file(obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish18'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
# now, turn off versioning, and delete source obj
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
obj.delete()
|
|
|
|
# no delete markers, archive listing is unchanged
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
# sanity, object is gone
|
|
self.assertRaises(ResponseError, obj.read)
|
|
self.assertEqual(404, obj.conn.response.status)
|
|
|
|
# but, archive version is unmodified
|
|
self.assert_previous_version(obj_name, v1_version_id, b'version1',
|
|
'text/jibberish18')
|
|
|
|
# a new overwrites will not have a version-id
|
|
resp = obj.write(b'version2', hdrs={
|
|
'Content-Type': 'text/jibberish19'
|
|
}, return_resp=True)
|
|
self.assertIsNone(resp.getheader('x-object-version-id'))
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
|
|
resp = obj.write(b'version3', hdrs={
|
|
'Content-Type': 'text/jibberish20'
|
|
}, return_resp=True)
|
|
self.assertIsNone(resp.getheader('x-object-version-id'))
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
|
|
obj.delete()
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
obj.delete(tolerate_missing=True)
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
def test_versioned_overwrite_from_old_version(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish32'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
v1_etag = normalize_etag(resp.getheader('etag'))
|
|
|
|
resp = obj.write(b'version2', hdrs={
|
|
'Content-Type': 'text/jibberish33'
|
|
}, return_resp=True)
|
|
v2_version_id = resp.getheader('x-object-version-id')
|
|
v2_etag = normalize_etag(resp.getheader('etag'))
|
|
|
|
# sanity
|
|
self.assertEqual(b'version2', obj.read())
|
|
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
listing_parms = {'format': 'json', 'reverse': 'true', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(2, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': versioned_obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish32',
|
|
'hash': v1_etag,
|
|
'version_id': v1_version_id,
|
|
'is_latest': False,
|
|
}, {
|
|
'name': versioned_obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish33',
|
|
'hash': v2_etag,
|
|
'version_id': v2_version_id,
|
|
'is_latest': True,
|
|
}])
|
|
|
|
# restore old version1 back in place with a copy request
|
|
# should get a new version-id
|
|
old_version_obj = self.env.container.file(versioned_obj_name)
|
|
resp = old_version_obj.copy(self.env.container.name,
|
|
versioned_obj_name,
|
|
parms={'version-id': v1_version_id},
|
|
return_resp=True)
|
|
v3_version_id = resp.getheader('x-object-version-id')
|
|
|
|
listing_parms = {'format': 'json', 'reverse': 'true', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(3, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': versioned_obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish32',
|
|
'hash': v1_etag,
|
|
'version_id': v1_version_id,
|
|
'is_latest': False,
|
|
}, {
|
|
'name': versioned_obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish33',
|
|
'hash': v2_etag,
|
|
'version_id': v2_version_id,
|
|
'is_latest': False,
|
|
}, {
|
|
'name': versioned_obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish32',
|
|
'hash': v1_etag,
|
|
'version_id': v3_version_id,
|
|
'is_latest': True,
|
|
}])
|
|
|
|
self.assertEqual(b'version1', obj.read())
|
|
obj_info = obj.info()
|
|
self.assertEqual('text/jibberish32', obj_info['content_type'])
|
|
self.assertEqual(v1_etag, normalize_etag(obj_info['etag']))
|
|
|
|
def test_delete_with_version_api_old_object(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish32'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
|
|
obj.write(b'version2', hdrs={'Content-Type': 'text/jibberish33'})
|
|
|
|
# sanity
|
|
self.assertEqual(b'version2', obj.read())
|
|
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
obj.delete(parms={'version-id': v1_version_id})
|
|
|
|
self.assertEqual(b'version2', obj.read())
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
def test_delete_with_version_api_current_object(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
obj.write(b'version1', hdrs={'Content-Type': 'text/jibberish32'})
|
|
|
|
resp = obj.write(b'version2', hdrs={
|
|
'Content-Type': 'text/jibberish33'
|
|
}, return_resp=True)
|
|
v2_version_id = resp.getheader('x-object-version-id')
|
|
|
|
# sanity
|
|
self.assertEqual(b'version2', obj.read())
|
|
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
obj.delete(parms={'version-id': v2_version_id})
|
|
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.read()
|
|
self.assertEqual(404, cm.exception.status)
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
def test_delete_delete_marker_with_version_api(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
obj.write(b'version1', hdrs={'Content-Type': 'text/jibberish32'})
|
|
|
|
obj.delete()
|
|
resp_headers = {
|
|
h.lower(): v for h, v in obj.conn.response.getheaders()}
|
|
self.assertIn('x-object-version-id', resp_headers)
|
|
dm_version_id = resp_headers['x-object-version-id']
|
|
|
|
# sanity
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.info(parms={'version-id': dm_version_id})
|
|
resp_headers = {
|
|
h.lower(): v for h, v in cm.exception.headers}
|
|
self.assertEqual(dm_version_id,
|
|
resp_headers['x-object-version-id'])
|
|
self.assertEqual(DELETE_MARKER_CONTENT_TYPE,
|
|
resp_headers['content-type'])
|
|
|
|
obj.delete(parms={'version-id': dm_version_id})
|
|
resp_headers = {
|
|
h.lower(): v for h, v in obj.conn.response.getheaders()}
|
|
self.assertEqual(dm_version_id,
|
|
resp_headers['x-object-version-id'])
|
|
|
|
def test_delete_with_version_api_last_object(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish1'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
|
|
# sanity
|
|
self.assertEqual(b'version1', obj.read())
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
# delete
|
|
obj.delete(parms={'version-id': v1_version_id})
|
|
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.read()
|
|
self.assertEqual(404, cm.exception.status)
|
|
self.assertTotalVersions(self.env.container, 0)
|
|
|
|
def test_delete_with_version_api_null_version(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
obj.write(b'version1', hdrs={'Content-Type': 'text/jibberish32'})
|
|
obj.write(b'version2', hdrs={'Content-Type': 'text/jibberish33'})
|
|
|
|
# sanity
|
|
self.assertEqual(b'version2', obj.read())
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
|
|
obj.delete(parms={'version-id': 'null'})
|
|
with self.assertRaises(ResponseError) as caught:
|
|
obj.read()
|
|
self.assertEqual(404, caught.exception.status)
|
|
|
|
# no versions removed
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
|
|
def test_delete_with_version_api_old_object_disabled(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish32'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
|
|
obj.write(b'version2', hdrs={'Content-Type': 'text/jibberish33'})
|
|
|
|
# disabled versioning
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
# sanity
|
|
self.assertEqual(b'version2', obj.read())
|
|
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
obj.delete(parms={'version-id': v1_version_id})
|
|
|
|
self.assertEqual(b'version2', obj.read())
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
def test_delete_with_version_api_current_object_disabled(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
obj.write(b'version1', hdrs={'Content-Type': 'text/jibberish32'})
|
|
|
|
resp = obj.write(b'version2', hdrs={
|
|
'Content-Type': 'text/jibberish33'
|
|
}, return_resp=True)
|
|
v2_version_id = resp.getheader('x-object-version-id')
|
|
|
|
# disabled versioning
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
# sanity
|
|
self.assertEqual(b'version2', obj.read())
|
|
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
obj.delete(parms={'version-id': v2_version_id})
|
|
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.read()
|
|
self.assertEqual(404, cm.exception.status)
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
def test_delete_with_version_api_old_object_current_unversioned(self):
|
|
versioned_obj_name = Utils.create_name()
|
|
obj = self.env.container.file(versioned_obj_name)
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish32'
|
|
}, return_resp=True)
|
|
v1_version_id = resp.getheader('x-object-version-id')
|
|
|
|
# disabled versioning
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
# write unversioned object (i.e., version-id='null')
|
|
obj.write(b'version2', hdrs={'Content-Type': 'text/jibberish33'})
|
|
|
|
# sanity
|
|
self.assertEqual(b'version2', obj.read())
|
|
|
|
self.assertTotalVersions(self.env.container, 2)
|
|
obj.delete(parms={'version-id': v1_version_id})
|
|
|
|
self.assertEqual(b'version2', obj.read())
|
|
self.assertTotalVersions(self.env.container, 1)
|
|
|
|
|
|
class TestObjectVersioningUTF8(Base2, TestObjectVersioning):
|
|
pass
|
|
|
|
|
|
class TestContainerOperations(TestObjectVersioningBase):
|
|
|
|
def _prep_object_versions(self):
|
|
|
|
# object with multiple versions and currently deleted
|
|
obj1_v1 = {}
|
|
obj1_v1['name'] = 'c' + Utils.create_name()
|
|
obj = self.env.container.file(obj1_v1['name'])
|
|
|
|
# v1
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish11',
|
|
'ETag': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
}, return_resp=True)
|
|
obj1_v1['id'] = resp.getheader('x-object-version-id')
|
|
|
|
# v2
|
|
resp = obj.write(b'version2', hdrs={
|
|
'Content-Type': 'text/jibberish12',
|
|
'ETag': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
}, return_resp=True)
|
|
obj1_v2 = {}
|
|
obj1_v2['name'] = obj1_v1['name']
|
|
obj1_v2['id'] = resp.getheader('x-object-version-id')
|
|
|
|
# v3
|
|
resp = obj.write(b'version3', hdrs={
|
|
'Content-Type': 'text/jibberish13',
|
|
'ETag': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
}, return_resp=True)
|
|
obj1_v3 = {}
|
|
obj1_v3['name'] = obj1_v1['name']
|
|
obj1_v3['id'] = resp.getheader('x-object-version-id')
|
|
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.write(b'version4', hdrs={
|
|
'Content-Type': 'text/jibberish11',
|
|
'ETag': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
})
|
|
self.assertEqual(422, cm.exception.status)
|
|
|
|
# v4
|
|
obj.delete()
|
|
resp_headers = {
|
|
h.lower(): v for h, v in obj.conn.response.getheaders()}
|
|
obj1_v4 = {}
|
|
obj1_v4['name'] = obj1_v1['name']
|
|
obj1_v4['id'] = resp_headers.get('x-object-version-id')
|
|
|
|
# object with just a single version
|
|
obj2_v1 = {}
|
|
obj2_v1['name'] = 'b' + Utils.create_name()
|
|
obj = self.env.container.file(obj2_v1['name'])
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish20',
|
|
'ETag': '966634ebf2fc135707d6753692bf4b1e',
|
|
}, return_resp=True)
|
|
obj2_v1['id'] = resp.getheader('x-object-version-id')
|
|
|
|
# object never existed, just a delete marker
|
|
obj3_v1 = {}
|
|
obj3_v1['name'] = 'a' + Utils.create_name()
|
|
obj = self.env.container.file(obj3_v1['name'])
|
|
obj.delete(tolerate_missing=True)
|
|
self.assertEqual(obj.conn.response.status, 404)
|
|
resp_headers = {
|
|
h.lower(): v for h, v in obj.conn.response.getheaders()}
|
|
obj3_v1['id'] = resp_headers.get('x-object-version-id')
|
|
|
|
return (obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1)
|
|
|
|
def _prep_unversioned_objects(self):
|
|
objs = (
|
|
'deleted' + Utils.create_name(),
|
|
'in' + Utils.create_name(),
|
|
'order' + Utils.create_name(),
|
|
)
|
|
|
|
# object with multiple writes and currently deleted
|
|
obj = self.env.unversioned_container.file(objs[0])
|
|
obj.write(b'data', hdrs={
|
|
'Content-Type': 'text/jibberish11',
|
|
'ETag': md5(b'data', usedforsecurity=False).hexdigest(),
|
|
})
|
|
obj.delete()
|
|
|
|
obj = self.env.unversioned_container.file(objs[1])
|
|
obj.write(b'first', hdrs={
|
|
'Content-Type': 'text/blah-blah-blah',
|
|
'ETag': md5(b'first', usedforsecurity=False).hexdigest(),
|
|
})
|
|
|
|
obj = self.env.unversioned_container.file(objs[2])
|
|
obj.write(b'second', hdrs={
|
|
'Content-Type': 'text/plain',
|
|
'ETag': md5(b'second', usedforsecurity=False).hexdigest(),
|
|
})
|
|
return objs
|
|
|
|
def test_list_all_versions(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions in container
|
|
listing_parms = {'format': 'json', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(6, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj3_v1['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj3_v1['id'],
|
|
}, {
|
|
'name': obj2_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish20',
|
|
'hash': '966634ebf2fc135707d6753692bf4b1e',
|
|
'is_latest': True,
|
|
'version_id': obj2_v1['id'],
|
|
}, {
|
|
'name': obj1_v4['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}, {
|
|
'name': obj1_v3['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish13',
|
|
'hash': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v3['id'],
|
|
}, {
|
|
'name': obj1_v2['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v2['id'],
|
|
}, {
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v1['id'],
|
|
}])
|
|
|
|
def test_list_all_versions_reverse(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions in container in reverse order
|
|
listing_parms = {'format': 'json', 'reverse': 'true', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(6, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v1['id'],
|
|
}, {
|
|
'name': obj1_v2['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v2['id'],
|
|
}, {
|
|
'name': obj1_v3['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish13',
|
|
'hash': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v3['id'],
|
|
}, {
|
|
'name': obj1_v4['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}, {
|
|
'name': obj2_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish20',
|
|
'hash': '966634ebf2fc135707d6753692bf4b1e',
|
|
'is_latest': True,
|
|
'version_id': obj2_v1['id'],
|
|
}, {
|
|
'name': obj3_v1['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj3_v1['id'],
|
|
}])
|
|
|
|
def test_list_versions_prefix(self):
|
|
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions for a given object
|
|
listing_parms = {'format': 'json',
|
|
'versions': None, 'prefix': obj1_v1['name']}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(4, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v4['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}, {
|
|
'name': obj1_v3['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish13',
|
|
'hash': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v3['id'],
|
|
}, {
|
|
'name': obj1_v2['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v2['id'],
|
|
}, {
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v1['id'],
|
|
}])
|
|
|
|
def test_list_versions_prefix_reverse(self):
|
|
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions for a given object in reverse order
|
|
listing_parms = {'format': 'json', 'reverse': 'true',
|
|
'versions': None, 'prefix': obj1_v1['name']}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(4, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v1['id'],
|
|
}, {
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v2['id'],
|
|
}, {
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish13',
|
|
'hash': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v3['id'],
|
|
}, {
|
|
'name': obj1_v1['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}])
|
|
|
|
def test_list_limit(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions in container
|
|
listing_parms = {'format': 'json',
|
|
'limit': 3,
|
|
'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(3, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj3_v1['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj3_v1['id'],
|
|
}, {
|
|
'name': obj2_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish20',
|
|
'hash': '966634ebf2fc135707d6753692bf4b1e',
|
|
'is_latest': True,
|
|
'version_id': obj2_v1['id'],
|
|
}, {
|
|
'name': obj1_v4['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}])
|
|
|
|
def test_list_limit_marker(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions in container
|
|
listing_parms = {'format': 'json',
|
|
'limit': 2,
|
|
'marker': obj2_v1['name'],
|
|
'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(2, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v4['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}, {
|
|
'name': obj1_v3['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish13',
|
|
'hash': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v3['id'],
|
|
}])
|
|
|
|
def test_list_version_marker(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions starting with version_marker
|
|
listing_parms = {'format': 'json',
|
|
'marker': obj1_v3['name'],
|
|
'version_marker': obj1_v3['id'],
|
|
'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(2, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v2['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v2['id'],
|
|
}, {
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v1['id'],
|
|
}])
|
|
|
|
def test_list_version_marker_reverse(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list all versions starting with version_marker in reverse order
|
|
listing_parms = {'format': 'json',
|
|
'marker': obj1_v3['name'],
|
|
'version_marker': obj1_v3['id'],
|
|
'reverse': 'true',
|
|
'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(3, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v4['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}, {
|
|
'name': obj2_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish20',
|
|
'hash': '966634ebf2fc135707d6753692bf4b1e',
|
|
'is_latest': True,
|
|
'version_id': obj2_v1['id'],
|
|
}, {
|
|
'name': obj3_v1['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj3_v1['id'],
|
|
}])
|
|
|
|
def test_list_prefix_version_marker(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list versions with prefix starting with version_marker
|
|
listing_parms = {'format': 'json',
|
|
'prefix': obj1_v3['name'],
|
|
'marker': obj1_v3['name'],
|
|
'version_marker': obj1_v3['id'],
|
|
'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(2, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v2['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v2['id'],
|
|
}, {
|
|
'name': obj1_v1['name'],
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj1_v1['id'],
|
|
}])
|
|
|
|
def test_list_prefix_version_marker_reverse(self):
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
# list versions with prefix starting with version_marker
|
|
# in reverse order
|
|
listing_parms = {'format': 'json',
|
|
'prefix': obj1_v3['name'],
|
|
'marker': obj1_v3['name'],
|
|
'version_marker': obj1_v3['id'],
|
|
'reverse': 'true',
|
|
'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(1, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj1_v4['name'],
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': True,
|
|
'version_id': obj1_v4['id'],
|
|
}])
|
|
|
|
def test_unacceptable(self):
|
|
def do_test(format):
|
|
with self.assertRaises(ResponseError) as caught:
|
|
self.env.container.files(parms={
|
|
'format': format, 'versions': None})
|
|
self.assertEqual(caught.exception.status, 406)
|
|
|
|
do_test('plain')
|
|
do_test('xml')
|
|
|
|
def do_test(accept):
|
|
with self.assertRaises(ResponseError) as caught:
|
|
self.env.container.files(hdrs={'Accept': accept},
|
|
parms={'versions': None})
|
|
self.assertEqual(caught.exception.status, 406)
|
|
|
|
do_test('text/plain')
|
|
do_test('text/xml')
|
|
do_test('application/xml')
|
|
do_test('foo/bar')
|
|
|
|
def testFileListingLimitMarkerPrefix(self):
|
|
cont = self.env.container
|
|
|
|
files = ['apple', 'banana', 'cacao', 'date', 'elderberry']
|
|
for f in files:
|
|
file_item = cont.file(f)
|
|
self.assertTrue(file_item.write_random())
|
|
# immediately ovewrite
|
|
self.assertTrue(file_item.write_random())
|
|
time.sleep(.01) # guarantee that the timestamp changes
|
|
|
|
# sanity
|
|
for i in range(len(files)):
|
|
f = files[i]
|
|
for j in range(1, len(files) - i):
|
|
self.assertEqual(cont.files(parms={'limit': j, 'marker': f}),
|
|
files[i + 1: i + j + 1])
|
|
self.assertEqual(cont.files(parms={'marker': f}), files[i + 1:])
|
|
self.assertEqual(cont.files(parms={'marker': f, 'prefix': f}), [])
|
|
self.assertEqual(cont.files(parms={'prefix': f}), [f])
|
|
|
|
# repeat items in files list
|
|
versions = [f2 for f1 in files for f2 in (f1,) * 2]
|
|
|
|
# now list versions too
|
|
v = 0
|
|
for i in range(len(files)):
|
|
f = files[i]
|
|
for j in range(1, len(files) - i):
|
|
self.assertEqual(versions[i + v + 2: i + j + v + 2], [
|
|
item['name'] for item in cont.files(parms={
|
|
'limit': j, 'marker': f, 'versions': None})])
|
|
self.assertEqual(versions[v + i + 2:], [
|
|
item['name'] for item in cont.files(parms={
|
|
'marker': f, 'versions': None})])
|
|
self.assertEqual(cont.files(parms={'marker': f, 'prefix': f,
|
|
'versions': None}), [])
|
|
self.assertEqual([f, f], [
|
|
item['name'] for item in cont.files(parms={
|
|
'prefix': f, 'versions': None})])
|
|
v = v + 1
|
|
|
|
def testPrefixAndLimit(self):
|
|
cont = self.env.container
|
|
|
|
prefix_file_count = 10
|
|
limit_count = 2
|
|
prefixs = ['apple/', 'banana/', 'cacao/']
|
|
prefix_files = {}
|
|
|
|
for prefix in prefixs:
|
|
prefix_files[prefix] = []
|
|
|
|
for i in range(prefix_file_count):
|
|
file_item = cont.file(prefix + Utils.create_name())
|
|
self.assertTrue(file_item.write_random())
|
|
self.assertTrue(file_item.write_random())
|
|
prefix_files[prefix].append(file_item.name)
|
|
time.sleep(.01) # guarantee that the timestamp changes
|
|
|
|
versions_prefix_files = {}
|
|
for prefix in prefixs:
|
|
versions_prefix_files[prefix] = [f2 for f1 in prefix_files[prefix]
|
|
for f2 in (f1,) * 2]
|
|
# sanity
|
|
for format_type in [None, 'json', 'xml']:
|
|
for prefix in prefixs:
|
|
files = cont.files(parms={'prefix': prefix,
|
|
'format': format_type})
|
|
if isinstance(files[0], dict):
|
|
files = [x.get('name', x.get('subdir')) for x in files]
|
|
self.assertEqual(files, sorted(prefix_files[prefix]))
|
|
|
|
# list versions
|
|
for format_type in [None, 'json']:
|
|
for prefix in prefixs:
|
|
files = cont.files(parms={'prefix': prefix,
|
|
'versions': None,
|
|
'format': format_type})
|
|
if isinstance(files[0], dict):
|
|
files = [x.get('name', x.get('subdir')) for x in files]
|
|
self.assertEqual(files, sorted(versions_prefix_files[prefix]))
|
|
|
|
# list versions
|
|
for format_type in [None, 'json']:
|
|
for prefix in prefixs:
|
|
files = cont.files(parms={'limit': limit_count,
|
|
'versions': None,
|
|
'prefix': prefix,
|
|
'format': format_type})
|
|
if isinstance(files[0], dict):
|
|
files = [x.get('name', x.get('subdir')) for x in files]
|
|
self.assertEqual(len(files), limit_count)
|
|
|
|
for file_item in files:
|
|
self.assertTrue(file_item.startswith(prefix))
|
|
|
|
def testListDelimiter(self):
|
|
cont = self.env.container
|
|
|
|
delimiter = '-'
|
|
files = ['test', delimiter.join(['test', 'bar']),
|
|
delimiter.join(['test', 'foo'])]
|
|
for f in files:
|
|
file_item = cont.file(f)
|
|
self.assertTrue(file_item.write_random())
|
|
|
|
# object with no current version, just a delete marker
|
|
del_file = 'del-baz'
|
|
obj = self.env.container.file(del_file)
|
|
obj.delete(tolerate_missing=True)
|
|
self.assertEqual(obj.conn.response.status, 404)
|
|
|
|
# now, turn off versioning and write a un-versioned obj
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
# a new write will not have a version-id
|
|
off_file = 'off-xyz'
|
|
obj = self.env.container.file(off_file)
|
|
resp = obj.write(b'unversioned', return_resp=True)
|
|
self.assertIsNone(resp.getheader('x-object-version-id'))
|
|
|
|
# sanity
|
|
# list latest, delete marker should not show-up
|
|
for format_type in [None, 'json', 'xml']:
|
|
results = cont.files(parms={'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['off-xyz', 'test', 'test-bar',
|
|
'test-foo'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['off-', 'test', 'test-'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'format': format_type,
|
|
'reverse': 'yes'})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['test-', 'test', 'off-'])
|
|
|
|
# list versions, we should see delete marker here
|
|
for format_type in [None, 'json']:
|
|
results = cont.files(parms={'versions': None,
|
|
'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['del-baz', 'off-xyz', 'test',
|
|
'test-bar', 'test-foo'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'versions': None,
|
|
'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['del-', 'off-', 'test', 'test-'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'versions': None,
|
|
'format': format_type,
|
|
'reverse': 'yes'})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['test-', 'test', 'off-', 'del-'])
|
|
|
|
def testListMultiCharDelimiter(self):
|
|
cont = self.env.container
|
|
|
|
delimiter = '-&'
|
|
files = ['test', delimiter.join(['test', 'bar']),
|
|
delimiter.join(['test', 'foo'])]
|
|
for f in files:
|
|
file_item = cont.file(f)
|
|
self.assertTrue(file_item.write_random())
|
|
|
|
# object with no current version, just a delete marker
|
|
del_file = 'del-&baz'
|
|
obj = self.env.container.file(del_file)
|
|
obj.delete(tolerate_missing=True)
|
|
self.assertEqual(obj.conn.response.status, 404)
|
|
|
|
# now, turn off versioning and write a un-versioned obj
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
# a new write will not have a version-id
|
|
off_file = 'off-&xyz'
|
|
obj = self.env.container.file(off_file)
|
|
resp = obj.write(b'unversioned', return_resp=True)
|
|
self.assertIsNone(resp.getheader('x-object-version-id'))
|
|
|
|
# sanity
|
|
# list latest, delete marker should not show-up
|
|
for format_type in [None, 'json', 'xml']:
|
|
results = cont.files(parms={'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['off-&xyz', 'test', 'test-&bar',
|
|
'test-&foo'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['off-&', 'test', 'test-&'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'format': format_type,
|
|
'reverse': 'yes'})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['test-&', 'test', 'off-&'])
|
|
|
|
# list versions, we should see delete marker here
|
|
for format_type in [None, 'json']:
|
|
results = cont.files(parms={'versions': None,
|
|
'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['del-&baz', 'off-&xyz', 'test',
|
|
'test-&bar', 'test-&foo'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'versions': None,
|
|
'format': format_type})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['del-&', 'off-&', 'test', 'test-&'])
|
|
|
|
results = cont.files(parms={'delimiter': delimiter,
|
|
'versions': None,
|
|
'format': format_type,
|
|
'reverse': 'yes'})
|
|
if isinstance(results[0], dict):
|
|
results = [x.get('name', x.get('subdir')) for x in results]
|
|
self.assertEqual(results, ['test-&', 'test', 'off-&', 'del-&'])
|
|
|
|
def test_bytes_count(self):
|
|
|
|
container = self.env.container
|
|
|
|
# first store a non-versioned object
|
|
# disable versioning
|
|
container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
self.assertFalse(
|
|
config_true_value(container.info()['versions_enabled']))
|
|
|
|
obj = container.file(Utils.create_name())
|
|
self.assertTrue(obj.write(b'not-versioned'))
|
|
self.assertTotalVersions(container, 1)
|
|
|
|
# enable versioning
|
|
container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'True'})
|
|
self.assertTrue(
|
|
config_true_value(container.info()['versions_enabled']))
|
|
|
|
obj1_v1, obj1_v2, obj1_v3, obj1_v4, obj2_v1, obj3_v1 = \
|
|
self._prep_object_versions()
|
|
|
|
self.assertEqual(int(container.info()['bytes_used']), 32 + obj.size)
|
|
self.assertEqual(int(container.info()['object_count']), 2)
|
|
self.assertTotalVersions(container, 7)
|
|
|
|
def test_container_quota_bytes(self):
|
|
if 'container_quotas' not in tf.cluster_info:
|
|
raise SkipTest('Container quotas not enabled')
|
|
|
|
if tf.in_process:
|
|
tf.skip_if_no_xattrs()
|
|
|
|
container = self.env.container
|
|
|
|
# write two versions of 5 bytes each
|
|
obj = container.file(Utils.create_name())
|
|
self.assertTrue(obj.write(b'aaaaa'))
|
|
self.assertTrue(obj.write(b'bbbbb'))
|
|
self.assertTotalVersions(container, 2)
|
|
|
|
# set X-Container-Meta-Quota-Bytes is 10
|
|
container.update_metadata(
|
|
hdrs={'X-Container-Meta-Quota-Bytes': '10'})
|
|
self.assertEqual(container.info()['container_quota_bytes'], '10')
|
|
|
|
with self.assertRaises(ResponseError) as cm:
|
|
obj.write(b'ccccc')
|
|
self.assertEqual(413, cm.exception.status)
|
|
|
|
# reset container quota
|
|
container.update_metadata(
|
|
hdrs={'X-Container-Meta-Quota-Bytes': ''})
|
|
|
|
def test_list_unversioned_container(self):
|
|
_obj1, obj2, obj3 = self._prep_unversioned_objects()
|
|
# _obj1 got deleted, so won't show up at all
|
|
item2 = {
|
|
'name': obj2,
|
|
'bytes': 5,
|
|
'content_type': 'text/blah-blah-blah',
|
|
'hash': md5(b'first', usedforsecurity=False).hexdigest(),
|
|
'is_latest': True,
|
|
'version_id': 'null',
|
|
}
|
|
item3 = {
|
|
'name': obj3,
|
|
'bytes': 6,
|
|
'content_type': 'text/plain',
|
|
'hash': md5(b'second', usedforsecurity=False).hexdigest(),
|
|
'is_latest': True,
|
|
'version_id': 'null',
|
|
}
|
|
|
|
# version-aware listing works for unversioned containers
|
|
listing_parms = {'format': 'json',
|
|
'versions': None}
|
|
listing = self.env.unversioned_container.files(parms=listing_parms)
|
|
for item in listing:
|
|
item.pop('last_modified')
|
|
self.assertEqual(listing, [item2, item3])
|
|
|
|
listing_parms = {'format': 'json',
|
|
'prefix': obj2[:2],
|
|
'versions': None}
|
|
listing = self.env.unversioned_container.files(parms=listing_parms)
|
|
for item in listing:
|
|
item.pop('last_modified')
|
|
self.assertEqual(listing, [item2])
|
|
|
|
listing_parms = {'format': 'json',
|
|
'marker': obj2,
|
|
'versions': None}
|
|
listing = self.env.unversioned_container.files(parms=listing_parms)
|
|
for item in listing:
|
|
item.pop('last_modified')
|
|
self.assertEqual(listing, [item3])
|
|
|
|
listing_parms = {'format': 'json',
|
|
'delimiter': 'er',
|
|
'versions': None}
|
|
listing = self.env.unversioned_container.files(parms=listing_parms)
|
|
for item in listing:
|
|
if 'name' in item:
|
|
item.pop('last_modified')
|
|
self.assertEqual(listing, [item2, {'subdir': 'order'}])
|
|
|
|
listing_parms = {'format': 'json',
|
|
'reverse': 'true',
|
|
'versions': None}
|
|
listing = self.env.unversioned_container.files(parms=listing_parms)
|
|
for item in listing:
|
|
item.pop('last_modified')
|
|
self.assertEqual(listing, [item3, item2])
|
|
|
|
def test_is_latest(self):
|
|
obj = self.env.container.file(Utils.create_name())
|
|
|
|
# v1
|
|
resp = obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish11',
|
|
'ETag': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
}, return_resp=True)
|
|
obj_v1 = resp.getheader('x-object-version-id')
|
|
|
|
# v2
|
|
resp = obj.write(b'version2', hdrs={
|
|
'Content-Type': 'text/jibberish12',
|
|
'ETag': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
}, return_resp=True)
|
|
obj_v2 = resp.getheader('x-object-version-id')
|
|
|
|
obj.delete()
|
|
resp_headers = {
|
|
h.lower(): v for h, v in obj.conn.response.getheaders()}
|
|
obj_v3 = resp_headers.get('x-object-version-id')
|
|
|
|
resp = obj.write(b'version4', hdrs={
|
|
'Content-Type': 'text/jibberish14',
|
|
'ETag': md5(b'version4', usedforsecurity=False).hexdigest(),
|
|
}, return_resp=True)
|
|
obj_v4 = resp.getheader('x-object-version-id')
|
|
|
|
listing_parms = {'format': 'json', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(4, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj.name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish14',
|
|
'hash': md5(b'version4', usedforsecurity=False).hexdigest(),
|
|
'is_latest': True,
|
|
'version_id': obj_v4,
|
|
}, {
|
|
'name': obj.name,
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': False,
|
|
'version_id': obj_v3,
|
|
}, {
|
|
'name': obj.name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj_v2,
|
|
}, {
|
|
'name': obj.name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj_v1,
|
|
}])
|
|
|
|
self.env.container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
|
|
# v5 - non-versioned
|
|
obj.write(b'version5', hdrs={
|
|
'Content-Type': 'text/jibberish15',
|
|
'ETag': md5(b'version5', usedforsecurity=False).hexdigest(),
|
|
})
|
|
|
|
listing_parms = {'format': 'json', 'versions': None}
|
|
prev_versions = self.env.container.files(parms=listing_parms)
|
|
self.assertEqual(5, len(prev_versions))
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(prev_versions, [{
|
|
'name': obj.name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish15',
|
|
'hash': md5(b'version5', usedforsecurity=False).hexdigest(),
|
|
'is_latest': True,
|
|
'version_id': 'null',
|
|
}, {
|
|
'name': obj.name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish14',
|
|
'hash': md5(b'version4', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj_v4,
|
|
}, {
|
|
'name': obj.name,
|
|
'bytes': 0,
|
|
'content_type': 'application/x-deleted;swift_versions_deleted=1',
|
|
'hash': MD5_OF_EMPTY_STRING,
|
|
'is_latest': False,
|
|
'version_id': obj_v3,
|
|
}, {
|
|
'name': obj.name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj_v2,
|
|
}, {
|
|
'name': obj.name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': False,
|
|
'version_id': obj_v1,
|
|
}])
|
|
|
|
|
|
class TestContainerOperationsUTF8(Base2, TestContainerOperations):
|
|
pass
|
|
|
|
|
|
class TestDeleteContainer(TestObjectVersioningBase):
|
|
def tearDown(self):
|
|
# do nothing since test will delete all data + container
|
|
pass
|
|
|
|
def test_delete_container(self):
|
|
# sanity
|
|
container = self.env.container
|
|
self.assertTrue(
|
|
config_true_value(container.info()['versions_enabled']))
|
|
self.assertTotalVersions(container, 0)
|
|
|
|
# write an object to be versioned
|
|
obj = container.file(Utils.create_name)
|
|
obj.write(b"foo")
|
|
self.assertTotalVersions(container, 1)
|
|
|
|
# delete object and attempt to delete container
|
|
obj.delete()
|
|
self.assertTotalVersions(container, 2)
|
|
|
|
# expect failure because versioning is enabled and
|
|
# old versions still exist
|
|
self.assertFalse(container.delete())
|
|
|
|
# disable it
|
|
container.update_metadata(
|
|
hdrs={self.env.versions_header_key: 'False'})
|
|
self.assertFalse(
|
|
config_true_value(container.info()['versions_enabled']))
|
|
|
|
# expect failure because old versions still exist
|
|
self.assertFalse(container.delete())
|
|
|
|
# delete older versions
|
|
self._tear_down_files(container)
|
|
self.assertTotalVersions(container, 0)
|
|
|
|
# and finally delete container
|
|
self.assertTrue(container.delete())
|
|
|
|
|
|
class TestSloWithVersioning(TestObjectVersioningBase):
|
|
|
|
def setUp(self):
|
|
super(TestSloWithVersioning, self).setUp()
|
|
|
|
if 'slo' not in cluster_info:
|
|
raise SkipTest("SLO not enabled")
|
|
if tf.in_process:
|
|
tf.skip_if_no_xattrs()
|
|
|
|
# create a container with versioning
|
|
self.env.versions_header_key = 'X-Versions-Enabled'
|
|
self.container = self.env.account.container(Utils.create_name())
|
|
container_headers = {self.env.versions_header_key: 'True'}
|
|
if not self.container.create(hdrs=container_headers):
|
|
raise ResponseError(self.conn.response)
|
|
|
|
self.segments_container = self.env.account.container(
|
|
Utils.create_name())
|
|
if not self.segments_container.create():
|
|
raise ResponseError(self.conn.response)
|
|
|
|
# create some segments
|
|
self.seg_info = {}
|
|
for letter, size in (('a', 1024 * 1024),
|
|
('b', 1024 * 1024)):
|
|
seg_name = letter
|
|
file_item = self.segments_container.file(seg_name)
|
|
file_item.write((letter * size).encode('ascii'))
|
|
self.seg_info[seg_name] = {
|
|
'size_bytes': size,
|
|
'etag': file_item.md5,
|
|
'path': '/%s/%s' % (self.segments_container.name, seg_name)}
|
|
|
|
@property
|
|
def account_name(self):
|
|
if not self._account_name:
|
|
self._account_name = self.env.account.conn.storage_path.rsplit(
|
|
'/', 1)[-1]
|
|
return self._account_name
|
|
|
|
def _create_manifest(self, seg_name):
|
|
# create a manifest in the versioning container
|
|
file_item = self.container.file("my-slo-manifest")
|
|
resp = file_item.write(
|
|
json.dumps([self.seg_info[seg_name]]).encode('ascii'),
|
|
parms={'multipart-manifest': 'put'},
|
|
return_resp=True)
|
|
version_id = resp.getheader('x-object-version-id')
|
|
return file_item, version_id
|
|
|
|
def _assert_is_manifest(self, file_item, seg_name, version_id=None):
|
|
if version_id:
|
|
read_params = {'multipart-manifest': 'get',
|
|
'version-id': version_id}
|
|
else:
|
|
read_params = {'multipart-manifest': 'get'}
|
|
manifest_body = file_item.read(parms=read_params)
|
|
resp_headers = {
|
|
h.lower(): v for h, v in file_item.conn.response.getheaders()}
|
|
self.assertIn('x-static-large-object', resp_headers)
|
|
self.assertEqual('application/json; charset=utf-8',
|
|
file_item.content_type)
|
|
try:
|
|
manifest = json.loads(manifest_body)
|
|
except ValueError:
|
|
self.fail("GET with multipart-manifest=get got invalid json")
|
|
|
|
self.assertEqual(1, len(manifest))
|
|
key_map = {'etag': 'hash', 'size_bytes': 'bytes', 'path': 'name'}
|
|
for k_client, k_slo in key_map.items():
|
|
self.assertEqual(self.seg_info[seg_name][k_client],
|
|
manifest[0][k_slo])
|
|
|
|
def _assert_is_object(self, file_item, seg_data, version_id=None):
|
|
if version_id:
|
|
file_contents = file_item.read(parms={'version-id': version_id})
|
|
else:
|
|
file_contents = file_item.read()
|
|
self.assertEqual(1024 * 1024, len(file_contents))
|
|
self.assertEqual(seg_data, file_contents[:1])
|
|
self.assertEqual(seg_data, file_contents[-1:])
|
|
|
|
def tearDown(self):
|
|
self._tear_down_files(self.container)
|
|
|
|
def test_slo_manifest_version(self):
|
|
file_item, v1_version_id = self._create_manifest('a')
|
|
# sanity check: read the manifest, then the large object
|
|
self._assert_is_manifest(file_item, 'a')
|
|
self._assert_is_object(file_item, b'a')
|
|
|
|
# upload new manifest
|
|
file_item, v2_version_id = self._create_manifest('b')
|
|
# sanity check: read the manifest, then the large object
|
|
self._assert_is_manifest(file_item, 'b')
|
|
self._assert_is_object(file_item, b'b')
|
|
|
|
# we wrote two versions
|
|
self.assertTotalVersions(self.container, 2)
|
|
|
|
# check the version 1 is still a manifest
|
|
self._assert_is_manifest(file_item, 'a', v1_version_id)
|
|
self._assert_is_object(file_item, b'a', v1_version_id)
|
|
|
|
# listing looks good
|
|
file_info = file_item.info()
|
|
manifest_info = file_item.info(parms={'multipart-manifest': 'get'})
|
|
obj_list = self.container.files(parms={'format': 'json'})
|
|
for o in obj_list:
|
|
o.pop('last_modified')
|
|
# TODO: add symlink_path back in expected
|
|
o.pop('symlink_path')
|
|
expected = {
|
|
'bytes': file_info['content_length'],
|
|
'content_type': 'application/octet-stream',
|
|
'hash': normalize_etag(manifest_info['etag']),
|
|
'name': 'my-slo-manifest',
|
|
'slo_etag': file_info['etag'],
|
|
'version_symlink': True,
|
|
}
|
|
self.assertEqual([expected], obj_list)
|
|
|
|
# delete the newest manifest
|
|
file_item.delete()
|
|
|
|
# expect to have 3 versions now, last one being a delete-marker
|
|
self.assertTotalVersions(self.container, 3)
|
|
|
|
# restore version 1
|
|
file_item.copy(self.container.name, file_item.name,
|
|
parms={'multipart-manifest': 'get',
|
|
'version-id': v1_version_id})
|
|
self.assertTotalVersions(self.container, 4)
|
|
self._assert_is_manifest(file_item, 'a')
|
|
self._assert_is_object(file_item, b'a')
|
|
|
|
# versioned container listing still looks slo-like
|
|
file_info = file_item.info()
|
|
manifest_info = file_item.info(parms={'multipart-manifest': 'get'})
|
|
obj_list = self.container.files(parms={'format': 'json'})
|
|
for o in obj_list:
|
|
o.pop('last_modified')
|
|
# TODO: add symlink_path back in expected
|
|
o.pop('symlink_path')
|
|
expected = {
|
|
'bytes': file_info['content_length'],
|
|
'content_type': 'application/octet-stream',
|
|
'hash': normalize_etag(manifest_info['etag']),
|
|
'name': 'my-slo-manifest',
|
|
'slo_etag': file_info['etag'],
|
|
'version_symlink': True,
|
|
}
|
|
self.assertEqual([expected], obj_list)
|
|
|
|
status = file_item.conn.make_request(
|
|
'DELETE', file_item.path,
|
|
hdrs={'Accept': 'application/json'},
|
|
parms={'multipart-manifest': 'delete',
|
|
'version-id': v1_version_id})
|
|
body = file_item.conn.response.read()
|
|
self.assertEqual(status, 200, body)
|
|
resp = json.loads(body)
|
|
self.assertEqual(resp['Response Status'], '200 OK')
|
|
self.assertEqual(resp['Errors'], [])
|
|
self.assertEqual(resp['Number Deleted'], 2)
|
|
|
|
self.assertTotalVersions(self.container, 3)
|
|
# Since we included the ?multipart-manifest=delete, segments
|
|
# got cleaned up and now the current version is busted
|
|
with self.assertRaises(ResponseError) as caught:
|
|
file_item.read()
|
|
self.assertEqual(409, caught.exception.status)
|
|
|
|
def test_links_to_slo(self):
|
|
file_item, v1_version_id = self._create_manifest('a')
|
|
slo_info = file_item.info()
|
|
|
|
symlink_name = Utils.create_name()
|
|
sym_tgt_header = quote(unquote('%s/%s' % (
|
|
self.container.name, file_item.name)))
|
|
symlink = self.container.file(symlink_name)
|
|
|
|
# symlink to the slo
|
|
sym_headers = {'X-Symlink-Target': sym_tgt_header}
|
|
symlink.write(b'', hdrs=sym_headers)
|
|
self.assertEqual(slo_info, symlink.info())
|
|
|
|
# hardlink to the slo
|
|
sym_headers['X-Symlink-Target-Etag'] = slo_info['x_manifest_etag']
|
|
symlink.write(b'', hdrs=sym_headers)
|
|
self.assertEqual(slo_info, symlink.info())
|
|
|
|
|
|
class TestVersionsLocationWithVersioning(TestObjectVersioningBase):
|
|
|
|
# create a container with versioned writes
|
|
location_header_key = 'X-Versions-Location'
|
|
|
|
def setUp(self):
|
|
super(TestVersionsLocationWithVersioning, self).setUp()
|
|
|
|
if six.PY2:
|
|
# avoid getting a prefix that stops halfway through an encoded
|
|
# character
|
|
prefix = Utils.create_name().decode("utf-8")[:10].encode("utf-8")
|
|
else:
|
|
prefix = Utils.create_name()[:10]
|
|
|
|
self.versions_container = self.env.account.container(
|
|
prefix + "-versions")
|
|
if not self.versions_container.create():
|
|
raise ResponseError(self.conn.response)
|
|
|
|
self.container = self.env.account.container(prefix + "-objs")
|
|
container_headers = {
|
|
self.location_header_key: quote(self.versions_container.name)}
|
|
if not self.container.create(hdrs=container_headers):
|
|
raise ResponseError(self.conn.response)
|
|
|
|
def _prep_object_versions(self):
|
|
|
|
# object with multiple versions
|
|
object_name = Utils.create_name()
|
|
obj = self.container.file(object_name)
|
|
|
|
# v1
|
|
obj.write(b'version1', hdrs={
|
|
'Content-Type': 'text/jibberish11',
|
|
'ETag': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
})
|
|
|
|
# v2
|
|
obj.write(b'version2', hdrs={
|
|
'Content-Type': 'text/jibberish12',
|
|
'ETag': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
})
|
|
|
|
# v3
|
|
obj.write(b'version3', hdrs={
|
|
'Content-Type': 'text/jibberish13',
|
|
'ETag': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
})
|
|
|
|
return obj
|
|
|
|
def test_list_with_versions_param(self):
|
|
obj = self._prep_object_versions()
|
|
obj_name = obj.name
|
|
|
|
listing_parms = {'format': 'json', 'versions': None}
|
|
current_versions = self.container.files(parms=listing_parms)
|
|
self.assertEqual(1, len(current_versions))
|
|
for pv in current_versions:
|
|
pv.pop('last_modified')
|
|
self.assertEqual(current_versions, [{
|
|
'name': obj_name,
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish13',
|
|
'hash': md5(b'version3', usedforsecurity=False).hexdigest(),
|
|
'is_latest': True,
|
|
'version_id': 'null'
|
|
}])
|
|
|
|
prev_versions = self.versions_container.files(parms=listing_parms)
|
|
self.assertEqual(2, len(prev_versions))
|
|
|
|
for pv in prev_versions:
|
|
pv.pop('last_modified')
|
|
name = pv.pop('name')
|
|
self.assertTrue(name.startswith('%03x%s/' % (len(obj_name),
|
|
obj_name)))
|
|
|
|
self.assertEqual(prev_versions, [{
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish11',
|
|
'hash': md5(b'version1', usedforsecurity=False).hexdigest(),
|
|
'is_latest': True,
|
|
'version_id': 'null',
|
|
}, {
|
|
'bytes': 8,
|
|
'content_type': 'text/jibberish12',
|
|
'hash': md5(b'version2', usedforsecurity=False).hexdigest(),
|
|
'is_latest': True,
|
|
'version_id': 'null'
|
|
}])
|
|
|
|
def test_delete_with_null_version_id(self):
|
|
obj = self._prep_object_versions()
|
|
|
|
# sanity
|
|
self.assertEqual(b'version3', obj.read())
|
|
|
|
obj.delete(parms={'version-id': 'null'})
|
|
if self.location_header_key == 'X-Versions-Location':
|
|
self.assertEqual(b'version2', obj.read())
|
|
else:
|
|
with self.assertRaises(ResponseError) as caught:
|
|
obj.read()
|
|
self.assertEqual(404, caught.exception.status)
|
|
|
|
|
|
class TestHistoryLocationWithVersioning(TestVersionsLocationWithVersioning):
|
|
|
|
# create a container with versioned writes
|
|
location_header_key = 'X-History-Location'
|
|
|
|
|
|
class TestVersioningAccountTempurl(TestObjectVersioningBase):
|
|
env = TestTempurlEnv
|
|
digest_name = 'sha256'
|
|
|
|
def setUp(self):
|
|
self.env.versions_header_key = 'X-Versions-Enabled'
|
|
super(TestVersioningAccountTempurl, self).setUp()
|
|
if self.env.tempurl_enabled is False:
|
|
raise SkipTest("TempURL not enabled")
|
|
elif self.env.tempurl_enabled is not True:
|
|
# just some sanity checking
|
|
raise Exception(
|
|
"Expected tempurl_enabled to be True/False, got %r" %
|
|
(self.env.tempurl_enabled,))
|
|
|
|
if self.digest_name not in cluster_info['tempurl'].get(
|
|
'allowed_digests', ['sha1']):
|
|
raise SkipTest("tempurl does not support %s signatures" %
|
|
self.digest_name)
|
|
|
|
self.digest = getattr(hashlib, self.digest_name)
|
|
self.expires = int(time.time()) + 86400
|
|
self.obj_tempurl_parms = self.tempurl_parms(
|
|
'GET', self.expires, self.env.conn.make_path(self.env.obj.path),
|
|
self.env.tempurl_key)
|
|
|
|
def tempurl_parms(self, method, expires, path, key):
|
|
path = unquote(path)
|
|
if not six.PY2:
|
|
method = method.encode('utf8')
|
|
path = path.encode('utf8')
|
|
key = key.encode('utf8')
|
|
sig = hmac.new(
|
|
key,
|
|
b'%s\n%d\n%s' % (method, expires, path),
|
|
self.digest).hexdigest()
|
|
return {'temp_url_sig': sig, 'temp_url_expires': str(expires)}
|
|
|
|
def test_PUT(self):
|
|
obj = self.env.obj
|
|
|
|
# give out a signature which allows a PUT to obj
|
|
expires = int(time.time()) + 86400
|
|
put_parms = self.tempurl_parms(
|
|
'PUT', expires, self.env.conn.make_path(obj.path),
|
|
self.env.tempurl_key)
|
|
|
|
# try to overwrite existing object
|
|
resp = obj.write(b"version2", parms=put_parms,
|
|
cfg={'no_auth_token': True},
|
|
return_resp=True)
|
|
resp_headers = {
|
|
h.lower(): v for h, v in resp.getheaders()}
|
|
self.assertIn('x-object-version-id', resp_headers)
|
|
|
|
def test_GET_latest(self):
|
|
obj = self.env.obj
|
|
|
|
expires = int(time.time()) + 86400
|
|
get_parms = self.tempurl_parms(
|
|
'GET', expires, self.env.conn.make_path(obj.path),
|
|
self.env.tempurl_key)
|
|
|
|
# get v1 object (., version-id=null, no symlinks involved)
|
|
contents = obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
|
self.assert_status([200])
|
|
self.assertEqual(contents, b"obj contents")
|
|
|
|
# give out a signature which allows a PUT to obj
|
|
expires = int(time.time()) + 86400
|
|
put_parms = self.tempurl_parms(
|
|
'PUT', expires, self.env.conn.make_path(obj.path),
|
|
self.env.tempurl_key)
|
|
|
|
# try to overwrite existing object
|
|
resp = obj.write(b"version2", parms=put_parms,
|
|
cfg={'no_auth_token': True},
|
|
return_resp=True)
|
|
resp_headers = {
|
|
h.lower(): v for h, v in resp.getheaders()}
|
|
self.assertIn('x-object-version-id', resp_headers)
|
|
|
|
# get v2 object
|
|
contents = obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
|
self.assert_status([200])
|
|
self.assertEqual(contents, b"version2")
|
|
|
|
def test_GET_version_id(self):
|
|
# N.B.: The test is not intended to imply the desired behavior
|
|
# of a tempurl GET with version-id. Currently version-id is simply
|
|
# ignored as the latest version is always returned. In the future,
|
|
# users should be able to create a tempurl with version-id as a
|
|
# parameter.
|
|
|
|
# overwrite object a couple more times
|
|
obj = self.env.obj
|
|
resp = obj.write(b"version2", return_resp=True)
|
|
v2_version_id = resp.getheader('x-object-version-id')
|
|
obj.write(b"version3!!!")
|
|
|
|
expires = int(time.time()) + 86400
|
|
get_parms = self.tempurl_parms(
|
|
'GET', expires, self.env.conn.make_path(obj.path),
|
|
self.env.tempurl_key)
|
|
get_parms['version-id'] = v2_version_id
|
|
|
|
contents = obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
|
self.assert_status([200])
|
|
self.assertEqual(contents, b"version3!!!")
|
|
|
|
|
|
class TestVersioningContainerTempurl(TestObjectVersioningBase):
|
|
env = TestContainerTempurlEnv
|
|
digest_name = 'sha256'
|
|
|
|
def setUp(self):
|
|
self.env.versions_header_key = 'X-Versions-Enabled'
|
|
super(TestVersioningContainerTempurl, self).setUp()
|
|
if self.env.tempurl_enabled is False:
|
|
raise SkipTest("TempURL not enabled")
|
|
elif self.env.tempurl_enabled is not True:
|
|
# just some sanity checking
|
|
raise Exception(
|
|
"Expected tempurl_enabled to be True/False, got %r" %
|
|
(self.env.tempurl_enabled,))
|
|
|
|
if self.digest_name not in cluster_info['tempurl'].get(
|
|
'allowed_digests', ['sha1']):
|
|
raise SkipTest("tempurl does not support %s signatures" %
|
|
self.digest_name)
|
|
|
|
self.digest = getattr(hashlib, self.digest_name)
|
|
expires = int(time.time()) + 86400
|
|
sig = self.tempurl_sig(
|
|
'GET', expires, self.env.conn.make_path(self.env.obj.path),
|
|
self.env.tempurl_key)
|
|
self.obj_tempurl_parms = {'temp_url_sig': sig,
|
|
'temp_url_expires': str(expires)}
|
|
|
|
def tempurl_sig(self, method, expires, path, key):
|
|
path = unquote(path)
|
|
if not six.PY2:
|
|
method = method.encode('utf8')
|
|
path = path.encode('utf8')
|
|
key = key.encode('utf8')
|
|
return hmac.new(
|
|
key,
|
|
b'%s\n%d\n%s' % (method, expires, path),
|
|
self.digest).hexdigest()
|
|
|
|
def test_PUT(self):
|
|
obj = self.env.obj
|
|
|
|
# give out a signature which allows a PUT to new_obj
|
|
expires = int(time.time()) + 86400
|
|
sig = self.tempurl_sig(
|
|
'PUT', expires, self.env.conn.make_path(obj.path),
|
|
self.env.tempurl_key)
|
|
put_parms = {'temp_url_sig': sig,
|
|
'temp_url_expires': str(expires)}
|
|
|
|
# try to overwrite existing object
|
|
resp = obj.write(b"version2", parms=put_parms,
|
|
cfg={'no_auth_token': True},
|
|
return_resp=True)
|
|
resp_headers = {
|
|
h.lower(): v for h, v in resp.getheaders()}
|
|
self.assertIn('x-object-version-id', resp_headers)
|
|
|
|
def test_GET_latest(self):
|
|
obj = self.env.obj
|
|
|
|
expires = int(time.time()) + 86400
|
|
sig = self.tempurl_sig(
|
|
'GET', expires, self.env.conn.make_path(obj.path),
|
|
self.env.tempurl_key)
|
|
get_parms = {'temp_url_sig': sig,
|
|
'temp_url_expires': str(expires)}
|
|
|
|
# get v1 object (., version-id=null, no symlinks involved)
|
|
contents = obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
|
self.assert_status([200])
|
|
self.assertEqual(contents, b"obj contents")
|
|
|
|
# overwrite existing object
|
|
obj.write(b"version2")
|
|
|
|
# get v2 object (reading from versions container)
|
|
# versioning symlink allows us to bypass the normal
|
|
# container-tempurl-key scoping
|
|
contents = obj.read(parms=get_parms, cfg={'no_auth_token': True})
|
|
self.assert_status([200])
|
|
self.assertEqual(contents, b"version2")
|
|
# HEAD works, too
|
|
obj.info(parms=get_parms, cfg={'no_auth_token': True})
|
|
self.assert_status([200])
|