Allow for account/container metadata.
authorAntony Chazapis <chazapis@gmail.com>
Mon, 2 May 2011 21:55:15 +0000 (00:55 +0300)
committerAntony Chazapis <chazapis@gmail.com>
Mon, 2 May 2011 21:55:15 +0000 (00:55 +0300)
pithos/api/functions.py
pithos/api/util.py
pithos/backends/dummy.py
pithos/settings.py.dist

index 49084a7..8da3033 100644 (file)
@@ -13,7 +13,7 @@ except:
     from pithos.api.util import parse_http_date_safe\r
 \r
 from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity\r
-from pithos.api.util import get_object_meta, get_range, api_method\r
+from pithos.api.util import get_meta, get_range, api_method\r
 \r
 from settings import PROJECT_PATH\r
 from os import path\r
@@ -50,6 +50,8 @@ def account_demux(request, v_account):
         return account_meta(request, v_account)\r
     elif request.method == 'GET':\r
         return container_list(request, v_account)\r
+    elif request.method == 'POST':\r
+        return account_update(request, v_account)\r
     else:\r
         return method_not_allowed(request)\r
 \r
@@ -60,6 +62,8 @@ def container_demux(request, v_account, v_container):
         return object_list(request, v_account, v_container)\r
     elif request.method == 'PUT':\r
         return container_create(request, v_account, v_container)\r
+    elif request.method == 'POST':\r
+        return container_update(request, v_account, v_container)\r
     elif request.method == 'DELETE':\r
         return container_delete(request, v_account, v_container)\r
     else:\r
@@ -93,12 +97,30 @@ def account_meta(request, v_account):
         info = be.get_account_meta(request.user)\r
     except NameError:\r
         info = {'count': 0, 'bytes': 0}\r
-        \r
+    \r
     response = HttpResponse(status = 204)\r
     response['X-Account-Container-Count'] = info['count']\r
     response['X-Account-Bytes-Used'] = info['bytes']\r
+    for k in [x for x in info.keys() if x.startswith('X-Account-Meta-')]:\r
+        response[k] = info[k]\r
+    \r
     return response\r
 \r
+@api_method('POST')\r
+def account_update(request, v_account):\r
+    # Normal Response Codes: 202\r
+    # Error Response Codes: serviceUnavailable (503),\r
+    #                       itemNotFound (404),\r
+    #                       unauthorized (401),\r
+    #                       badRequest (400)\r
+    \r
+    meta = get_meta(request, 'X-Account-Meta-')\r
+    \r
+    be = BackEnd(STORAGE_PATH)\r
+    be.update_account_meta(request.user, meta)\r
+    \r
+    return HttpResponse(status = 202)\r
+\r
 @api_method('GET', format_allowed = True)\r
 def container_list(request, v_account):\r
     # Normal Response Codes: 200, 204\r
@@ -120,6 +142,7 @@ def container_list(request, v_account):
         containers = be.list_containers(request.user, marker, limit)\r
     except NameError:\r
         containers = []\r
+    # TODO: The cloudfiles python bindings expect 200 if json/xml.\r
     if len(containers) == 0:\r
         return HttpResponse(status = 204)\r
     \r
@@ -134,7 +157,7 @@ def container_list(request, v_account):
     if request.serialization == 'xml':\r
         data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})\r
     elif request.serialization  == 'json':\r
-        data = json.dumps(containers)    \r
+        data = json.dumps(containers)\r
     return HttpResponse(data, status = 200)\r
 \r
 @api_method('HEAD')\r
@@ -154,6 +177,9 @@ def container_meta(request, v_account, v_container):
     response = HttpResponse(status = 204)\r
     response['X-Container-Object-Count'] = info['count']\r
     response['X-Container-Bytes-Used'] = info['bytes']\r
+    for k in [x for x in info.keys() if x.startswith('X-Container-Meta-')]:\r
+        response[k] = info[k]\r
+    \r
     return response\r
 \r
 @api_method('PUT')\r
@@ -163,13 +189,38 @@ def container_create(request, v_account, v_container):
     #                       itemNotFound (404),\r
     #                       unauthorized (401),\r
     #                       badRequest (400)\r
-\r
+    \r
+    meta = get_meta(request, 'X-Container-Meta-')\r
+    \r
     be = BackEnd(STORAGE_PATH)\r
     try:\r
         be.create_container(request.user, v_container)\r
-        return HttpResponse(status = 201)\r
+        ret = 201\r
     except NameError:\r
-        return HttpResponse(status = 202)\r
+        ret = 202\r
+    \r
+    if len(meta) > 0:\r
+        be.update_container_meta(request.user, v_container, meta)\r
+    \r
+    return HttpResponse(status = ret)\r
+\r
+@api_method('POST')\r
+def container_update(request, v_account, v_container):\r
+    # Normal Response Codes: 202\r
+    # Error Response Codes: serviceUnavailable (503),\r
+    #                       itemNotFound (404),\r
+    #                       unauthorized (401),\r
+    #                       badRequest (400)\r
+    \r
+    meta = get_meta(request, 'X-Container-Meta-')\r
+    \r
+    be = BackEnd(STORAGE_PATH)\r
+    try:\r
+        be.update_container_meta(request.user, v_container, meta)\r
+    except NameError:\r
+        raise ItemNotFound()\r
+    \r
+    return HttpResponse(status = 202)\r
 \r
 @api_method('DELETE')\r
 def container_delete(request, v_account, v_container):\r
@@ -195,7 +246,7 @@ def container_delete(request, v_account, v_container):
         raise ItemNotFound()\r
     return HttpResponse(status = 204)\r
 \r
-# --- UP TO HERE ---\r
+# --- MERGED UP TO HERE ---\r
 \r
 @api_method('GET', format_allowed = True)\r
 def object_list(request, v_account, v_container):\r
@@ -362,7 +413,7 @@ def object_write(request, v_account, v_container, v_object):
         if not content_length or not content_type:\r
             raise LengthRequired()\r
     \r
-        meta = get_object_meta(request)\r
+        meta = get_meta(request, 'X-Object-Meta-')\r
         info = {'bytes': content_length, 'content_type': content_type, 'meta': meta}\r
     \r
         etag = request.META.get('HTTP_ETAG')\r
@@ -400,14 +451,14 @@ def object_copy(request, v_account, v_container, v_object):
         raise BadRequest('Bad Destination path.')\r
     dest_container = parts[1]\r
     dest_name = '/'.join(parts[2:])\r
-        \r
+    \r
     info = get_object_meta(request.user, v_container, v_object)\r
-        \r
+    \r
     content_type = request.META.get('CONTENT_TYPE')\r
     if content_type:\r
         info['content_type'] = content_type\r
         \r
-    meta = get_object_meta(request)\r
+    meta = get_meta(request, 'X-Object-Meta-')\r
     for k, v in meta.iteritems():\r
         info['meta'][k] = v\r
     \r
@@ -424,7 +475,7 @@ def object_update(request, v_account, v_container, v_object):
     #                       unauthorized (401),\r
     #                       badRequest (400)\r
     \r
-    meta = get_object_meta(request)\r
+    meta = get_meta(request, 'X-Object-Meta-')\r
     \r
     update_object_meta(request.user, v_container, v_object, meta)\r
     return HttpResponse(status = 202)\r
index 28d3f87..c2f07a4 100644 (file)
@@ -82,13 +82,21 @@ def parse_http_date_safe(date):
     except Exception:\r
         pass\r
 \r
-def get_object_meta(request):\r
+# Metadata handling.\r
+\r
+def format_meta_key(k):\r
+    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])\r
+\r
+def get_meta(request, prefix):\r
     """\r
-    Get all X-Object-Meta-* headers in a dict.\r
+    Get all prefix-* request headers in a dict.\r
+    All underscores are converted to dashes.\r
     """\r
-    prefix = 'HTTP_X_OBJECT_META_'\r
-    return dict([(k[len(prefix):].lower(), v) for k, v in request.META.iteritems() if k.startswith(prefix)])\r
-    \r
+    prefix = 'HTTP_' + prefix.upper().replace('-', '_')\r
+    return dict([(format_meta_key(k[5:]), v) for k, v in request.META.iteritems() if k.startswith(prefix)])\r
+\r
+# Range parsing.\r
+\r
 def get_range(request):\r
     """\r
     Parse a Range header from the request.\r
index c64e86b..20b9753 100644 (file)
@@ -3,17 +3,23 @@ import sqlite3
 import json\r
 import logging\r
 import types\r
+import hashlib\r
+\r
+logger = logging.getLogger(__name__)\r
+formatter = logging.Formatter('[%(levelname)s] %(message)s')\r
+handler = logging.FileHandler('backend.out')\r
+handler.setFormatter(formatter)\r
+logger.addHandler(handler)\r
 \r
 class BackEnd:\r
+\r
+    logger = None\r
+    \r
     def __init__(self, basepath, log_file='backend.out', log_level=logging.DEBUG):\r
         self.basepath = basepath\r
         \r
-        self.logger = logging.getLogger(__name__)\r
-        self.logger.setLevel(log_level)\r
-        formatter = logging.Formatter('[%(levelname)s] %(message)s')\r
-        handler = logging.FileHandler(log_file)\r
-        handler.setFormatter(formatter)\r
-        self.logger.addHandler(handler)\r
+        # TODO: Manage log_file.\r
+        logger.setLevel(log_level)\r
         \r
         if not os.path.exists(basepath):\r
             os.makedirs(basepath)\r
@@ -25,39 +31,54 @@ class BackEnd:
         sql = '''create table if not exists metadata(object_id int, name text, value text)'''\r
         self.con.execute(sql)\r
         self.con.commit()\r
-\r
+    \r
+    # TODO: Create/delete account?\r
+    \r
     def get_account_meta(self, account):\r
         """\r
-        returns a dictionary with the container metadata\r
+        returns a dictionary with the account metadata\r
         """\r
-        self.logger.debug("get_account_meta: %s", account)\r
+        logger.debug("get_account_meta: %s", account)\r
         fullname = os.path.join(self.basepath, account)\r
         if not os.path.exists(fullname):\r
             raise NameError('Account does not exist')\r
-        contents = os.listdir(fullname) \r
+        contents = os.listdir(fullname)\r
         count = len(contents)\r
         size = sum(os.path.getsize(os.path.join(self.basepath, account, objectname)) for objectname in contents)\r
-        return {'name': account, 'count': count, 'bytes': size}\r
-        \r
+        meta = self.__get_metadata(account)\r
+        meta.update({'name': account, 'count': count, 'bytes': size})\r
+        return meta\r
+\r
+    def update_account_meta(self, account, meta):\r
+        """\r
+        updates the metadata associated with the account\r
+        """\r
+        logger.debug("update_account_meta: %s %s", account, meta)\r
+        fullname = os.path.join(self.basepath, account)\r
+        if not os.path.exists(fullname):\r
+            os.makedirs(fullname)\r
+        self.__put_metadata(account, meta)\r
+        return\r
+    \r
     def create_container(self, account, name):\r
         """\r
         creates a new container with the given name\r
         if it doesn't exist under the basepath\r
         """\r
-        self.logger.debug("create_container: %s %s", account, name)\r
+        logger.debug("create_container: %s %s", account, name)\r
         fullname = os.path.join(self.basepath, account, name)\r
         if not os.path.exists(fullname):\r
             os.makedirs(fullname)\r
         else:\r
             raise NameError('Container already exists')\r
         return\r
-\r
+    \r
     def delete_container(self, account, name):\r
         """\r
         deletes the container with the given name\r
         if it exists under the basepath and is empty\r
         """\r
-        self.logger.debug("delete_container: %s %s", account, name)\r
+        logger.debug("delete_container: %s %s", account, name)\r
         fullname = os.path.join(self.basepath, account, name)\r
         if not os.path.exists(fullname):\r
             raise NameError('Container does not exist')\r
@@ -65,31 +86,45 @@ class BackEnd:
             raise Exception('Container is not empty')\r
         else:\r
             os.rmdir(fullname)\r
+            self.__del_dbpath(os.path.join(account, name))\r
         return\r
     \r
     def get_container_meta(self, account, name):\r
         """\r
         returns a dictionary with the container metadata\r
         """\r
-        self.logger.debug("get_container_meta: %s %s", account, name)\r
+        logger.debug("get_container_meta: %s %s", account, name)\r
         fullname = os.path.join(self.basepath, account, name)\r
         if not os.path.exists(fullname):\r
             raise NameError('Container does not exist')\r
-        contents = os.listdir(fullname) \r
+        contents = os.listdir(fullname)\r
         count = len(contents)\r
         size = sum(os.path.getsize(os.path.join(self.basepath, account, name, objectname)) for objectname in contents)\r
-        return {'name': name, 'count': count, 'bytes': size}\r
+        meta = self.__get_metadata(os.path.join(account, name))\r
+        meta.update({'name': name, 'count': count, 'bytes': size})\r
+        return meta\r
+    \r
+    def update_container_meta(self, account, name, meta):\r
+        """\r
+        updates the metadata associated with the container\r
+        """\r
+        logger.debug("update_container_meta: %s %s %s", account, name, meta)\r
+        fullname = os.path.join(self.basepath, account, name)\r
+        if not os.path.exists(fullname):\r
+            raise NameError('Container does not exist')\r
+        self.__put_metadata(os.path.join(account, name), meta)\r
+        return\r
     \r
     def list_containers(self, account, marker = None, limit = 10000):\r
         """\r
         returns a list of at most limit (default = 10000) containers \r
         starting from the next item after the optional marker\r
         """\r
-        self.logger.debug("list_containers: %s %s %s", account, marker, limit)\r
+        logger.debug("list_containers: %s %s %s", account, marker, limit)\r
         fullname = os.path.join(self.basepath, account)\r
         if not os.path.exists(fullname):\r
             raise NameError('Account does not exist')\r
-        containers = os.listdir(fullname)         \r
+        containers = os.listdir(fullname)\r
         start = 0\r
         if marker:\r
             try:\r
@@ -98,13 +133,63 @@ class BackEnd:
                 pass\r
         return containers[start:limit]\r
     \r
-    # --- UP TO HERE ---\r
+    \r
+    \r
+#     def __get_linkinfo(self, path):\r
+#         c = self.con.execute('select rowid from objects where name=''?''', (path,))\r
+#         row = c.fetchone()\r
+#         if row:\r
+#             return str(row[0])\r
+#         else:\r
+#             raise NameError('Requested path not found')\r
+#     \r
+#     def __put_linkinfo(self, path):\r
+#         id = self.con.execute('insert into objects (name) values (?)', (path,)).lastrowid\r
+#         self.con.commit()\r
+#         return id\r
+    \r
+    \r
+    \r
+    def __del_dbpath(self, path):\r
+        self.con.execute('delete from metadata where object_id in (select rowid from objects where name = ''?'')', (path,))\r
+        self.con.execute('delete from objects where name = ''?''', (path,))\r
+        self.con.commit()\r
+        return\r
+    \r
+    def __get_metadata(self, path):\r
+        c = self.con.execute('select m.name, m.value from metadata m, objects o where o.rowid = m.object_id and o.name = ''?''', (path,))\r
+        return dict(c.fetchall())\r
+    \r
+    def __put_metadata(self, path, meta):\r
+        c = self.con.execute('select rowid from objects where name=''?''', (path,))\r
+        row = c.fetchone()\r
+        if row:\r
+            link = str(row[0])\r
+        else:\r
+            link = self.con.execute('insert into objects (name) values (?)', (path,)).lastrowid      \r
+        for k, v in meta.iteritems():\r
+            self.con.execute('insert or replace into metadata (object_id, name, value) values (?, ?, ?)', (link, k, v))\r
+        self.con.commit()\r
+        return\r
+    \r
+    def __object_hash(self, location, block_size = 8192):\r
+        md5 = hashlib.md5()\r
+        f = open(location, 'r')\r
+        while True:\r
+            data = f.read(block_size)\r
+            if not data:\r
+                break\r
+            md5.update(data)\r
+        f.close()\r
+        return md5.hexdigest()\r
+    \r
+    # --- MERGED UP TO HERE ---\r
     \r
     def list_objects(self, account, container, prefix='', delimiter=None, marker = None, limit = 10000):\r
         """\r
         returns a list of the objects existing under a specific account container\r
         """\r
-        self.logger.info("list_objects: %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit)\r
+        logger.info("list_objects: %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit)\r
         dir = os.path.join(self.basepath, account, container)\r
         if not os.path.exists(dir):\r
             raise NameError('Container does not exist')\r
@@ -230,7 +315,7 @@ class BackEnd:
         f = open(location, 'w')\r
         f.write(data)\r
         f.close()\r
-        \r
+    \r
     def __delete_data(self, location, account, container):\r
         file = os.path.join(self.basepath, account, container, location)\r
         if not os.path.exists(dir):\r
index 6c6d97c..1569ff8 100644 (file)
@@ -75,13 +75,13 @@ SECRET_KEY = '$j0cdrfm*0sc2j+e@@2f-&3-_@2=^!z#+b-8o4_i10@2%ev7si'
 TEMPLATE_LOADERS = (\r
     'django.template.loaders.filesystem.Loader',\r
     'django.template.loaders.app_directories.Loader',\r
-#     'django.template.loaders.eggs.Loader',\r
+#    'django.template.loaders.eggs.Loader',\r
 )\r
 \r
 MIDDLEWARE_CLASSES = (\r
     'django.middleware.common.CommonMiddleware',\r
     'django.contrib.sessions.middleware.SessionMiddleware',\r
-    'django.middleware.csrf.CsrfViewMiddleware',\r
+#    'django.middleware.csrf.CsrfViewMiddleware',\r
     'django.contrib.auth.middleware.AuthenticationMiddleware',\r
     'django.contrib.messages.middleware.MessageMiddleware',\r
 )\r