Document 'update' parameter. Add to account/container POST.
authorAntony Chazapis <chazapis@gmail.com>
Wed, 22 Jun 2011 17:36:35 +0000 (20:36 +0300)
committerAntony Chazapis <chazapis@gmail.com>
Wed, 22 Jun 2011 17:36:35 +0000 (20:36 +0300)
docs/source/devguide.rst
pithos/api/functions.py
pithos/api/util.py
pithos/backends/base.py
pithos/backends/simple.py

index 6d889c8..c7378fb 100644 (file)
@@ -25,10 +25,11 @@ Document Revisions
 =========================  ================================
 Revision                   Description
 =========================  ================================
+0.4 (June 22, 2011)        Support updating/deleting individual metadata with ``POST``.
 0.3 (June 14, 2011)        Large object support with ``X-Object-Manifest``.
 \                          Allow for publicly available objects via ``https://hostname/public``.
 \                          Support time-variant account/container listings. 
-\                          Add source version when duplicating with PUT/COPY/MOVE.
+\                          Add source version when duplicating with PUT/COPY.
 \                          Request version in object HEAD/GET requests (list versions with GET).
 0.2 (May 31, 2011)         Add object meta listing and filtering in containers.
 \                          Include underlying storage characteristics in container meta.
@@ -183,6 +184,14 @@ Will use a ``200`` return code if the reply is of type json/xml.
 POST
 """"
 
+======================  ============================================
+Request Parameter Name  Value
+======================  ============================================
+update                  Do not replace metadata (no value parameter)
+======================  ============================================
+
+|
+
 ====================  ===========================
 Request Header Name   Value
 ====================  ===========================
@@ -191,7 +200,7 @@ X-Account-Meta-*      Optional user defined metadata
 
 No reply content/headers.
 
-The update operation will overwrite all user defined metadata.
+The operation will overwrite all user defined metadata, except if ``update`` is defined.
 
 ================  ===============================
 Return Code       Description
@@ -340,6 +349,14 @@ Return Code       Description
 POST
 """"
 
+======================  ============================================
+Request Parameter Name  Value
+======================  ============================================
+update                  Do not replace metadata (no value parameter)
+======================  ============================================
+
+|
+
 ====================  ================================
 Request Header Name   Value
 ====================  ================================
@@ -348,7 +365,7 @@ X-Container-Meta-*    Optional user defined metadata
 
 No reply content/headers.
 
-The update operation will overwrite all user defined metadata.
+The operation will overwrite all user defined metadata, except if ``update`` is defined.
 
 ================  ===============================
 Return Code       Description
@@ -574,6 +591,8 @@ X-Object-Public       Object is publicly accessible (optional) (**TBD**)
 X-Object-Meta-*       Optional user defined metadata
 ====================  ================================
 
+Refer to ``POST`` for a description of request headers. Metadata is also copied, updated with any values defined.
+
 No reply content/headers.
 
 ===========================  ==============================
@@ -592,6 +611,14 @@ 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
 ====================  ================================
@@ -606,12 +633,12 @@ X-Object-Public       Object is publicly accessible (optional) (**TBD**)
 X-Object-Meta-*       Optional user defined metadata
 ====================  ================================
 
-The ``Content-Encoding``, ``Content-Disposition``, ``X-Object-Manifest``, ``X-Object-Public`` (**TBD**) and ``X-Object-Meta-*`` headers are considered to be user defined metadata. The update operation will overwrite all previous values and remove any keys not supplied.
+The ``Content-Encoding``, ``Content-Disposition``, ``X-Object-Manifest``, ``X-Object-Public`` (**TBD**) 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 update an object:
+To update an object's data:
 
+* Set ``Content-Type`` to ``application/octet-stream``. If ``Content-Type`` has some other value, it will be ignored and only the metadata will be updated.
 * Supply ``Content-Length`` (except if using chunked transfers), ``Content-Type`` and ``Content-Range`` headers.
-* Set ``Content-Type`` to ``application/octet-stream``.
 * Set ``Content-Range`` as specified in RFC2616, with the following differences:
 
   * Client software MAY omit ``last-byte-pos`` of if the length of the range being transferred is unknown or difficult to determine.
@@ -636,7 +663,7 @@ Return Code                  Description
 202 (Accepted)               The request has been accepted (not a data update)
 204 (No Content)             The request succeeded (data updated)
 411 (Length Required)        Missing ``Content-Length`` in the request
-416 (Range Not Satisfiable)  The supplied range is out of limits or invalid size
+416 (Range Not Satisfiable)  The supplied range is invalid
 ===========================  ==============================
 
 
@@ -682,7 +709,7 @@ List of differences from the OOS API:
 * 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.
 * 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. 
-* Object metadata allowed, in addition to ``X-Object-Meta-*``: ``Content-Encoding``, ``Content-Disposition``, ``X-Object-Manifest``, ``X-Object-Public`` (**TBD**). These are all replaced with every update operation.
+* Object metadata allowed, in addition to ``X-Object-Meta-*``: ``Content-Encoding``, ``Content-Disposition``, ``X-Object-Manifest``, ``X-Object-Public`` (**TBD**). 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.
 * Multi-range object GET support as outlined in RFC2616.
 * Object hashmap retrieval through GET and the ``format`` parameter.
 * Partial object updates through POST, using the ``Content-Length``, ``Content-Type``, ``Content-Range`` and ``Transfer-Encoding`` headers.
@@ -702,6 +729,7 @@ Clarifications/suggestions:
 * Container/object lists use a ``200`` return code if the reply is of type json/xml. The reply will include an empty json/xml.
 * In headers, dates are formatted according to RFC 1123. In extended information listings, dates are formatted according to ISO 8601.
 * The ``Last-Modified`` header value always reflects the actual latest change timestamp, regardless of time control parameters and version requests. Time precondition checks with ``If-Modified-Since`` and ``If-Unmodified-Since`` headers are applied to this value.
+* A copy/move using ``PUT``/``COPY``/``MOVE`` will always update metadata, keeping all old values except the ones redefined in the request headers.
 * A ``HEAD`` or ``GET`` for an ``X-Object-Manifest`` object, will include modified ``Content-Length`` and ``ETag`` headers, according to the characteristics of the objects under the specified prefix. The ``Etag`` will be the MD5 hash of the corresponding ETags concatenated. In extended container listings there is no metadata processing.
 
 The Pithos Client
@@ -796,6 +824,8 @@ Assuming an authentication token is obtained (**TBD**), the following high-level
          -H "X-Auth-Token: 0000" \
          https://pithos.dev.grnet.gr/v1/user/pithos?format=json
 
+  It is recommended that extended replies are cached and subsequent requests utilize the ``If-Modified-Since`` header.
+
 * List metadata keys used by objects in a container
 
   Will be in the ``X-Container-Object-Meta`` reply header, included in container information or object list (``HEAD`` or ``GET``).
index 9ae62b7..4030700 100644 (file)
@@ -139,8 +139,11 @@ def account_update(request, v_account):
     #                       unauthorized (401),
     #                       badRequest (400)
     
-    meta = get_account_meta(request)    
-    backend.update_account_meta(request.user, v_account, meta, replace=True)
+    meta = get_account_meta(request)
+    replace = True
+    if 'update' in request.GET:
+        replace = False
+    backend.update_account_meta(request.user, v_account, meta, replace)
     return HttpResponse(status=202)
 
 @api_method('GET', format_allowed=True)
@@ -248,8 +251,11 @@ def container_update(request, v_account, v_container):
     #                       badRequest (400)
     
     meta = get_container_meta(request)
+    replace = True
+    if 'update' in request.GET:
+        replace = False
     try:
-        backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
+        backend.update_container_meta(request.user, v_account, v_container, meta, replace)
     except NameError:
         raise ItemNotFound('Container does not exist')
     return HttpResponse(status=202)
index 7c967bf..d51e0e2 100644 (file)
@@ -77,7 +77,7 @@ def get_meta_prefix(request, prefix):
     """Get all prefix-* request headers in a dict. Reformat keys with format_meta_key()."""
     
     prefix = 'HTTP_' + prefix.upper().replace('-', '_')
-    return dict([(format_meta_key(k[5:]), v) for k, v in request.META.iteritems() if k.startswith(prefix)])
+    return dict([(format_meta_key(k[5:]), v) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
 
 def get_account_meta(request):
     """Get metadata from an account request."""
@@ -232,28 +232,12 @@ def copy_or_move_object(request, v_account, src_container, src_name, dest_contai
     
     meta = get_object_meta(request)
     permissions = get_sharing(request)
-    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
-    
-    try:
-        if move:
-            src_meta = backend.get_object_meta(request.user, v_account, src_container, src_name)
-        else:
-            src_meta = backend.get_object_meta(request.user, v_account, src_container, src_name, src_version)
-    except NameError, IndexError:
-        raise ItemNotFound('Container or object does not exist')
-    
-    # Keep previous values of 'Content-Type' (if a new one is absent) and 'hash'.
-    if 'Content-Type' in meta and 'Content-Type' in src_meta:
-        del(src_meta['Content-Type'])
-    for k in ('Content-Type', 'hash'):
-        if k in src_meta:
-            meta[k] = src_meta[k]
-    
+    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
     try:
         if move:
-            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions)
+            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
         else:
-            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions, src_version)
+            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
     except NameError, IndexError:
         raise ItemNotFound('Container or object does not exist')
     except ValueError:
index ed86620..f9d1558 100644 (file)
@@ -39,6 +39,8 @@ class BaseBackend(object):
     
     Note that the account level is always valid as it is checked from another subsystem.
     
+    When not replacing metadata, keys with empty values should be deleted.
+    
     The following variables should be available:
         'hash_algorithm': Suggested is 'sha256'
         'block_size': Suggested is 4MB
@@ -174,7 +176,7 @@ class BaseBackend(object):
         """Update the metadata associated with the object.
         
         Parameters:
-            'meta': Dictionary with metadata to update.
+            'meta': Dictionary with metadata to update
             'replace': Replace instead of update
         
         Raises:
index b0887bc..8c44f61 100644 (file)
@@ -235,7 +235,10 @@ class SimpleBackend(BaseBackend):
         
         logger.debug("get_object_permissions: %s %s %s", account, container, name)
         path = self._get_objectinfo(account, container, name)[0]
-        return self._get_permissions(path)
+        perm_path, perms = self._get_permissions(path)
+        if path == perm_path:
+            return perms
+        return {}
     
     def update_object_permissions(self, user, account, container, name, permissions):
         """Update the permissions associated with the object."""
@@ -476,8 +479,12 @@ class SimpleBackend(BaseBackend):
         
         src_version_id, dest_version_id = self._copy_version(path, path, not replace, True)
         for k, v in meta.iteritems():
-            sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
-            self.con.execute(sql, (dest_version_id, k, v))
+            if not replace and v == '':
+                sql = 'delete from metadata where version_id = ? and key = ?'
+                self.con.execute(sql, (dest_version_id, k))
+            else:
+                sql = 'insert or replace into metadata (version_id, key, value) values (?, ?, ?)'
+                self.con.execute(sql, (dest_version_id, k, v))
         self.con.commit()
     
     def _can_read(self, user, path):
@@ -510,13 +517,14 @@ class SimpleBackend(BaseBackend):
         return r, w
     
     def _get_permissions(self, path):
-        sql = 'select read, write from permissions where name = ?'
-        c = self.con.execute(sql, (path,))
+        # Check for permissions at path or above.
+        sql = 'select name, read, write from permissions where ? like name || ?'
+        c = self.con.execute(sql, (path, '%'))
         row = c.fetchone()
         if not row:
-            return {}
+            return path, {}
         
-        r, w = row
+        name, r, w = row
         if r == '' and w == '':
             return {'private': True}
         ret = {}
@@ -524,7 +532,7 @@ class SimpleBackend(BaseBackend):
             ret['write'] = w.split(',')
         if r != '':
             ret['read'] = r.split(',')        
-        return ret
+        return name, ret
     
     def _put_permissions(self, path, r, w):
         sql = 'insert or replace into permissions (name, read, write) values (?, ?, ?)'