Conditional object create/update.
authorAntony Chazapis <chazapis@gmail.com>
Fri, 22 Jul 2011 13:03:32 +0000 (16:03 +0300)
committerAntony Chazapis <chazapis@gmail.com>
Fri, 22 Jul 2011 13:03:32 +0000 (16:03 +0300)
docs/source/devguide.rst
pithos/api/functions.py
pithos/api/util.py

index 1edb4d4..24f491f 100644 (file)
@@ -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 ``/<container>/<object>``
 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.
 
index ece0173..9231631 100644 (file)
@@ -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:
index 32c565f..f819765 100644 (file)
@@ -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')