slight modifications to support list object versions
[pithos] / pithos / api / functions.py
index 9e9ecc5..76023b7 100644 (file)
@@ -34,7 +34,6 @@
 import os
 import logging
 import hashlib
-import uuid
 
 from django.http import HttpResponse
 from django.template.loader import render_to_string
@@ -45,9 +44,10 @@ from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Ite
     LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
 from pithos.api.util import (format_meta_key, printable_meta_dict, get_account_meta,
     put_account_meta, get_container_meta, put_container_meta, get_object_meta, put_object_meta,
-    validate_modification_preconditions, validate_matching_preconditions, copy_or_move_object,
-    get_content_length, get_range, get_content_range, raw_input_socket, socket_read_iterator,
-    ObjectWrapper, hashmap_hash, api_method)
+    update_manifest_meta, validate_modification_preconditions, validate_matching_preconditions,
+    split_container_object_string, copy_or_move_object, get_int_parameter, get_content_length,
+    get_content_range, raw_input_socket, socket_read_iterator, object_data_response,
+    hashmap_hash, api_method)
 from pithos.backends import backend
 
 
@@ -63,24 +63,24 @@ def top_demux(request):
 def account_demux(request, v_account):
     if request.method == 'HEAD':
         return account_meta(request, v_account)
-    elif request.method == 'GET':
-        return container_list(request, v_account)
     elif request.method == 'POST':
         return account_update(request, v_account)
+    elif request.method == 'GET':
+        return container_list(request, v_account)
     else:
         return method_not_allowed(request)
 
 def container_demux(request, v_account, v_container):
     if request.method == 'HEAD':
         return container_meta(request, v_account, v_container)
-    elif request.method == 'GET':
-        return object_list(request, v_account, v_container)
     elif request.method == 'PUT':
         return container_create(request, v_account, v_container)
     elif request.method == 'POST':
         return container_update(request, v_account, v_container)
     elif request.method == 'DELETE':
         return container_delete(request, v_account, v_container)
+    elif request.method == 'GET':
+        return object_list(request, v_account, v_container)
     else:
         return method_not_allowed(request)
 
@@ -125,7 +125,8 @@ def account_meta(request, v_account):
     #                       unauthorized (401),
     #                       badRequest (400)
     
-    meta = backend.get_account_meta(request.user)
+    until = get_int_parameter(request, 'until')
+    meta = backend.get_account_meta(request.user, v_account, until)
     
     response = HttpResponse(status=204)
     put_account_meta(response, meta)
@@ -139,7 +140,7 @@ def account_update(request, v_account):
     #                       badRequest (400)
     
     meta = get_account_meta(request)    
-    backend.update_account_meta(request.user, meta, replace=True)
+    backend.update_account_meta(request.user, v_account, meta, replace=True)
     return HttpResponse(status=202)
 
 @api_method('GET', format_allowed=True)
@@ -150,7 +151,8 @@ def container_list(request, v_account):
     #                       unauthorized (401),
     #                       badRequest (400)
     
-    meta = backend.get_account_meta(request.user)
+    until = get_int_parameter(request, 'until')
+    meta = backend.get_account_meta(request.user, v_account, until)
     
     validate_modification_preconditions(request, meta)
     
@@ -168,7 +170,7 @@ def container_list(request, v_account):
             limit = 10000
     
     try:
-        containers = backend.list_containers(request.user, marker, limit)
+        containers = backend.list_containers(request.user, v_account, marker, limit, until)
     except NameError:
         containers = []
     
@@ -178,18 +180,19 @@ def container_list(request, v_account):
             response.status_code = 204
             return response
         response.status_code = 200
-        response.content = '\n'.join(containers) + '\n'
+        response.content = '\n'.join([x[0] for x in containers]) + '\n'
         return response
     
     container_meta = []
     for x in containers:
-        try:
-            meta = backend.get_container_meta(request.user, x)
-        except NameError:
-            continue
-        container_meta.append(printable_meta_dict(meta))
+        if x[1] is not None:
+            try:
+                meta = backend.get_container_meta(request.user, v_account, x[0], until)
+                container_meta.append(printable_meta_dict(meta))
+            except NameError:
+                pass
     if request.serialization == 'xml':
-        data = render_to_string('containers.xml', {'account': request.user, 'containers': container_meta})
+        data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
     elif request.serialization  == 'json':
         data = json.dumps(container_meta)
     response.status_code = 200
@@ -204,9 +207,10 @@ def container_meta(request, v_account, v_container):
     #                       unauthorized (401),
     #                       badRequest (400)
     
+    until = get_int_parameter(request, 'until')
     try:
-        meta = backend.get_container_meta(request.user, v_container)
-        meta['object_meta'] = backend.list_object_meta(request.user, v_container)
+        meta = backend.get_container_meta(request.user, v_account, v_container, until)
+        meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
     except NameError:
         raise ItemNotFound('Container does not exist')
     
@@ -225,13 +229,13 @@ def container_create(request, v_account, v_container):
     meta = get_container_meta(request)
     
     try:
-        backend.put_container(request.user, v_container)
+        backend.put_container(request.user, v_account, v_container)
         ret = 201
     except NameError:
         ret = 202
     
     if len(meta) > 0:
-        backend.update_container_meta(request.user, v_container, meta, replace=True)
+        backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
     
     return HttpResponse(status=ret)
 
@@ -245,7 +249,7 @@ def container_update(request, v_account, v_container):
     
     meta = get_container_meta(request)
     try:
-        backend.update_container_meta(request.user, v_container, meta, replace=True)
+        backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
     except NameError:
         raise ItemNotFound('Container does not exist')
     return HttpResponse(status=202)
@@ -260,7 +264,7 @@ def container_delete(request, v_account, v_container):
     #                       badRequest (400)
     
     try:
-        backend.delete_container(request.user, v_container)
+        backend.delete_container(request.user, v_account, v_container)
     except NameError:
         raise ItemNotFound('Container does not exist')
     except IndexError:
@@ -275,9 +279,10 @@ def object_list(request, v_account, v_container):
     #                       unauthorized (401),
     #                       badRequest (400)
     
+    until = get_int_parameter(request, 'until')
     try:
-        meta = backend.get_container_meta(request.user, v_container)
-        meta['object_meta'] = backend.list_object_meta(request.user, v_container)
+        meta = backend.get_container_meta(request.user, v_account, v_container, until)
+        meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
     except NameError:
         raise ItemNotFound('Container does not exist')
     
@@ -322,7 +327,7 @@ def object_list(request, v_account, v_container):
         keys = []
     
     try:
-        objects = backend.list_objects(request.user, v_container, prefix, delimiter, marker, limit, virtual, keys)
+        objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, until)
     except NameError:
         raise ItemNotFound('Container does not exist')
     
@@ -332,19 +337,20 @@ def object_list(request, v_account, v_container):
             response.status_code = 204
             return response
         response.status_code = 200
-        response.content = '\n'.join(objects) + '\n'
+        response.content = '\n'.join([x[0] for x in objects]) + '\n'
         return response
     
     object_meta = []
     for x in objects:
-        try:
-            meta = backend.get_object_meta(request.user, v_container, x)
-        except NameError:
+        if x[1] is None:
             # Virtual objects/directories.
-            if virtual and delimiter and x.endswith(delimiter):
-                object_meta.append({'subdir': x})
-            continue
-        object_meta.append(printable_meta_dict(meta))
+            object_meta.append({'subdir': x[0]})
+        else:
+            try:
+                meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
+                object_meta.append(printable_meta_dict(meta))
+            except NameError:
+                pass
     if request.serialization == 'xml':
         data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
     elif request.serialization  == 'json':
@@ -361,10 +367,15 @@ def object_meta(request, v_account, v_container, v_object):
     #                       unauthorized (401),
     #                       badRequest (400)
     
+    version = get_int_parameter(request, 'version')
     try:
-        meta = backend.get_object_meta(request.user, v_container, v_object)
+        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
     except NameError:
         raise ItemNotFound('Object does not exist')
+    except IndexError:
+        raise ItemNotFound('Version does not exist')
+    
+    update_manifest_meta(request, v_account, meta)
     
     response = HttpResponse(status=204)
     put_object_meta(response, meta)
@@ -381,10 +392,17 @@ def object_read(request, v_account, v_container, v_object):
     #                       badRequest (400),
     #                       notModified (304)
     
+    version = get_int_parameter(request, 'version')
+    if not version:
+        version = request.GET.get('version')
     try:
-        meta = backend.get_object_meta(request.user, v_container, v_object)
+        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
     except NameError:
         raise ItemNotFound('Object does not exist')
+    except IndexError:
+        raise ItemNotFound('Version does not exist')
+    
+    update_manifest_meta(request, v_account, meta)
     
     # Evaluate conditions.
     validate_modification_preconditions(request, meta)
@@ -395,14 +413,57 @@ def object_read(request, v_account, v_container, v_object):
         response['ETag'] = meta['hash']
         return response
     
-    try:
-        # TODO: Also check for IndexError.
-        size, hashmap = backend.get_object_hashmap(request.user, v_container, v_object)
-    except NameError:
-        raise ItemNotFound('Object does not exist')
+    # Reply with the version list.
+    if version == 'list':
+        if request.serialization == 'text':
+            raise BadRequest('No format specified for version list.')
+        
+        d = {'versions': backend.list_versions(request.user, v_account, v_container, v_object)}
+        if request.serialization == 'xml':
+            d['object'] = v_object
+            data = render_to_string('versions.xml', d)
+        elif request.serialization  == 'json':
+            data = json.dumps(d)
+        
+        response = HttpResponse(data, status=200)
+        put_object_meta(response, meta)
+        response['Content-Length'] = len(data)
+        return response
+    
+    sizes = []
+    hashmaps = []
+    if 'X-Object-Manifest' in meta:
+        try:
+            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
+            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
+        except ValueError:
+            raise BadRequest('Invalid X-Object-Manifest header')
+        except NameError:
+            raise ItemNotFound('Container does not exist')
+        
+        try:
+            for x in objects:
+                s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
+                sizes.append(s)
+                hashmaps.append(h)
+        except NameError:
+            raise ItemNotFound('Object does not exist')
+        except IndexError:
+            raise ItemNotFound('Version does not exist')
+    else:
+        try:
+            s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
+            sizes.append(s)
+            hashmaps.append(h)
+        except NameError:
+            raise ItemNotFound('Object does not exist')
+        except IndexError:
+            raise ItemNotFound('Version does not exist')
     
     # Reply with the hashmap.
     if request.serialization != 'text':
+        size = sum(sizes)
+        hashmap = sum(hashmaps, [])
         d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
         if request.serialization == 'xml':
             d['object'] = v_object
@@ -415,36 +476,7 @@ def object_read(request, v_account, v_container, v_object):
         response['Content-Length'] = len(data)
         return response
     
-    # Range handling.
-    ranges = get_range(request, size)
-    if ranges is None:
-        ranges = [(0, size)]
-        ret = 200
-    else:
-        check = [True for offset, length in ranges if
-                    length <= 0 or length > size or
-                    offset < 0 or offset >= size or
-                    offset + length > size]
-        if len(check) > 0:
-            raise RangeNotSatisfiable('Requested range exceeds object limits')        
-        ret = 206
-    
-    if ret == 206 and len(ranges) > 1:
-        boundary = uuid.uuid4().hex
-    else:
-        boundary = ''
-    wrapper = ObjectWrapper(request.user, v_container, v_object, ranges, size, hashmap, boundary)
-    response = HttpResponse(wrapper, status=ret)
-    put_object_meta(response, meta)
-    if ret == 206:
-        if len(ranges) == 1:
-            offset, length = ranges[0]
-            response['Content-Length'] = length # Update with the correct length.
-            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
-        else:
-            del(response['Content-Length'])
-            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
-    return response
+    return object_data_response(request, sizes, hashmaps, meta)
 
 @api_method('PUT')
 def object_write(request, v_account, v_container, v_object):
@@ -463,9 +495,17 @@ def object_write(request, v_account, v_container, v_object):
         content_length = get_content_length(request)
         
         if move_from:
-            copy_or_move_object(request, move_from, (v_container, v_object), move=True)
+            try:
+                src_container, src_name = split_container_object_string(move_from)
+            except ValueError:
+                raise BadRequest('Invalid X-Move-From header')
+            copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
         else:
-            copy_or_move_object(request, copy_from, (v_container, v_object), move=False)
+            try:
+                src_container, src_name = split_container_object_string(copy_from)
+            except ValueError:
+                raise BadRequest('Invalid X-Copy-From header')
+            copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
         return HttpResponse(status=201)
     
     meta = get_object_meta(request)
@@ -477,21 +517,15 @@ def object_write(request, v_account, v_container, v_object):
         raise LengthRequired('Missing Content-Type header')
     
     md5 = hashlib.md5()
-    if content_length == 0:
-        try:
-            backend.update_object_hashmap(request.user, v_container, v_object, 0, [])
-        except NameError:
-            raise ItemNotFound('Container does not exist')
-    else:
-        size = 0
-        hashmap = []
-        sock = raw_input_socket(request)
-        for data in socket_read_iterator(sock, content_length, backend.block_size):
-            # TODO: Raise 408 (Request Timeout) if this takes too long.
-            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
-            size += len(data)
-            hashmap.append(backend.put_block(data))
-            md5.update(data)
+    size = 0
+    hashmap = []
+    sock = raw_input_socket(request)
+    for data in socket_read_iterator(sock, content_length, backend.block_size):
+        # TODO: Raise 408 (Request Timeout) if this takes too long.
+        # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
+        size += len(data)
+        hashmap.append(backend.put_block(data))
+        md5.update(data)
     
     meta['hash'] = md5.hexdigest().lower()
     etag = request.META.get('HTTP_ETAG')
@@ -499,13 +533,9 @@ def object_write(request, v_account, v_container, v_object):
         raise UnprocessableEntity('Object ETag does not match')
     
     try:
-        backend.update_object_hashmap(request.user, v_container, v_object, size, hashmap)
+        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
     except NameError:
         raise ItemNotFound('Container does not exist')
-    try:
-        backend.update_object_meta(request.user, v_container, v_object, meta, replace=True)
-    except NameError:
-        raise ItemNotFound('Object does not exist')
     
     response = HttpResponse(status=201)
     response['ETag'] = meta['hash']
@@ -522,7 +552,11 @@ def object_copy(request, v_account, v_container, v_object):
     dest_path = request.META.get('HTTP_DESTINATION')
     if not dest_path:
         raise BadRequest('Missing Destination header')
-    copy_or_move_object(request, (v_container, v_object), dest_path, move=False)
+    try:
+        dest_container, dest_name = split_container_object_string(dest_path)
+    except ValueError:
+        raise BadRequest('Invalid Destination header')
+    copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
     return HttpResponse(status=201)
 
 @api_method('MOVE')
@@ -536,7 +570,11 @@ def object_move(request, v_account, v_container, v_object):
     dest_path = request.META.get('HTTP_DESTINATION')
     if not dest_path:
         raise BadRequest('Missing Destination header')
-    copy_or_move_object(request, (v_container, v_object), dest_path, move=True)
+    try:
+        dest_container, dest_name = split_container_object_string(dest_path)
+    except ValueError:
+        raise BadRequest('Invalid Destination header')
+    copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
     return HttpResponse(status=201)
 
 @api_method('POST')
@@ -553,7 +591,7 @@ def object_update(request, v_account, v_container, v_object):
         del(meta['Content-Type']) # Do not allow changing the Content-Type.
     
     try:
-        prev_meta = backend.get_object_meta(request.user, v_container, v_object)
+        prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
     except NameError:
         raise ItemNotFound('Object does not exist')
     
@@ -564,7 +602,7 @@ def object_update(request, v_account, v_container, v_object):
             if k in prev_meta:
                 meta[k] = prev_meta[k]
         try:
-            backend.update_object_meta(request.user, v_container, v_object, meta, replace=True)
+            backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace=True)
         except NameError:
             raise ItemNotFound('Object does not exist')
     
@@ -589,15 +627,14 @@ def object_update(request, v_account, v_container, v_object):
         content_length = get_content_length(request)
     
     try:
-        # TODO: Also check for IndexError.
-        size, hashmap = backend.get_object_hashmap(request.user, v_container, v_object)
+        size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
     except NameError:
         raise ItemNotFound('Object does not exist')
     
     offset, length, total = ranges
     if offset is None:
         offset = size
-    if length is None:
+    if length is None or content_length == -1:
         length = content_length # Nevermind the error.
     elif length != content_length:
         raise BadRequest('Content length does not match range length')
@@ -631,20 +668,12 @@ def object_update(request, v_account, v_container, v_object):
     
     if offset > size:
         size = offset
+    meta = {'hash': hashmap_hash(hashmap)} # Update ETag.
     try:
-        backend.update_object_hashmap(request.user, v_container, v_object, size, hashmap)
+        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, False)
     except NameError:
         raise ItemNotFound('Container does not exist')
-    
-    # Update ETag.
-    # TODO: Move this to the backend.
-    meta = {}
-    meta['hash'] = hashmap_hash(hashmap)
-    try:
-        backend.update_object_meta(request.user, v_container, v_object, meta)
-    except NameError:
-        raise ItemNotFound('Object does not exist')
-    
+        
     response = HttpResponse(status=204)
     response['ETag'] = meta['hash']
     return response
@@ -658,7 +687,7 @@ def object_delete(request, v_account, v_container, v_object):
     #                       badRequest (400)
     
     try:
-        backend.delete_object(request.user, v_container, v_object)
+        backend.delete_object(request.user, v_account, v_container, v_object)
     except NameError:
         raise ItemNotFound('Object does not exist')
     return HttpResponse(status=204)