Make metadata handling consistent in Object Store
Make metadata handling consistent across all Object Store resources. The consistent methods are get_*_metdata, set_*_metadata, and delete_*_metadata which is similar to the __get__, __set__, and __delete__ methods of a descriptor or the __getitem__, __setitem__, and __delitem__ of a MutableMapping so it should be fairly natural for Python users. Change-Id: Ie6a892b48ac63ef6a6832b66f9e65e1b5d8a6d76 Closes-Bug: #1487150
This commit is contained in:
parent
3265aa3e8d
commit
4ae0638048
@ -18,6 +18,52 @@ from openstack import resource
|
|||||||
class BaseResource(resource.Resource):
|
class BaseResource(resource.Resource):
|
||||||
service = object_store_service.ObjectStoreService()
|
service = object_store_service.ObjectStoreService()
|
||||||
|
|
||||||
|
#: Metadata stored for this resource. *Type: dict*
|
||||||
|
metadata = dict()
|
||||||
|
|
||||||
|
_custom_metadata_prefix = None
|
||||||
|
_system_metadata = dict()
|
||||||
|
|
||||||
|
def _calculate_headers(self, metadata):
|
||||||
|
headers = dict()
|
||||||
|
for key in metadata:
|
||||||
|
if key in self._system_metadata:
|
||||||
|
header = self._system_metadata[key]
|
||||||
|
else:
|
||||||
|
header = self._custom_metadata_prefix + key
|
||||||
|
headers[header] = metadata[key]
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def set_metadata(self, session, metadata):
|
||||||
|
url = self._get_url(self, self.id)
|
||||||
|
session.post(url, endpoint_filter=self.service,
|
||||||
|
headers=self._calculate_headers(metadata))
|
||||||
|
|
||||||
|
def delete_metadata(self, session, keys):
|
||||||
|
url = self._get_url(self, self.id)
|
||||||
|
headers = {key: '' for key in keys}
|
||||||
|
session.post(url, endpoint_filter=self.service,
|
||||||
|
headers=self._calculate_headers(headers))
|
||||||
|
|
||||||
|
def _set_metadata(self):
|
||||||
|
self.metadata = dict()
|
||||||
|
headers = self.get_headers()
|
||||||
|
|
||||||
|
for header in headers:
|
||||||
|
if header.startswith(self._custom_metadata_prefix):
|
||||||
|
key = header[len(self._custom_metadata_prefix):].lower()
|
||||||
|
self.metadata[key] = headers[header]
|
||||||
|
|
||||||
|
def get(self, session, include_headers=False, args=None):
|
||||||
|
super(BaseResource, self).get(session, include_headers, args)
|
||||||
|
self._set_metadata()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def head(self, session):
|
||||||
|
super(BaseResource, self).head(session)
|
||||||
|
self._set_metadata()
|
||||||
|
return self
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_by_id(cls, session, resource_id, attrs, path_args=None):
|
def update_by_id(cls, session, resource_id, attrs, path_args=None):
|
||||||
"""Update a Resource with the given attributes.
|
"""Update a Resource with the given attributes.
|
||||||
|
@ -19,30 +19,36 @@ from openstack import proxy
|
|||||||
class Proxy(proxy.BaseProxy):
|
class Proxy(proxy.BaseProxy):
|
||||||
|
|
||||||
def get_account_metadata(self):
|
def get_account_metadata(self):
|
||||||
"""Get metatdata for this account
|
"""Get metadata for this account.
|
||||||
|
|
||||||
:rtype:
|
:rtype:
|
||||||
:class:`~openstack.object_store.v1.account.Account`
|
:class:`~openstack.object_store.v1.account.Account`
|
||||||
"""
|
"""
|
||||||
return self._head(_account.Account)
|
return self._head(_account.Account)
|
||||||
|
|
||||||
def set_account_metadata(self, account):
|
def set_account_metadata(self, **metadata):
|
||||||
"""Set metatdata for this account.
|
"""Set metadata for this account.
|
||||||
|
|
||||||
:param account: Account metadata specified on a
|
:param kwargs metadata: Key/value pairs to be set as metadata
|
||||||
:class:`~openstack.object_store.v1.account.Account` object
|
on the container. Custom metadata can be set.
|
||||||
to be sent to the server.
|
Custom metadata are keys and values defined
|
||||||
:type account:
|
by the user.
|
||||||
:class:`~openstack.object_store.v1.account.Account`
|
|
||||||
|
|
||||||
:rtype: ``None``
|
|
||||||
"""
|
"""
|
||||||
account.update(self.session)
|
account = self._get_resource(_account.Account, None)
|
||||||
|
account.set_metadata(self.session, metadata)
|
||||||
|
|
||||||
|
def delete_account_metadata(self, keys):
|
||||||
|
"""Delete metadata for this account.
|
||||||
|
|
||||||
|
:param list keys: The keys of metadata to be deleted.
|
||||||
|
"""
|
||||||
|
account = self._get_resource(_account.Account, None)
|
||||||
|
account.delete_metadata(self.session, keys)
|
||||||
|
|
||||||
def containers(self, **query):
|
def containers(self, **query):
|
||||||
"""Obtain Container objects for this account.
|
"""Obtain Container objects for this account.
|
||||||
|
|
||||||
:param kwargs \*\*query: Optional query parameters to be sent to limit
|
:param kwargs query: Optional query parameters to be sent to limit
|
||||||
the resources being returned.
|
the resources being returned.
|
||||||
|
|
||||||
:rtype: A generator of
|
:rtype: A generator of
|
||||||
@ -50,30 +56,6 @@ class Proxy(proxy.BaseProxy):
|
|||||||
"""
|
"""
|
||||||
return _container.Container.list(self.session, **query)
|
return _container.Container.list(self.session, **query)
|
||||||
|
|
||||||
def get_container_metadata(self, container):
|
|
||||||
"""Get metatdata for a container
|
|
||||||
|
|
||||||
:param container: The value can be the name of a container or a
|
|
||||||
:class:`~openstack.object_store.v1.container.Container`
|
|
||||||
instance.
|
|
||||||
|
|
||||||
:returns: One :class:`~openstack.object_store.v1.container.Container`
|
|
||||||
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
|
||||||
when no resource can be found.
|
|
||||||
"""
|
|
||||||
return self._head(_container.Container, container)
|
|
||||||
|
|
||||||
def set_container_metadata(self, container):
|
|
||||||
"""Set metatdata for a container.
|
|
||||||
|
|
||||||
:param container: A container object containing metadata to be set.
|
|
||||||
:type container:
|
|
||||||
:class:`~openstack.object_store.v1.container.Container`
|
|
||||||
|
|
||||||
:rtype: ``None``
|
|
||||||
"""
|
|
||||||
container.create(self.session)
|
|
||||||
|
|
||||||
def create_container(self, **attrs):
|
def create_container(self, **attrs):
|
||||||
"""Create a new container from attributes
|
"""Create a new container from attributes
|
||||||
|
|
||||||
@ -103,6 +85,54 @@ class Proxy(proxy.BaseProxy):
|
|||||||
self._delete(_container.Container, container,
|
self._delete(_container.Container, container,
|
||||||
ignore_missing=ignore_missing)
|
ignore_missing=ignore_missing)
|
||||||
|
|
||||||
|
def get_container_metadata(self, container):
|
||||||
|
"""Get metadata for a container
|
||||||
|
|
||||||
|
:param container: The value can be the name of a container or a
|
||||||
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
|
instance.
|
||||||
|
|
||||||
|
:returns: One :class:`~openstack.object_store.v1.container.Container`
|
||||||
|
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
||||||
|
when no resource can be found.
|
||||||
|
"""
|
||||||
|
return self._head(_container.Container, container)
|
||||||
|
|
||||||
|
def set_container_metadata(self, container, **metadata):
|
||||||
|
"""Set metadata for a container.
|
||||||
|
|
||||||
|
:param container: The value can be the name of a container or a
|
||||||
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
|
instance.
|
||||||
|
:param kwargs metadata: Key/value pairs to be set as metadata
|
||||||
|
on the container. Both custom and system
|
||||||
|
metadata can be set. Custom metadata are keys
|
||||||
|
and values defined by the user. System
|
||||||
|
metadata are keys defined by the Object Store
|
||||||
|
and values defined by the user. The system
|
||||||
|
metadata keys are:
|
||||||
|
- `content_type`
|
||||||
|
- `detect_content_type`
|
||||||
|
- `versions_location`
|
||||||
|
- `read_ACL`
|
||||||
|
- `write_ACL`
|
||||||
|
- `sync_to`
|
||||||
|
- `sync_key`
|
||||||
|
"""
|
||||||
|
res = self._get_resource(_container.Container, container)
|
||||||
|
res.set_metadata(self.session, metadata)
|
||||||
|
|
||||||
|
def delete_container_metadata(self, container, keys):
|
||||||
|
"""Delete metadata for a container.
|
||||||
|
|
||||||
|
:param container: The value can be the ID of a container or a
|
||||||
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
|
instance.
|
||||||
|
:param list keys: The keys of metadata to be deleted.
|
||||||
|
"""
|
||||||
|
res = self._get_resource(_container.Container, container)
|
||||||
|
res.delete_metadata(self.session, keys)
|
||||||
|
|
||||||
def objects(self, container, **query):
|
def objects(self, container, **query):
|
||||||
"""Return a generator that yields the Container's objects.
|
"""Return a generator that yields the Container's objects.
|
||||||
|
|
||||||
@ -121,28 +151,24 @@ class Proxy(proxy.BaseProxy):
|
|||||||
objs = _obj.Object.list(self.session,
|
objs = _obj.Object.list(self.session,
|
||||||
path_args={"container": container.name},
|
path_args={"container": container.name},
|
||||||
**query)
|
**query)
|
||||||
# TODO(briancurtin): Objects have to know their container at this
|
for obj in objs:
|
||||||
# point, otherwise further operations like getting their metadata
|
obj.container = container.name
|
||||||
# or downloading them is a hassle because the end-user would have
|
yield obj
|
||||||
# to maintain both the container and the object separately.
|
|
||||||
for ob in objs:
|
|
||||||
ob.container = container.name
|
|
||||||
yield ob
|
|
||||||
|
|
||||||
def _get_container_name(self, object, container):
|
def _get_container_name(self, obj, container):
|
||||||
if isinstance(object, _obj.Object):
|
if isinstance(obj, _obj.Object):
|
||||||
if object.container is not None:
|
if obj.container is not None:
|
||||||
return object.container
|
return obj.container
|
||||||
if container is not None:
|
if container is not None:
|
||||||
container = _container.Container.from_id(container)
|
container = _container.Container.from_id(container)
|
||||||
return container.name
|
return container.name
|
||||||
|
|
||||||
raise ValueError("container must be specified")
|
raise ValueError("container must be specified")
|
||||||
|
|
||||||
def get_object(self, object, container=None):
|
def get_object(self, obj, container=None):
|
||||||
"""Get the data associated with an object
|
"""Get the data associated with an object
|
||||||
|
|
||||||
:param object: The value can be the name of an object or a
|
:param obj: The value can be the name of an object or a
|
||||||
:class:`~openstack.object_store.v1.obj.Object` instance.
|
:class:`~openstack.object_store.v1.obj.Object` instance.
|
||||||
:param container: The value can be the name of a container or a
|
:param container: The value can be the name of a container or a
|
||||||
:class:`~openstack.object_store.v1.container.Container`
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
@ -154,15 +180,15 @@ class Proxy(proxy.BaseProxy):
|
|||||||
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
||||||
when no resource can be found.
|
when no resource can be found.
|
||||||
"""
|
"""
|
||||||
container_name = self._get_container_name(object, container)
|
container_name = self._get_container_name(obj, container)
|
||||||
|
|
||||||
return self._get(_obj.Object, object,
|
return self._get(_obj.Object, obj,
|
||||||
path_args={"container": container_name})
|
path_args={"container": container_name})
|
||||||
|
|
||||||
def download_object(self, object, container=None, path=None):
|
def download_object(self, obj, container=None, path=None):
|
||||||
"""Download the data contained inside an object to disk.
|
"""Download the data contained inside an object to disk.
|
||||||
|
|
||||||
:param object: The value can be the name of an object or a
|
:param obj: The value can be the name of an object or a
|
||||||
:class:`~openstack.object_store.v1.obj.Object` instance.
|
:class:`~openstack.object_store.v1.obj.Object` instance.
|
||||||
:param container: The value can be the name of a container or a
|
:param container: The value can be the name of a container or a
|
||||||
:class:`~openstack.object_store.v1.container.Container`
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
@ -173,7 +199,7 @@ class Proxy(proxy.BaseProxy):
|
|||||||
when no resource can be found.
|
when no resource can be found.
|
||||||
"""
|
"""
|
||||||
with open(path, "w") as out:
|
with open(path, "w") as out:
|
||||||
out.write(self.get_object(object, container))
|
out.write(self.get_object(obj, container))
|
||||||
|
|
||||||
def upload_object(self, **attrs):
|
def upload_object(self, **attrs):
|
||||||
"""Upload a new object from attributes
|
"""Upload a new object from attributes
|
||||||
@ -199,10 +225,10 @@ class Proxy(proxy.BaseProxy):
|
|||||||
"""Copy an object."""
|
"""Copy an object."""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def delete_object(self, object, ignore_missing=True, container=None):
|
def delete_object(self, obj, ignore_missing=True, container=None):
|
||||||
"""Delete an object
|
"""Delete an object
|
||||||
|
|
||||||
:param object: The value can be either the name of an object or a
|
:param obj: The value can be either the name of an object or a
|
||||||
:class:`~openstack.object_store.v1.container.Container`
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
instance.
|
instance.
|
||||||
:param container: The value can be the ID of a container or a
|
:param container: The value can be the ID of a container or a
|
||||||
@ -216,17 +242,16 @@ class Proxy(proxy.BaseProxy):
|
|||||||
|
|
||||||
:returns: ``None``
|
:returns: ``None``
|
||||||
"""
|
"""
|
||||||
container_name = self._get_container_name(object, container)
|
container_name = self._get_container_name(obj, container)
|
||||||
|
|
||||||
self._delete(_obj.Object, object, ignore_missing=ignore_missing,
|
self._delete(_obj.Object, obj, ignore_missing=ignore_missing,
|
||||||
path_args={"container": container_name})
|
path_args={"container": container_name})
|
||||||
|
|
||||||
def get_object_metadata(self, object, container=None):
|
def get_object_metadata(self, obj, container=None):
|
||||||
"""Get metatdata for an object
|
"""Get metadata for an object.
|
||||||
|
|
||||||
:param object: The value is an
|
:param obj: The value can be the name of an object or a
|
||||||
:class:`~openstack.object_store.v1.obj.Object`
|
:class:`~openstack.object_store.v1.obj.Object` instance.
|
||||||
instance.
|
|
||||||
:param container: The value can be the ID of a container or a
|
:param container: The value can be the ID of a container or a
|
||||||
:class:`~openstack.object_store.v1.container.Container`
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
instance.
|
instance.
|
||||||
@ -235,17 +260,51 @@ class Proxy(proxy.BaseProxy):
|
|||||||
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
:raises: :class:`~openstack.exceptions.ResourceNotFound`
|
||||||
when no resource can be found.
|
when no resource can be found.
|
||||||
"""
|
"""
|
||||||
container_name = self._get_container_name(object, container)
|
container_name = self._get_container_name(obj, container)
|
||||||
|
|
||||||
return self._head(_obj.Object, object,
|
return self._head(_obj.Object, obj,
|
||||||
path_args={"container": container_name})
|
path_args={"container": container_name})
|
||||||
|
|
||||||
def set_object_metadata(self, object):
|
def set_object_metadata(self, obj, container=None, **metadata):
|
||||||
"""Set metatdata for an object.
|
"""Set metadata for an object.
|
||||||
|
|
||||||
:param object: The object to set metadata for.
|
Note: This method will do an extra HEAD call.
|
||||||
:type object: :class:`~openstack.object_store.v1.obj.Object`
|
|
||||||
|
|
||||||
:rtype: ``None``
|
:param obj: The value can be the name of an object or a
|
||||||
|
:class:`~openstack.object_store.v1.obj.Object` instance.
|
||||||
|
:param container: The value can be the name of a container or a
|
||||||
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
|
instance.
|
||||||
|
:param kwargs metadata: Key/value pairs to be set as metadata
|
||||||
|
on the container. Both custom and system
|
||||||
|
metadata can be set. Custom metadata are keys
|
||||||
|
and values defined by the user. System
|
||||||
|
metadata are keys defined by the Object Store
|
||||||
|
and values defined by the user. The system
|
||||||
|
metadata keys are:
|
||||||
|
- `content_type`
|
||||||
|
- `content_encoding`
|
||||||
|
- `content_disposition`
|
||||||
|
- `detect_content_type`
|
||||||
|
- `delete_after`
|
||||||
|
- `delete_at`
|
||||||
"""
|
"""
|
||||||
object.create(self.session)
|
container_name = self._get_container_name(obj, container)
|
||||||
|
res = self._get_resource(_obj.Object, obj,
|
||||||
|
path_args={"container": container_name})
|
||||||
|
res.set_metadata(self.session, metadata)
|
||||||
|
|
||||||
|
def delete_object_metadata(self, obj, container=None, keys=None):
|
||||||
|
"""Delete metadata for an object.
|
||||||
|
|
||||||
|
:param obj: The value can be the name of an object or a
|
||||||
|
:class:`~openstack.object_store.v1.obj.Object` instance.
|
||||||
|
:param container: The value can be the ID of a container or a
|
||||||
|
:class:`~openstack.object_store.v1.container.Container`
|
||||||
|
instance.
|
||||||
|
:param list keys: The keys of metadata to be deleted.
|
||||||
|
"""
|
||||||
|
container_name = self._get_container_name(obj, container)
|
||||||
|
res = self._get_resource(_obj.Object, obj,
|
||||||
|
path_args={"container": container_name})
|
||||||
|
res.delete_metadata(self.session, keys)
|
||||||
|
@ -17,6 +17,8 @@ from openstack import resource
|
|||||||
|
|
||||||
|
|
||||||
class Account(_base.BaseResource):
|
class Account(_base.BaseResource):
|
||||||
|
_custom_metadata_prefix = "X-Account-Meta-"
|
||||||
|
|
||||||
base_path = "/"
|
base_path = "/"
|
||||||
|
|
||||||
allow_retrieve = True
|
allow_retrieve = True
|
||||||
|
@ -17,6 +17,17 @@ from openstack import resource
|
|||||||
|
|
||||||
|
|
||||||
class Container(_base.BaseResource):
|
class Container(_base.BaseResource):
|
||||||
|
_custom_metadata_prefix = "X-Container-Meta-"
|
||||||
|
_system_metadata = {
|
||||||
|
"content_type": "content-type",
|
||||||
|
"detect_content_type": "x-detect-content-type",
|
||||||
|
"versions_location": "x-versions-location",
|
||||||
|
"read_ACL": "x-container-read",
|
||||||
|
"write_ACL": "x-container-write",
|
||||||
|
"sync_to": "x-container-sync-to",
|
||||||
|
"sync_key": "x-container-sync-key"
|
||||||
|
}
|
||||||
|
|
||||||
base_path = "/"
|
base_path = "/"
|
||||||
id_attribute = "name"
|
id_attribute = "name"
|
||||||
|
|
||||||
@ -70,9 +81,7 @@ class Container(_base.BaseResource):
|
|||||||
#: the name before you include it in the header. To disable
|
#: the name before you include it in the header. To disable
|
||||||
#: versioning, set the header to an empty string.
|
#: versioning, set the header to an empty string.
|
||||||
versions_location = resource.header("x-versions-location")
|
versions_location = resource.header("x-versions-location")
|
||||||
#: Set to any value to disable versioning.
|
#: The MIME type of the list of names.
|
||||||
remove_versions_location = resource.header("x-remove-versions-location")
|
|
||||||
#: Changes the MIME type for the object.
|
|
||||||
content_type = resource.header("content-type")
|
content_type = resource.header("content-type")
|
||||||
#: If set to true, Object Storage guesses the content type based
|
#: If set to true, Object Storage guesses the content type based
|
||||||
#: on the file extension and ignores the value sent in the
|
#: on the file extension and ignores the value sent in the
|
||||||
|
@ -11,12 +11,25 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
from openstack import format
|
from openstack import format
|
||||||
from openstack.object_store import object_store_service
|
from openstack.object_store import object_store_service
|
||||||
|
from openstack.object_store.v1 import _base
|
||||||
from openstack import resource
|
from openstack import resource
|
||||||
|
|
||||||
|
|
||||||
class Object(resource.Resource):
|
class Object(_base.BaseResource):
|
||||||
|
_custom_metadata_prefix = "X-Object-Meta-"
|
||||||
|
_system_metadata = {
|
||||||
|
"content_disposition": "content-disposition",
|
||||||
|
"content_encoding": "content-encoding",
|
||||||
|
"content_type": "content-type",
|
||||||
|
"detect_content_type": "x-detect-content-type",
|
||||||
|
"delete_after": "x-delete-after",
|
||||||
|
"delete_at": "x-delete-at"
|
||||||
|
}
|
||||||
|
|
||||||
base_path = "/%(container)s"
|
base_path = "/%(container)s"
|
||||||
service = object_store_service.ObjectStoreService()
|
service = object_store_service.ObjectStoreService()
|
||||||
id_attribute = "name"
|
id_attribute = "name"
|
||||||
@ -87,9 +100,6 @@ class Object(resource.Resource):
|
|||||||
content_type = resource.header("content_type", alias="content-type")
|
content_type = resource.header("content_type", alias="content-type")
|
||||||
#: The type of ranges that the object accepts.
|
#: The type of ranges that the object accepts.
|
||||||
accept_ranges = resource.header("accept-ranges")
|
accept_ranges = resource.header("accept-ranges")
|
||||||
#: The date and time that the object was created or the last
|
|
||||||
#: time that the metadata was changed.
|
|
||||||
last_modified = resource.header("last_modified", alias="last-modified")
|
|
||||||
#: For objects smaller than 5 GB, this value is the MD5 checksum
|
#: For objects smaller than 5 GB, this value is the MD5 checksum
|
||||||
#: of the object content. The value is not quoted.
|
#: of the object content. The value is not quoted.
|
||||||
#: For manifest objects, this value is the MD5 checksum of the
|
#: For manifest objects, this value is the MD5 checksum of the
|
||||||
@ -115,6 +125,10 @@ class Object(resource.Resource):
|
|||||||
#: which is the default.
|
#: which is the default.
|
||||||
#: If not set, this header is not returned by this operation.
|
#: If not set, this header is not returned by this operation.
|
||||||
content_disposition = resource.header("content-disposition")
|
content_disposition = resource.header("content-disposition")
|
||||||
|
#: Specifies the number of seconds after which the object is
|
||||||
|
#: removed. Internally, the Object Storage system stores this
|
||||||
|
#: value in the X-Delete-At metadata item.
|
||||||
|
delete_after = resource.header("x-delete-after", type=int)
|
||||||
#: If set, the time when the object will be deleted by the system
|
#: If set, the time when the object will be deleted by the system
|
||||||
#: in the format of a UNIX Epoch timestamp.
|
#: in the format of a UNIX Epoch timestamp.
|
||||||
#: If not set, this header is not returned by this operation.
|
#: If not set, this header is not returned by this operation.
|
||||||
@ -125,6 +139,9 @@ class Object(resource.Resource):
|
|||||||
object_manifest = resource.header("x-object-manifest")
|
object_manifest = resource.header("x-object-manifest")
|
||||||
#: The timestamp of the transaction.
|
#: The timestamp of the transaction.
|
||||||
timestamp = resource.header("x-timestamp", type=format.UNIXEpoch)
|
timestamp = resource.header("x-timestamp", type=format.UNIXEpoch)
|
||||||
|
#: The date and time that the object was created or the last
|
||||||
|
#: time that the metadata was changed.
|
||||||
|
last_modified = resource.header("last_modified", alias="last-modified")
|
||||||
|
|
||||||
# Headers for PUT and POST requests
|
# Headers for PUT and POST requests
|
||||||
#: Set to chunked to enable chunked transfer encoding. If used,
|
#: Set to chunked to enable chunked transfer encoding. If used,
|
||||||
@ -142,21 +159,65 @@ class Object(resource.Resource):
|
|||||||
#: Using PUT with X-Copy-From has the same effect as using the
|
#: Using PUT with X-Copy-From has the same effect as using the
|
||||||
#: COPY operation to copy an object.
|
#: COPY operation to copy an object.
|
||||||
copy_from = resource.header("x-copy-from")
|
copy_from = resource.header("x-copy-from")
|
||||||
#: Specifies the number of seconds after which the object is
|
|
||||||
#: removed. Internally, the Object Storage system stores this
|
|
||||||
#: value in the X-Delete-At metadata item.
|
|
||||||
delete_after = resource.header("x-delete-after", type=int)
|
|
||||||
|
|
||||||
def get(self, session, args=None):
|
# The Object Store treats the metadata for its resources inconsistently so
|
||||||
|
# Object.set_metadata must override the BaseResource.set_metadata to
|
||||||
|
# account for it.
|
||||||
|
def set_metadata(self, session, metadata):
|
||||||
|
# Filter out items with empty values so the create metadata behaviour
|
||||||
|
# is the same as account and container
|
||||||
|
filtered_metadata = \
|
||||||
|
{key: value for key, value in metadata.iteritems() if value}
|
||||||
|
|
||||||
|
# Get a copy of the original metadata so it doesn't get erased on POST
|
||||||
|
# and update it with the new metadata values.
|
||||||
|
obj = self.head(session)
|
||||||
|
metadata2 = copy.deepcopy(obj.metadata)
|
||||||
|
metadata2.update(filtered_metadata)
|
||||||
|
|
||||||
|
# Include any original system metadata so it doesn't get erased on POST
|
||||||
|
for key in self._system_metadata:
|
||||||
|
value = getattr(obj, key)
|
||||||
|
if value and key not in metadata2:
|
||||||
|
metadata2[key] = value
|
||||||
|
|
||||||
|
super(Object, self).set_metadata(session, metadata2)
|
||||||
|
|
||||||
|
# The Object Store treats the metadata for its resources inconsistently so
|
||||||
|
# Object.delete_metadata must override the BaseResource.delete_metadata to
|
||||||
|
# account for it.
|
||||||
|
def delete_metadata(self, session, keys):
|
||||||
|
# Get a copy of the original metadata so it doesn't get erased on POST
|
||||||
|
# and update it with the new metadata values.
|
||||||
|
obj = self.head(session)
|
||||||
|
metadata = copy.deepcopy(obj.metadata)
|
||||||
|
|
||||||
|
# Include any original system metadata so it doesn't get erased on POST
|
||||||
|
for key in self._system_metadata:
|
||||||
|
value = getattr(obj, key)
|
||||||
|
if value:
|
||||||
|
metadata[key] = value
|
||||||
|
|
||||||
|
# Remove the metadata
|
||||||
|
for key in keys:
|
||||||
|
if key == 'delete_after':
|
||||||
|
del(metadata['delete_at'])
|
||||||
|
else:
|
||||||
|
del(metadata[key])
|
||||||
|
|
||||||
|
url = self._get_url(self, self.id)
|
||||||
|
session.post(url, endpoint_filter=self.service,
|
||||||
|
headers=self._calculate_headers(metadata))
|
||||||
|
|
||||||
|
def get(self, session, include_headers=False, args=None):
|
||||||
url = self._get_url(self, self.id)
|
url = self._get_url(self, self.id)
|
||||||
# TODO(thowe): Add filter header support bug #1488269
|
|
||||||
headers = {'Accept': 'bytes'}
|
headers = {'Accept': 'bytes'}
|
||||||
resp = session.get(url, endpoint_filter=self.service, headers=headers)
|
resp = session.get(url, endpoint_filter=self.service, headers=headers)
|
||||||
resp = resp.content
|
resp = resp.content
|
||||||
|
self._set_metadata()
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
def create(self, session):
|
def create(self, session):
|
||||||
"""Create a remote resource from this instance."""
|
|
||||||
url = self._get_url(self, self.id)
|
url = self._get_url(self, self.id)
|
||||||
|
|
||||||
headers = self.get_headers()
|
headers = self.get_headers()
|
||||||
|
@ -75,13 +75,18 @@ class TestServer(base.BaseFunctionalTest):
|
|||||||
self.assertDictEqual(self.conn.compute.replace_server_metadata(sot),
|
self.assertDictEqual(self.conn.compute.replace_server_metadata(sot),
|
||||||
{})
|
{})
|
||||||
|
|
||||||
# Insert first and last name metadata
|
# Create first and last name metadata
|
||||||
meta = {"first": "Matthew", "last": "Dellavedova"}
|
meta = {"first": "Matthew", "last": "Dellavedova"}
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
self.conn.compute.create_server_metadata(sot, **meta), meta)
|
self.conn.compute.create_server_metadata(sot, **meta), meta)
|
||||||
|
|
||||||
|
# Create something that already exists
|
||||||
|
meta = {"last": "Inman"}
|
||||||
|
self.assertDictEqual(
|
||||||
|
self.conn.compute.create_server_metadata(sot, **meta), meta)
|
||||||
|
|
||||||
# Update only the first name
|
# Update only the first name
|
||||||
short = {"first": "Matt", "last": "Dellavedova"}
|
short = {"first": "Matt", "last": "Inman"}
|
||||||
self.assertDictEqual(
|
self.assertDictEqual(
|
||||||
self.conn.compute.update_server_metadata(sot,
|
self.conn.compute.update_server_metadata(sot,
|
||||||
first=short["first"]),
|
first=short["first"]),
|
||||||
|
79
openstack/tests/functional/object_store/v1/test_account.py
Normal file
79
openstack/tests/functional/object_store/v1/test_account.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from openstack.tests.functional import base
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccount(base.BaseFunctionalTest):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def tearDownClass(cls):
|
||||||
|
super(TestAccount, cls).tearDownClass()
|
||||||
|
account = cls.conn.object_store.get_account_metadata()
|
||||||
|
cls.conn.object_store.delete_account_metadata(account.metadata.keys())
|
||||||
|
|
||||||
|
def test_system_metadata(self):
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertGreaterEqual(account.account_bytes_used, 0)
|
||||||
|
self.assertGreaterEqual(account.account_container_count, 0)
|
||||||
|
self.assertGreaterEqual(account.account_object_count, 0)
|
||||||
|
|
||||||
|
def test_custom_metadata(self):
|
||||||
|
# get custom metadata
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertFalse(account.metadata)
|
||||||
|
|
||||||
|
# set no custom metadata
|
||||||
|
self.conn.object_store.set_account_metadata()
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertFalse(account.metadata)
|
||||||
|
|
||||||
|
# set empty custom metadata
|
||||||
|
self.conn.object_store.set_account_metadata(k0='')
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertFalse(account.metadata)
|
||||||
|
|
||||||
|
# set custom metadata
|
||||||
|
self.conn.object_store.set_account_metadata(k1='v1')
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertTrue(account.metadata)
|
||||||
|
self.assertEqual(1, len(account.metadata))
|
||||||
|
self.assertIn('k1', account.metadata)
|
||||||
|
self.assertEqual('v1', account.metadata['k1'])
|
||||||
|
|
||||||
|
# set more custom metadata
|
||||||
|
self.conn.object_store.set_account_metadata(k2='v2')
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertTrue(account.metadata)
|
||||||
|
self.assertEqual(2, len(account.metadata))
|
||||||
|
self.assertIn('k1', account.metadata)
|
||||||
|
self.assertEqual('v1', account.metadata['k1'])
|
||||||
|
self.assertIn('k2', account.metadata)
|
||||||
|
self.assertEqual('v2', account.metadata['k2'])
|
||||||
|
|
||||||
|
# update custom metadata
|
||||||
|
self.conn.object_store.set_account_metadata(k1='v1.1')
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertTrue(account.metadata)
|
||||||
|
self.assertEqual(2, len(account.metadata))
|
||||||
|
self.assertIn('k1', account.metadata)
|
||||||
|
self.assertEqual('v1.1', account.metadata['k1'])
|
||||||
|
self.assertIn('k2', account.metadata)
|
||||||
|
self.assertEqual('v2', account.metadata['k2'])
|
||||||
|
|
||||||
|
# unset custom metadata
|
||||||
|
self.conn.object_store.delete_account_metadata(['k1'])
|
||||||
|
account = self.conn.object_store.get_account_metadata()
|
||||||
|
self.assertTrue(account.metadata)
|
||||||
|
self.assertEqual(1, len(account.metadata))
|
||||||
|
self.assertIn('k2', account.metadata)
|
||||||
|
self.assertEqual('v2', account.metadata['k2'])
|
@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from openstack.object_store.v1 import container
|
from openstack.object_store.v1 import container as _container
|
||||||
from openstack.tests.functional import base
|
from openstack.tests.functional import base
|
||||||
|
|
||||||
|
|
||||||
@ -23,9 +23,9 @@ class TestContainer(base.BaseFunctionalTest):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(TestContainer, cls).setUpClass()
|
super(TestContainer, cls).setUpClass()
|
||||||
tainer = cls.conn.object_store.create_container(name=cls.NAME)
|
container = cls.conn.object_store.create_container(name=cls.NAME)
|
||||||
assert isinstance(tainer, container.Container)
|
assert isinstance(container, _container.Container)
|
||||||
cls.assertIs(cls.NAME, tainer.name)
|
cls.assertIs(cls.NAME, container.name)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def tearDownClass(cls):
|
def tearDownClass(cls):
|
||||||
@ -37,8 +37,98 @@ class TestContainer(base.BaseFunctionalTest):
|
|||||||
names = [o.name for o in self.conn.object_store.containers()]
|
names = [o.name for o in self.conn.object_store.containers()]
|
||||||
self.assertIn(self.NAME, names)
|
self.assertIn(self.NAME, names)
|
||||||
|
|
||||||
def test_get_metadata(self):
|
def test_system_metadata(self):
|
||||||
tainer = self.conn.object_store.get_container_metadata(self.NAME)
|
# get system metadata
|
||||||
self.assertEqual(0, tainer.object_count)
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
self.assertEqual(0, tainer.bytes_used)
|
self.assertEqual(0, container.object_count)
|
||||||
self.assertEqual(self.NAME, tainer.name)
|
self.assertEqual(0, container.bytes_used)
|
||||||
|
|
||||||
|
# set system metadata
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertIsNone(container.read_ACL)
|
||||||
|
self.assertIsNone(container.write_ACL)
|
||||||
|
self.conn.object_store.set_container_metadata(
|
||||||
|
container, read_ACL='.r:*', write_ACL='demo:demo')
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertEqual('.r:*', container.read_ACL)
|
||||||
|
self.assertEqual('demo:demo', container.write_ACL)
|
||||||
|
|
||||||
|
# update system metadata
|
||||||
|
self.conn.object_store.set_container_metadata(
|
||||||
|
container, read_ACL='.r:demo')
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertEqual('.r:demo', container.read_ACL)
|
||||||
|
self.assertEqual('demo:demo', container.write_ACL)
|
||||||
|
|
||||||
|
# set system metadata and custom metadata
|
||||||
|
self.conn.object_store.set_container_metadata(
|
||||||
|
container, k0='v0', sync_key='1234')
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertTrue(container.metadata)
|
||||||
|
self.assertIn('k0', container.metadata)
|
||||||
|
self.assertEqual('v0', container.metadata['k0'])
|
||||||
|
self.assertEqual('.r:demo', container.read_ACL)
|
||||||
|
self.assertEqual('demo:demo', container.write_ACL)
|
||||||
|
self.assertEqual('1234', container.sync_key)
|
||||||
|
|
||||||
|
# unset system metadata
|
||||||
|
self.conn.object_store.delete_container_metadata(container,
|
||||||
|
['sync_key'])
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertTrue(container.metadata)
|
||||||
|
self.assertIn('k0', container.metadata)
|
||||||
|
self.assertEqual('v0', container.metadata['k0'])
|
||||||
|
self.assertEqual('.r:demo', container.read_ACL)
|
||||||
|
self.assertEqual('demo:demo', container.write_ACL)
|
||||||
|
self.assertIsNone(container.sync_key)
|
||||||
|
|
||||||
|
def test_custom_metadata(self):
|
||||||
|
# get custom metadata
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertFalse(container.metadata)
|
||||||
|
|
||||||
|
# set no custom metadata
|
||||||
|
self.conn.object_store.set_container_metadata(container)
|
||||||
|
container = self.conn.object_store.get_container_metadata(container)
|
||||||
|
self.assertFalse(container.metadata)
|
||||||
|
|
||||||
|
# set empty custom metadata
|
||||||
|
self.conn.object_store.set_container_metadata(container, k0='')
|
||||||
|
container = self.conn.object_store.get_container_metadata(container)
|
||||||
|
self.assertFalse(container.metadata)
|
||||||
|
|
||||||
|
# set custom metadata
|
||||||
|
self.conn.object_store.set_container_metadata(container, k1='v1')
|
||||||
|
container = self.conn.object_store.get_container_metadata(container)
|
||||||
|
self.assertTrue(container.metadata)
|
||||||
|
self.assertEqual(1, len(container.metadata))
|
||||||
|
self.assertIn('k1', container.metadata)
|
||||||
|
self.assertEqual('v1', container.metadata['k1'])
|
||||||
|
|
||||||
|
# set more custom metadata by named container
|
||||||
|
self.conn.object_store.set_container_metadata(self.NAME, k2='v2')
|
||||||
|
container = self.conn.object_store.get_container_metadata(container)
|
||||||
|
self.assertTrue(container.metadata)
|
||||||
|
self.assertEqual(2, len(container.metadata))
|
||||||
|
self.assertIn('k1', container.metadata)
|
||||||
|
self.assertEqual('v1', container.metadata['k1'])
|
||||||
|
self.assertIn('k2', container.metadata)
|
||||||
|
self.assertEqual('v2', container.metadata['k2'])
|
||||||
|
|
||||||
|
# update metadata
|
||||||
|
self.conn.object_store.set_container_metadata(container, k1='v1.1')
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertTrue(container.metadata)
|
||||||
|
self.assertEqual(2, len(container.metadata))
|
||||||
|
self.assertIn('k1', container.metadata)
|
||||||
|
self.assertEqual('v1.1', container.metadata['k1'])
|
||||||
|
self.assertIn('k2', container.metadata)
|
||||||
|
self.assertEqual('v2', container.metadata['k2'])
|
||||||
|
|
||||||
|
# delete metadata
|
||||||
|
self.conn.object_store.delete_container_metadata(container, ['k1'])
|
||||||
|
container = self.conn.object_store.get_container_metadata(self.NAME)
|
||||||
|
self.assertTrue(container.metadata)
|
||||||
|
self.assertEqual(1, len(container.metadata))
|
||||||
|
self.assertIn('k2', container.metadata)
|
||||||
|
self.assertEqual('v2', container.metadata['k2'])
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import datetime
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from openstack.tests.functional import base
|
from openstack.tests.functional import base
|
||||||
@ -46,12 +47,110 @@ class TestObject(base.BaseFunctionalTest):
|
|||||||
result = self.conn.object_store.get_object(self.sot)
|
result = self.conn.object_store.get_object(self.sot)
|
||||||
self.assertEqual(self.DATA, result)
|
self.assertEqual(self.DATA, result)
|
||||||
|
|
||||||
def test_get_metadata(self):
|
def test_system_metadata(self):
|
||||||
self.sot.data = None
|
# get system metadata
|
||||||
self.sot.set_headers({'x-object-meta-test': 'orly'})
|
obj = self.conn.object_store.get_object_metadata(
|
||||||
result = self.conn.object_store.set_object_metadata(self.sot)
|
self.FILE, container=self.FOLDER)
|
||||||
result = self.conn.object_store.get_object_metadata(self.sot)
|
self.assertGreaterEqual(0, obj.bytes)
|
||||||
self.assertEqual(self.FILE, result.name)
|
self.assertIsNotNone(obj.etag)
|
||||||
headers = result.get_headers()
|
|
||||||
self.assertEqual(str(len(self.DATA)), headers['content-length'])
|
# set system metadata
|
||||||
self.assertEqual('orly', headers['x-object-meta-test'])
|
obj = self.conn.object_store.get_object_metadata(
|
||||||
|
self.FILE, container=self.FOLDER)
|
||||||
|
self.assertIsNone(obj.content_disposition)
|
||||||
|
self.assertIsNone(obj.content_encoding)
|
||||||
|
self.conn.object_store.set_object_metadata(
|
||||||
|
obj, content_disposition='attachment', content_encoding='gzip')
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertEqual('attachment', obj.content_disposition)
|
||||||
|
self.assertEqual('gzip', obj.content_encoding)
|
||||||
|
|
||||||
|
# update system metadata
|
||||||
|
self.conn.object_store.set_object_metadata(
|
||||||
|
obj, content_encoding='deflate')
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertEqual('attachment', obj.content_disposition)
|
||||||
|
self.assertEqual('deflate', obj.content_encoding)
|
||||||
|
|
||||||
|
# set system metadata and custom metadata
|
||||||
|
self.conn.object_store.set_object_metadata(
|
||||||
|
obj, k0='v0', delete_after=100)
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertIn('k0', obj.metadata)
|
||||||
|
self.assertEqual('v0', obj.metadata['k0'])
|
||||||
|
self.assertEqual('attachment', obj.content_disposition)
|
||||||
|
self.assertEqual('deflate', obj.content_encoding)
|
||||||
|
self.assertIsInstance(obj.delete_at, datetime.datetime)
|
||||||
|
|
||||||
|
# unset system metadata
|
||||||
|
self.conn.object_store.delete_object_metadata(
|
||||||
|
obj, keys=['delete_after'])
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertIn('k0', obj.metadata)
|
||||||
|
self.assertEqual('v0', obj.metadata['k0'])
|
||||||
|
self.assertEqual('attachment', obj.content_disposition)
|
||||||
|
self.assertEqual('deflate', obj.content_encoding)
|
||||||
|
self.assertIsNone(obj.delete_at)
|
||||||
|
|
||||||
|
# unset more system metadata
|
||||||
|
self.conn.object_store.delete_object_metadata(
|
||||||
|
obj, keys=['content_disposition'])
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertIn('k0', obj.metadata)
|
||||||
|
self.assertEqual('v0', obj.metadata['k0'])
|
||||||
|
self.assertIsNone(obj.content_disposition)
|
||||||
|
self.assertEqual('deflate', obj.content_encoding)
|
||||||
|
self.assertIsNone(obj.delete_at)
|
||||||
|
|
||||||
|
def test_custom_metadata(self):
|
||||||
|
# get custom metadata
|
||||||
|
obj = self.conn.object_store.get_object_metadata(
|
||||||
|
self.FILE, container=self.FOLDER)
|
||||||
|
self.assertFalse(obj.metadata)
|
||||||
|
|
||||||
|
# set no custom metadata
|
||||||
|
self.conn.object_store.set_object_metadata(obj)
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertFalse(obj.metadata)
|
||||||
|
|
||||||
|
# set empty custom metadata
|
||||||
|
self.conn.object_store.set_object_metadata(obj, k0='')
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertFalse(obj.metadata)
|
||||||
|
|
||||||
|
# set custom metadata
|
||||||
|
self.conn.object_store.set_object_metadata(obj, k1='v1')
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertTrue(obj.metadata)
|
||||||
|
self.assertEqual(1, len(obj.metadata))
|
||||||
|
self.assertIn('k1', obj.metadata)
|
||||||
|
self.assertEqual('v1', obj.metadata['k1'])
|
||||||
|
|
||||||
|
# set more custom metadata by named object and container
|
||||||
|
self.conn.object_store.set_object_metadata(self.FILE, self.FOLDER,
|
||||||
|
k2='v2')
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertTrue(obj.metadata)
|
||||||
|
self.assertEqual(2, len(obj.metadata))
|
||||||
|
self.assertIn('k1', obj.metadata)
|
||||||
|
self.assertEqual('v1', obj.metadata['k1'])
|
||||||
|
self.assertIn('k2', obj.metadata)
|
||||||
|
self.assertEqual('v2', obj.metadata['k2'])
|
||||||
|
|
||||||
|
# update custom metadata
|
||||||
|
self.conn.object_store.set_object_metadata(obj, k1='v1.1')
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertTrue(obj.metadata)
|
||||||
|
self.assertEqual(2, len(obj.metadata))
|
||||||
|
self.assertIn('k1', obj.metadata)
|
||||||
|
self.assertEqual('v1.1', obj.metadata['k1'])
|
||||||
|
self.assertIn('k2', obj.metadata)
|
||||||
|
self.assertEqual('v2', obj.metadata['k2'])
|
||||||
|
|
||||||
|
# unset custom metadata
|
||||||
|
self.conn.object_store.delete_object_metadata(obj, keys=['k1'])
|
||||||
|
obj = self.conn.object_store.get_object_metadata(obj)
|
||||||
|
self.assertTrue(obj.metadata)
|
||||||
|
self.assertEqual(1, len(obj.metadata))
|
||||||
|
self.assertIn('k2', obj.metadata)
|
||||||
|
self.assertEqual('v2', obj.metadata['k2'])
|
||||||
|
Loading…
Reference in New Issue
Block a user