Use format for uploaded blocks hash lists at the container level.
[pithos] / pithos / api / util.py
index 9d81530..5e80dec 100644 (file)
@@ -1,4 +1,4 @@
-# Copyright 2011 GRNET S.A. All rights reserved.
+# Copyright 2011-2012 GRNET S.A. All rights reserved.
 # 
 # Redistribution and use in source and binary forms, with or
 # without modification, are permitted provided that the following
@@ -41,16 +41,18 @@ from urllib import quote, unquote
 
 from django.conf import settings
 from django.http import HttpResponse
+from django.template.loader import render_to_string
 from django.utils import simplejson as json
 from django.utils.http import http_date, parse_etags
-from django.utils.encoding import smart_str
+from django.utils.encoding import smart_unicode, smart_str
 from django.core.files.uploadhandler import FileUploadHandler
 from django.core.files.uploadedfile import UploadedFile
 
-from pithos.api.compat import parse_http_date_safe, parse_http_date
+from pithos.lib.compat import parse_http_date_safe, parse_http_date
+
 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
                                 Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
-                                RangeNotSatisfiable, ServiceUnavailable)
+                                RangeNotSatisfiable, InternalServerError, NotImplemented)
 from pithos.api.short_url import encode_url
 from pithos.backends import connect_backend
 from pithos.backends.base import NotAllowedError, QuotaError
@@ -178,12 +180,14 @@ def get_object_headers(request):
     return meta, get_sharing(request), get_public(request)
 
 def put_object_headers(response, meta, restricted=False):
-    response['ETag'] = meta['ETag']
+    if 'ETag' in meta:
+        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-UUID'] = meta['uuid']
         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 +201,7 @@ def put_object_headers(response, meta, restricted=False):
     else:
         for k in ('Content-Encoding', 'Content-Disposition'):
             if k in meta:
-                response[k] = meta[k]
+                response[k] = smart_str(meta[k], strings_only=True)
 
 def update_manifest_meta(request, v_account, meta):
     """Update metadata if the object has an X-Object-Manifest."""
@@ -211,8 +215,9 @@ def update_manifest_meta(request, v_account, meta):
                                 src_container, prefix=src_name, virtual=False)
             for x in objects:
                 src_meta = request.backend.get_object_meta(request.user_uniq,
-                                        v_account, src_container, x[0], x[1])
-                etag += src_meta['ETag']
+                                        v_account, src_container, x[0], 'pithos', x[1])
+                if 'ETag' in src_meta:
+                    etag += src_meta['ETag']
                 bytes += src_meta['bytes']
         except:
             # Ignore errors.
@@ -298,17 +303,19 @@ def split_container_object_string(s):
 def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False):
     """Copy or move an object."""
     
+    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
+        del(request.META['CONTENT_TYPE'])
     meta, permissions, public = get_object_headers(request)
     src_version = request.META.get('HTTP_X_SOURCE_VERSION')
     try:
         if move:
             version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
                                                         dest_account, dest_container, dest_name,
-                                                        meta, False, permissions)
+                                                        'pithos', meta, False, permissions)
         else:
             version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
                                                         dest_account, dest_container, dest_name,
-                                                        meta, False, permissions, src_version)
+                                                        'pithos', meta, False, permissions, src_version)
     except NotAllowedError:
         raise Forbidden('Not allowed')
     except (NameError, IndexError):
@@ -316,7 +323,7 @@ def copy_or_move_object(request, src_account, src_container, src_name, dest_acco
     except ValueError:
         raise BadRequest('Invalid sharing header')
     except AttributeError, e:
-        raise Conflict('\n'.join(e.data) + '\n')
+        raise Conflict(simple_list_response(request, e.data))
     except QuotaError:
         raise RequestEntityTooLarge('Quota exceeded')
     if public is not None:
@@ -493,7 +500,7 @@ def raw_input_socket(request):
         return request._req
     if 'wsgi.input' in request.environ:
         return request.environ['wsgi.input']
-    raise ServiceUnavailable('Unknown server software')
+    raise NotImplemented('Unknown server software')
 
 MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024) # 5GB
 
@@ -738,27 +745,36 @@ def put_object_block(request, hashmap, data, offset):
         hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
     return bl # Return ammount of data written.
 
-def hashmap_hash(request, hashmap):
-    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
-    
-    def subhash(d):
-        h = hashlib.new(request.backend.hash_algorithm)
-        h.update(d)
-        return h.digest()
-    
-    if len(hashmap) == 0:
-        return hexlify(subhash(''))
-    if len(hashmap) == 1:
-        return hashmap[0]
-    
-    s = 2
-    while s < len(hashmap):
-        s = s * 2
-    h = [unhexlify(x) for x in hashmap]
-    h += [('\x00' * len(h[0]))] * (s - len(hashmap))
-    while len(h) > 1:
-        h = [subhash(h[x] + h[x + 1]) for x in range(0, len(h), 2)]
-    return hexlify(h[0])
+def hashmap_md5(request, hashmap, size):
+    """Produce the MD5 sum from the data in the hashmap."""
+    
+    # TODO: Search backend for the MD5 of another object with the same hashmap and size...
+    md5 = hashlib.md5()
+    bs = request.backend.block_size
+    for bi, hash in enumerate(hashmap):
+        data = request.backend.get_block(hash)
+        if bi == len(hashmap) - 1:
+            bs = size % bs
+        pad = bs - min(len(data), bs)
+        md5.update(data + ('\x00' * pad))
+    return md5.hexdigest().lower()
+
+def simple_list_response(request, l):
+    if request.serialization == 'text':
+        return '\n'.join(l) + '\n'
+    if request.serialization == 'xml':
+        return render_to_string('items.xml', {'items': l})
+    if request.serialization == 'json':
+        return json.dumps(l)
+
+def get_backend():
+    backend = connect_backend(db_module=settings.BACKEND_DB_MODULE,
+                              db_connection=settings.BACKEND_DB_CONNECTION,
+                              block_module=settings.BACKEND_BLOCK_MODULE,
+                              block_path=settings.BACKEND_BLOCK_PATH)
+    backend.default_policy['quota'] = settings.BACKEND_QUOTA
+    backend.default_policy['versioning'] = settings.BACKEND_VERSIONING
+    return backend
 
 def update_request_headers(request):
     # Handle URL-encoded keys and values.
@@ -805,11 +821,13 @@ def update_response_headers(request, response):
         response['Date'] = format_date_time(time())
 
 def render_fault(request, fault):
-    if settings.DEBUG or settings.TEST:
+    if isinstance(fault, InternalServerError) and (settings.DEBUG or settings.TEST):
         fault.details = format_exc(fault)
     
     request.serialization = 'text'
-    data = '\n'.join((fault.message, fault.details)) + '\n'
+    data = fault.message + '\n'
+    if fault.details:
+        data += '\n' + fault.details
     response = HttpResponse(data, status=fault.code)
     update_response_headers(request, response)
     return response
@@ -861,7 +879,7 @@ def api_method(http_method=None, format_allowed=False, user_required=True):
                 
                 # Fill in custom request variables.
                 request.serialization = request_serialization(request, format_allowed)
-                request.backend = connect_backend()
+                request.backend = get_backend()
                 
                 response = func(request, *args, **kwargs)
                 update_response_headers(request, response)
@@ -870,7 +888,7 @@ def api_method(http_method=None, format_allowed=False, user_required=True):
                 return render_fault(request, fault)
             except BaseException, e:
                 logger.exception('Unexpected error: %s' % e)
-                fault = ServiceUnavailable('Unexpected error')
+                fault = InternalServerError('Unexpected error')
                 return render_fault(request, fault)
             finally:
                 if getattr(request, 'backend', None) is not None: