Always include the Merkle hash in object meta (as returned from the backend). Clean...
authorAntony Chazapis <chazapis@gmail.com>
Mon, 21 Nov 2011 09:07:49 +0000 (11:07 +0200)
committerAntony Chazapis <chazapis@gmail.com>
Mon, 21 Nov 2011 09:07:49 +0000 (11:07 +0200)
Needs database update.

Fixes #1451

README.upgrade
docs/source/devguide.rst
pithos/api/functions.py
pithos/api/util.py
pithos/backends/base.py
pithos/backends/modular.py
pithos/public/functions.py

index 6277144..8cce135 100644 (file)
@@ -1 +1,10 @@
-Upgrade notes.
+UPGRADE
+=======
+
+0.7.9 -> 0.7.10
+---------------
+* Update settings.py (BACKEND*, SERVICE_NAME, *_EMAIL, *_TARGET)
+* Update 'attributes' table in mysql:
+    
+       mysql> update attributes set `key`='ETag' where `key`='hash';
+
index 1c71de6..12def4d 100644 (file)
@@ -25,7 +25,7 @@ Document Revisions
 =========================  ================================
 Revision                   Description
 =========================  ================================
-0.7 (Oct 21, 2011)         Suggest upload/download methods using hashmaps.
+0.7 (Nov 21, 2011)         Suggest upload/download methods using hashmaps.
 \                          Propose syncing algorithm.
 \                          Support cross-account object copy and move.
 \                          Pass token as a request parameter when using ``POST`` via an HTML form.
@@ -33,6 +33,7 @@ Revision                   Description
 \                          Use container ``POST`` to upload missing blocks of data.
 \                          Report policy in account headers.
 \                          Add insufficient quota reply.
+\                          Use special meta to always report Merkle hash.
 0.6 (Sept 13, 2011)        Reply with Merkle hash as the ETag when updating objects.
 \                          Include version id in object replace/change replies.
 \                          Change conflict (409) replies format to text.
@@ -437,6 +438,7 @@ content_type                The MIME content type of the object
 content_encoding            The encoding of the object (optional)
 content-disposition         The presentation style of the object (optional)
 last_modified               The last object modification date (regardless of version)
+x_object_hash               The Merkle hash
 x_object_version            The object's version identifier
 x_object_version_timestamp  The object's version timestamp
 x_object_modified_by        The user that committed the object's version
@@ -600,6 +602,7 @@ Content-Type                The MIME content type of the object
 Last-Modified               The last object modification date (regardless of version)
 Content-Encoding            The encoding of the object (optional)
 Content-Disposition         The presentation style of the object (optional)
+X-Object-Hash               The Merkle hash
 X-Object-Version            The object's version identifier
 X-Object-Version-Timestamp  The object's version timestamp
 X-Object-Modified-By        The user that comitted the object's version
@@ -695,6 +698,7 @@ Content-Range               The range of data included (only on a single range r
 Last-Modified               The last object modification date (regardless of version)
 Content-Encoding            The encoding of the object (optional)
 Content-Disposition         The presentation style of the object (optional)
+X-Object-Hash               The Merkle hash
 X-Object-Version            The object's version identifier
 X-Object-Version-Timestamp  The object's version timestamp
 X-Object-Modified-By        The user that comitted the object's version
@@ -979,6 +983,7 @@ List of differences from the OOS API:
 * Multi-range object ``GET`` support as outlined in RFC2616.
 * Object hashmap retrieval through ``GET`` and the ``format`` parameter.
 * Object create via hashmap through ``PUT`` and the ``format`` parameter.
+* The object's Merkle hash is always returned in the ``X-Object-Hash`` header.
 * 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``. New ETag corresponds to the Merkle hash of the object's hashmap.
 * Include new version identifier in replies for object replace/change requests.
index be638d8..eb9efd7 100644 (file)
@@ -532,6 +532,8 @@ def object_list(request, v_account, v_container):
             except NameError:
                 pass
             else:
+                rename_meta_key(meta, 'hash', 'x_object_hash') # Will be replaced by ETag.
+                rename_meta_key(meta, 'ETag', 'hash')
                 rename_meta_key(meta, 'modified', 'last_modified')
                 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
                 rename_meta_key(meta, 'version', 'x_object_version')
@@ -584,7 +586,7 @@ def object_meta(request, v_account, v_container, v_object):
         validate_matching_preconditions(request, meta)
     except NotModified:
         response = HttpResponse(status=304)
-        response['ETag'] = meta['hash']
+        response['ETag'] = meta['ETag']
         return response
     
     response = HttpResponse(status=200)
@@ -653,7 +655,7 @@ def object_read(request, v_account, v_container, v_object):
         validate_matching_preconditions(request, meta)
     except NotModified:
         response = HttpResponse(status=304)
-        response['ETag'] = meta['hash']
+        response['ETag'] = meta['ETag']
         return response
     
     sizes = []
@@ -805,7 +807,7 @@ def object_write(request, v_account, v_container, v_object):
             except:
                 raise BadRequest('Invalid data formatting')
         
-        meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
+        meta.update({'ETag': hashmap_hash(request, hashmap)}) # Update ETag.
     else:
         md5 = hashlib.md5()
         size = 0
@@ -818,9 +820,9 @@ def object_write(request, v_account, v_container, v_object):
             hashmap.append(request.backend.put_block(data))
             md5.update(data)
         
-        meta['hash'] = md5.hexdigest().lower()
+        meta['ETag'] = md5.hexdigest().lower()
         etag = request.META.get('HTTP_ETAG')
-        if etag and parse_etags(etag)[0].lower() != meta['hash']:
+        if etag and parse_etags(etag)[0].lower() != meta['ETag']:
             raise UnprocessableEntity('Object ETag does not match')
     
     try:
@@ -849,7 +851,7 @@ def object_write(request, v_account, v_container, v_object):
             raise ItemNotFound('Object does not exist')
     
     response = HttpResponse(status=201)
-    response['ETag'] = meta['hash']
+    response['ETag'] = meta['ETag']
     response['X-Object-Version'] = version_id
     return response
 
@@ -876,7 +878,7 @@ def object_write_form(request, v_account, v_container, v_object):
         hashmap.append(request.backend.put_block(data))
         md5.update(data)
     
-    meta['hash'] = md5.hexdigest().lower()
+    meta['ETag'] = md5.hexdigest().lower()
     
     try:
         version_id = request.backend.update_object_hashmap(request.user_uniq,
@@ -889,7 +891,7 @@ def object_write_form(request, v_account, v_container, v_object):
         raise RequestEntityTooLarge('Quota exceeded')
     
     response = HttpResponse(status=201)
-    response['ETag'] = meta['hash']
+    response['ETag'] = meta['ETag']
     response['X-Object-Version'] = version_id
     return response
 
@@ -991,12 +993,12 @@ def object_update(request, v_account, v_container, v_object):
     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'.
+    # If replacing, keep previous values of 'Content-Type' and 'ETag'.
     replace = True
     if 'update' in request.GET:
         replace = False
     if replace:
-        for k in ('Content-Type', 'hash'):
+        for k in ('Content-Type', 'ETag'):
             if k in prev_meta:
                 meta[k] = prev_meta[k]
     
@@ -1153,7 +1155,7 @@ def object_update(request, v_account, v_container, v_object):
     if dest_bytes is not None and dest_bytes < size:
         size = dest_bytes
         hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
-    meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
+    meta.update({'ETag': hashmap_hash(request, hashmap)}) # Update ETag.
     try:
         version_id = request.backend.update_object_hashmap(request.user_uniq,
                         v_account, v_container, v_object, size, hashmap, meta,
@@ -1178,7 +1180,7 @@ def object_update(request, v_account, v_container, v_object):
             raise ItemNotFound('Object does not exist')
     
     response = HttpResponse(status=204)
-    response['ETag'] = meta['hash']
+    response['ETag'] = meta['ETag']
     response['X-Object-Version'] = version_id
     return response
 
index a8c2ec7..ae7490f 100644 (file)
@@ -173,11 +173,12 @@ def get_object_headers(request):
     return meta, get_sharing(request), get_public(request)
 
 def put_object_headers(response, meta, restricted=False):
-    response['ETag'] = meta['hash']
+    response['ETag'] = meta['ETag']
     response['Content-Length'] = meta['bytes']
     response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
     response['Last-Modified'] = http_date(int(meta['modified']))
     if not restricted:
+        response['X-Object-Hash'] = meta['hash']
         response['X-Object-Modified-By'] = smart_str(meta['modified_by'], strings_only=True)
         response['X-Object-Version'] = meta['version']
         response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
@@ -197,7 +198,7 @@ def update_manifest_meta(request, v_account, meta):
     """Update metadata if the object has an X-Object-Manifest."""
     
     if 'X-Object-Manifest' in meta:
-        hash = ''
+        etag = ''
         bytes = 0
         try:
             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
@@ -206,15 +207,15 @@ def update_manifest_meta(request, v_account, meta):
             for x in objects:
                 src_meta = request.backend.get_object_meta(request.user_uniq,
                                         v_account, src_container, x[0], x[1])
-                hash += src_meta['hash']
+                etag += src_meta['ETag']
                 bytes += src_meta['bytes']
         except:
             # Ignore errors.
             return
         meta['bytes'] = bytes
         md5 = hashlib.md5()
-        md5.update(hash)
-        meta['hash'] = md5.hexdigest().lower()
+        md5.update(etag)
+        meta['ETag'] = md5.hexdigest().lower()
 
 def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
     if permissions is None:
@@ -261,20 +262,20 @@ def validate_modification_preconditions(request, meta):
 def validate_matching_preconditions(request, meta):
     """Check that the ETag conforms with the preconditions set."""
     
-    hash = meta.get('hash', None)
+    etag = meta.get('ETag', None)
     
     if_match = request.META.get('HTTP_IF_MATCH')
     if if_match is not None:
-        if hash is None:
+        if etag is None:
             raise PreconditionFailed('Resource does not exist')
-        if if_match != '*' and hash not in [x.lower() for x in parse_etags(if_match)]:
+        if if_match != '*' and etag 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:
         # 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)]:
+        if etag is not None:
+            if if_none_match == '*' or etag 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')
@@ -665,7 +666,7 @@ def object_data_response(request, sizes, hashmaps, meta, public=False):
                     ranges = [(0, size)]
                     ret = 200
             except ValueError:
-                if if_range != meta['hash']:
+                if if_range != meta['ETag']:
                     ranges = [(0, size)]
                     ret = 200
     
index 3651236..f887bde 100644 (file)
@@ -316,6 +316,8 @@ class BaseBackend(object):
             
             'bytes': The total data size
             
+            'hash': The hashmap hash
+            
             'modified': Last modification timestamp (overall)
             
             'modified_by': The user that committed the object (version requested)
index b20b7a3..d4c7181 100644 (file)
@@ -384,7 +384,7 @@ class ModularBackend(BaseBackend):
         
         if self._get_statistics(node)[0] > 0:
             raise IndexError('Container is not empty')
-        hashes = self.node.node_purge_children(node, until, CLUSTER_HISTORY)
+        hashes = self.node.node_purge_children(node, inf, CLUSTER_HISTORY)
         for h in hashes:
             self.store.map_delete(h)
         self.node.node_purge_children(node, inf, CLUSTER_DELETED)
@@ -446,7 +446,7 @@ class ModularBackend(BaseBackend):
                 modified = del_props[self.MTIME]
         
         meta = dict(self.node.attribute_get(props[self.SERIAL]))
-        meta.update({'name': name, 'bytes': props[self.SIZE]})
+        meta.update({'name': name, 'bytes': props[self.SIZE], 'hash':props[self.HASH]})
         meta.update({'version': props[self.SERIAL], 'version_timestamp': props[self.MTIME]})
         meta.update({'modified': modified, 'modified_by': props[self.MUSER]})
         return meta
@@ -623,12 +623,10 @@ class ModularBackend(BaseBackend):
             hashes += self.node.node_purge(node, until, CLUSTER_HISTORY)
             for h in hashes:
                 self.store.map_delete(h)
-            self.node.node_purge_children(node, until, CLUSTER_DELETED)
+            self.node.node_purge(node, until, CLUSTER_DELETED)
             try:
                 props = self._get_version(node)
             except NameError:
-                pass
-            else:
                 self.permissions.access_clear(path)
             return
         
index bed5b00..7b976b7 100644 (file)
@@ -71,8 +71,8 @@ def object_meta(request, v_account, v_container, v_object):
         raise ItemNotFound('Object does not exist')
     update_manifest_meta(request, v_account, meta)
     
-    response = HttpResponse(status=204)
-    put_object_meta(response, meta, True)
+    response = HttpResponse(status=200)
+    put_object_headers(response, meta, True)
     return response
 
 @api_method('GET', user_required=False)
@@ -103,7 +103,7 @@ def object_read(request, v_account, v_container, v_object):
         validate_matching_preconditions(request, meta)
     except NotModified:
         response = HttpResponse(status=304)
-        response['ETag'] = meta['hash']
+        response['ETag'] = meta['ETag']
         return response
     
     sizes = []