From: Antony Chazapis Date: Fri, 22 Jul 2011 13:03:32 +0000 (+0300) Subject: Conditional object create/update. X-Git-Tag: pithos/v0.7.8~169^2~2^2~1 X-Git-Url: https://code.grnet.gr/git/pithos/commitdiff_plain/a8326bef39303a8a42897b1b46f94590b0db2a12 Conditional object create/update. --- diff --git a/docs/source/devguide.rst b/docs/source/devguide.rst index 1edb4d4..24f491f 100644 --- a/docs/source/devguide.rst +++ b/docs/source/devguide.rst @@ -25,13 +25,14 @@ Document Revisions ========================= ================================ Revision Description ========================= ================================ -0.5 (July 21, 2011) Object update from another object's data. +0.5 (July 22, 2011) Object update from another object's data. \ Support object truncate. \ Create object using a standard HTML form. \ Purge container/object history. \ List other accounts that share objects with a user. \ List shared containers/objects. \ Update implementation guidelines. +\ Check preconditions when creating/updating objects. 0.4 (July 01, 2011) Object permissions and account groups. \ Control versioning behavior and container quotas with container policy directives. \ Support updating/deleting individual metadata with ``POST``. @@ -159,6 +160,15 @@ POST Update account metadata HEAD """" +==================== =========================== +Request Header Name Value +==================== =========================== +If-Modified-Since Retrieve if account has changed since provided timestamp +If-Unmodified-Since Retrieve if account has not changed since provided timestamp +==================== =========================== + +| + ====================== =================================== Request Parameter Name Value ====================== =================================== @@ -247,14 +257,6 @@ Will use a ``200`` return code if the reply is of type json/xml. POST """" -====================== ============================================ -Request Parameter Name Value -====================== ============================================ -update Do not replace metadata/groups (no value parameter) -====================== ============================================ - -| - ==================== =========================== Request Header Name Value ==================== =========================== @@ -262,6 +264,14 @@ X-Account-Group-* Optional user defined groups X-Account-Meta-* Optional user defined metadata ==================== =========================== +| + +====================== ============================================ +Request Parameter Name Value +====================== ============================================ +update Do not replace metadata/groups (no value parameter) +====================== ============================================ + No reply content/headers. The operation will overwrite all user defined metadata, except if ``update`` is defined. @@ -293,6 +303,15 @@ DELETE Delete container HEAD """" +==================== =========================== +Request Header Name Value +==================== =========================== +If-Modified-Since Retrieve if container has changed since provided timestamp +If-Unmodified-Since Retrieve if container has not changed since provided timestamp +==================== =========================== + +| + ====================== =================================== Request Parameter Name Value ====================== =================================== @@ -437,14 +456,6 @@ Return Code Description POST """" -====================== ============================================ -Request Parameter Name Value -====================== ============================================ -update Do not replace metadata/policy (no value parameter) -====================== ============================================ - -| - ==================== ================================ Request Header Name Value ==================== ================================ @@ -452,6 +463,14 @@ X-Container-Policy-* Container behavior and limits X-Container-Meta-* Optional user defined metadata ==================== ================================ +| + +====================== ============================================ +Request Parameter Name Value +====================== ============================================ +update Do not replace metadata/policy (no value parameter) +====================== ============================================ + No reply content/headers. The operation will overwrite all user defined metadata, except if ``update`` is defined. @@ -506,6 +525,17 @@ DELETE Delete object HEAD """" +==================== ================================ +Request Header Name Value +==================== ================================ +If-Match Retrieve if ETags match +If-None-Match Retrieve if ETags don't match +If-Modified-Since Retrieve if object has changed since provided timestamp +If-Unmodified-Since Retrieve if object has not changed since provided timestamp +==================== ================================ + +| + ====================== =================================== Request Parameter Name Value ====================== =================================== @@ -645,6 +675,8 @@ PUT ==================== ================================ Request Header Name Value ==================== ================================ +If-Match Put if ETags match with current object +If-None-Match Put if ETags don't match with current object ETag The MD5 hash of the object (optional to check written data) Content-Length The size of the data written Content-Type The MIME content type of the object @@ -712,6 +744,8 @@ COPY ==================== ================================ Request Header Name Value ==================== ================================ +If-Match Proceed if ETags match with object +If-None-Match Proceed if ETags don't match with object Destination The destination path in the form ``//`` Content-Type The MIME content type of the object (optional) Content-Encoding The encoding of the object (optional) @@ -744,17 +778,11 @@ Same as ``COPY``, without the ``X-Source-Version`` request header. The ``MOVE`` POST """" -====================== ============================================ -Request Parameter Name Value -====================== ============================================ -update Do not replace metadata (no value parameter) -====================== ============================================ - -| - ==================== ================================ Request Header Name Value ==================== ================================ +If-Match Proceed if ETags match with object +If-None-Match Proceed if ETags don't match with object Content-Length The size of the data written (optional, to update) Content-Type The MIME content type of the object (optional, to update) Content-Range The range of data supplied (optional, to update) @@ -770,6 +798,14 @@ X-Object-Public Object is publicly accessible (optional) X-Object-Meta-* Optional user defined metadata ==================== ================================ +| + +====================== ============================================ +Request Parameter Name Value +====================== ============================================ +update Do not replace metadata (no value parameter) +====================== ============================================ + The ``Content-Encoding``, ``Content-Disposition``, ``X-Object-Manifest`` and ``X-Object-Meta-*`` headers are considered to be user defined metadata. An operation without the ``update`` parameter will overwrite all previous values and remove any keys not supplied. When using ``update`` any metadata with an empty value will be deleted. To change permissions, include an ``X-Object-Sharing`` header (as defined in ``PUT``). To publish, include an ``X-Object-Public`` header, with a value of ``true``. If no such headers are defined, no changes will be applied to sharing/public. Use empty values to remove permissions/unpublish (unpublishing also works with ``false`` as a header value). Sharing options are applied to the object - not its versions. @@ -886,7 +922,7 @@ List of differences from the OOS API: * Container policies to manage behavior and limits. * Headers ``X-Container-Block-*`` at the container level, exposing the underlying storage characteristics. * All metadata replies, at all levels, include latest modification information. -* At all levels, a ``GET`` request may use ``If-Modified-Since`` and ``If-Unmodified-Since`` headers. +* At all levels, a ``HEAD`` or ``GET`` request may use ``If-Modified-Since`` and ``If-Unmodified-Since`` headers. * Container/object lists include all associated metadata if the reply is of type json/xml. Some names are kept to their OOS API equivalents for compatibility. * Option to include only shared containers/objects in listings. * Object metadata allowed, in addition to ``X-Object-Meta-*``: ``Content-Encoding``, ``Content-Disposition``, ``X-Object-Manifest``. These are all replaced with every update operation, except if using the ``update`` parameter (in which case individual keys can also be deleted). Deleting meta by providing empty values also works when copying/moving an object. @@ -896,6 +932,7 @@ List of differences from the OOS API: * Object create using ``POST`` to support standard HTML forms. * Partial object updates through ``POST``, using the ``Content-Length``, ``Content-Type``, ``Content-Range`` and ``Transfer-Encoding`` headers. Use another object's data to update with ``X-Source-Object`` and ``X-Source-Version``. Truncate with ``X-Object-Bytes``. * Object ``MOVE`` support. +* Conditional object create/update operations, using ``If-Match`` and ``If-None-Match`` headers. * Time-variant account/container listings via the ``until`` parameter. * Object versions - parameter ``version`` in ``HEAD``/``GET`` (list versions with ``GET``), ``X-Object-Version-*`` meta in replies, ``X-Source-Version`` in ``PUT``/``COPY``. * Sharing/publishing with ``X-Object-Sharing``, ``X-Object-Public`` at the object level. Cross-user operations are allowed - controlled by sharing directives. Permissions may include groups defined with ``X-Account-Group-*`` at the account level. These apply to the object - not its versions. @@ -948,7 +985,7 @@ Some of these functions are performed by the client software and some by the Pit Implementation Guidelines ^^^^^^^^^^^^^^^^^^^^^^^^^ -Pithos clients should use the ``pithos`` and ``trash`` containers for active and inactive objects respectively. If any of these containers is not found, the client software should create it, without interrupting the user's workflow. The ``home`` element corresponds to ``pithos`` and the ``trash`` element to ``trash``. Use ``PUT`` with the ``X-Move-From`` header, or ``MOVE`` to transfer objects from one container to the other. Use ``DELETE`` to remove from ``pithos`` without trashing, or to remove from ``trash``. When moving objects, detect naming conflicts with the ``If-Match`` or ``If-None-Match`` headers (**TBD**). Such conflicts should be resolved by the user. +Pithos clients should use the ``pithos`` and ``trash`` containers for active and inactive objects respectively. If any of these containers is not found, the client software should create it, without interrupting the user's workflow. The ``home`` element corresponds to ``pithos`` and the ``trash`` element to ``trash``. Use ``PUT`` with the ``X-Move-From`` header, or ``MOVE`` to transfer objects from one container to the other. Use ``DELETE`` to remove from ``pithos`` without trashing, or to remove from ``trash``. When moving objects, detect naming conflicts with the ``If-Match`` or ``If-None-Match`` headers. Such conflicts should be resolved by the user. Object names should use the ``/`` delimiter to impose a hierarchy of folders and files. diff --git a/pithos/api/functions.py b/pithos/api/functions.py index ece0173..9231631 100644 --- a/pithos/api/functions.py +++ b/pithos/api/functions.py @@ -189,6 +189,8 @@ def account_meta(request, v_account): except NotAllowedError: raise Unauthorized('Access denied') + validate_modification_preconditions(request, meta) + response = HttpResponse(status=204) put_account_headers(response, meta, groups) return response @@ -303,6 +305,8 @@ def container_meta(request, v_account, v_container): except NameError: raise ItemNotFound('Container does not exist') + validate_modification_preconditions(request, meta) + response = HttpResponse(status=204) put_container_headers(response, meta, policy) return response @@ -322,6 +326,8 @@ def container_create(request, v_account, v_container): ret = 201 except NotAllowedError: raise Unauthorized('Access denied') + except ValueError: + raise BadRequest('Invalid policy header') except NameError: ret = 202 @@ -519,6 +525,15 @@ def object_meta(request, v_account, v_container, v_object): update_sharing_meta(permissions, v_account, v_container, v_object, meta) update_public_meta(public, meta) + # Evaluate conditions. + validate_modification_preconditions(request, meta) + try: + validate_matching_preconditions(request, meta) + except NotModified: + response = HttpResponse(status=304) + response['ETag'] = meta['hash'] + return response + response = HttpResponse(status=200) put_object_headers(response, meta) return response @@ -652,6 +667,16 @@ def object_write(request, v_account, v_container, v_object): if not request.GET.get('format'): request.serialization = 'text' + # Evaluate conditions. + if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'): + try: + meta = backend.get_object_meta(request.user, v_account, v_container, v_object) + except NotAllowedError: + raise Unauthorized('Access denied') + except NameError: + meta = {} + validate_matching_preconditions(request, meta) + copy_from = request.META.get('HTTP_X_COPY_FROM') move_from = request.META.get('HTTP_X_MOVE_FROM') if copy_from or move_from: @@ -800,6 +825,18 @@ def object_copy(request, v_account, v_container, v_object): dest_container, dest_name = split_container_object_string(dest_path) except ValueError: raise BadRequest('Invalid Destination header') + + # Evaluate conditions. + if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'): + src_version = request.META.get('HTTP_X_SOURCE_VERSION') + try: + meta = backend.get_object_meta(request.user, v_account, v_container, v_object, src_version) + except NotAllowedError: + raise Unauthorized('Access denied') + except (NameError, IndexError): + raise ItemNotFound('Container or object does not exist') + validate_matching_preconditions(request, meta) + copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False) return HttpResponse(status=201) @@ -818,6 +855,17 @@ def object_move(request, v_account, v_container, v_object): dest_container, dest_name = split_container_object_string(dest_path) except ValueError: raise BadRequest('Invalid Destination header') + + # Evaluate conditions. + if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'): + try: + meta = backend.get_object_meta(request.user, v_account, v_container, v_object) + except NotAllowedError: + raise Unauthorized('Access denied') + except NameError: + raise ItemNotFound('Container or object does not exist') + validate_matching_preconditions(request, meta) + copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True) return HttpResponse(status=201) @@ -840,6 +888,11 @@ def object_update(request, v_account, v_container, v_object): raise Unauthorized('Access denied') except NameError: raise ItemNotFound('Object does not exist') + + # Evaluate conditions. + if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'): + validate_matching_preconditions(request, prev_meta) + # If replacing, keep previous values of 'Content-Type' and 'hash'. replace = True if 'update' in request.GET: diff --git a/pithos/api/util.py b/pithos/api/util.py index 32c565f..f819765 100644 --- a/pithos/api/util.py +++ b/pithos/api/util.py @@ -228,18 +228,24 @@ def validate_modification_preconditions(request, meta): def validate_matching_preconditions(request, meta): """Check that the ETag conforms with the preconditions set.""" - if 'hash' not in meta: - return # TODO: Always return? + hash = meta.get('hash', None) if_match = request.META.get('HTTP_IF_MATCH') - if if_match is not None and if_match != '*': - if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]: - raise PreconditionFailed('Resource Etag does not match') + if if_match is not None: + if hash is None: + raise PreconditionFailed('Resource does not exist') + if if_match != '*' and hash not in [x.lower() for x in parse_etags(if_match)]: + raise PreconditionFailed('Resource ETag does not match') if_none_match = request.META.get('HTTP_IF_NONE_MATCH') if if_none_match is not None: - if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]: - raise NotModified('Resource Etag matches') + # TODO: If this passes, must ignore If-Modified-Since header. + if hash is not None: + if if_none_match == '*' or hash in [x.lower() for x in parse_etags(if_none_match)]: + # TODO: Continue if an If-Modified-Since header is present. + if request.method in ('HEAD', 'GET'): + raise NotModified('Resource ETag matches') + raise PreconditionFailed('Resource exists or ETag matches') def split_container_object_string(s): if not len(s) > 0 or s[0] != '/': @@ -262,7 +268,7 @@ def copy_or_move_object(request, v_account, src_container, src_name, dest_contai backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version) except NotAllowedError: raise Unauthorized('Access denied') - except NameError, IndexError: + except (NameError, IndexError): raise ItemNotFound('Container or object does not exist') except ValueError: raise BadRequest('Invalid sharing header')