Clean up, sort out logging.
authorAntony Chazapis <chazapis@gmail.com>
Thu, 5 May 2011 10:32:57 +0000 (13:32 +0300)
committerAntony Chazapis <chazapis@gmail.com>
Thu, 5 May 2011 10:32:57 +0000 (13:32 +0300)
pithos/api/compat.py [new file with mode: 0644]
pithos/api/functions.py
pithos/api/urls.py
pithos/api/util.py
pithos/backends/dummy.py
pithos/middleware/__init__.py [new file with mode: 0644]

diff --git a/pithos/api/compat.py b/pithos/api/compat.py
new file mode 100644 (file)
index 0000000..5c4bb84
--- /dev/null
@@ -0,0 +1,86 @@
+# Copyright (c) Django Software Foundation and individual contributors.
+# All rights reserved.
+# 
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+# 
+#     1. Redistributions of source code must retain the above copyright notice, 
+#        this list of conditions and the following disclaimer.
+#     
+#     2. Redistributions in binary form must reproduce the above copyright 
+#        notice, this list of conditions and the following disclaimer in the
+#        documentation and/or other materials provided with the distribution.
+# 
+#     3. Neither the name of Django nor the names of its contributors may be used
+#        to endorse or promote products derived from this software without
+#        specific prior written permission.
+# 
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import re
+import datetime
+import calendar
+
+__D = r'(?P<day>\d{2})'
+__D2 = r'(?P<day>[ \d]\d)'
+__M = r'(?P<mon>\w{3})'
+__Y = r'(?P<year>\d{4})'
+__Y2 = r'(?P<year>\d{2})'
+__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
+RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))
+RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))
+ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
+
+def parse_http_date(date):
+    """
+    Parses a date format as specified by HTTP RFC2616 section 3.3.1.
+
+    The three formats allowed by the RFC are accepted, even if only the first
+    one is still in widespread use.
+
+    Returns an floating point number expressed in seconds since the epoch, in
+    UTC.
+    """
+    # emails.Util.parsedate does the job for RFC1123 dates; unfortunately
+    # RFC2616 makes it mandatory to support RFC850 dates too. So we roll
+    # our own RFC-compliant parsing.
+    for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
+        m = regex.match(date)
+        if m is not None:
+            break
+    else:
+        raise ValueError("%r is not in a valid HTTP date format" % date)
+    try:
+        year = int(m.group('year'))
+        if year < 100:
+            if year < 70:
+                year += 2000
+            else:
+                year += 1900
+        month = MONTHS.index(m.group('mon').lower()) + 1
+        day = int(m.group('day'))
+        hour = int(m.group('hour'))
+        min = int(m.group('min'))
+        sec = int(m.group('sec'))
+        result = datetime.datetime(year, month, day, hour, min, sec)
+        return calendar.timegm(result.utctimetuple())
+    except Exception:
+        raise ValueError("%r is not a valid date" % date)
+
+def parse_http_date_safe(date):
+    """
+    Same as parse_http_date, but returns None if the input is invalid.
+    """
+    try:
+        return parse_http_date(date)
+    except Exception:
+        pass
index ff3e1c9..e852459 100644 (file)
@@ -10,20 +10,20 @@ from django.utils.http import http_date, parse_etags
 try:\r
     from django.utils.http import parse_http_date_safe\r
 except:\r
-    from pithos.api.util import parse_http_date_safe\r
+    from pithos.api.compat import parse_http_date_safe\r
 \r
 from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity\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
-STORAGE_PATH = path.join(PROJECT_PATH, 'data')\r
-\r
 from pithos.backends.dummy import BackEnd\r
 \r
+import os\r
+import datetime\r
 import logging\r
 \r
-logging.basicConfig(level=logging.INFO)\r
+from settings import PROJECT_PATH\r
+STORAGE_PATH = os.path.join(PROJECT_PATH, 'data')\r
+\r
+logger = logging.getLogger(__name__)\r
 \r
 @api_method('GET')\r
 def authenticate(request):\r
@@ -39,9 +39,8 @@ def authenticate(request):
         raise BadRequest('Missing auth user or key.')\r
     \r
     response = HttpResponse(status = 204)\r
-    response['X-Auth-Token'] = 'eaaafd18-0fed-4b3a-81b4-663c99ec1cbb'\r
-    # TODO: Must support X-Storage-Url to be compatible.\r
-    response['X-Storage-Url'] = 'http://127.0.0.1:8000/v1/asdf'\r
+    response['X-Auth-Token'] = '0000'\r
+    response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')\r
     return response\r
 \r
 def account_demux(request, v_account):\r
@@ -141,11 +140,11 @@ 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
     if request.serialization == 'text':\r
+        if len(containers) == 0:\r
+            # The cloudfiles python bindings expect 200 if json/xml.\r
+            return HttpResponse(status = 204)\r
         return HttpResponse('\n'.join(containers), status = 200)\r
     \r
     # TODO: Do this with a backend parameter?\r
@@ -153,7 +152,6 @@ def container_list(request, v_account):
         containers = [be.get_container_meta(request.user, x) for x in containers]\r
     except NameError:\r
         raise ItemNotFound()\r
-    # TODO: Format dates.\r
     if request.serialization == 'xml':\r
         data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})\r
     elif request.serialization  == 'json':\r
@@ -233,18 +231,11 @@ def container_delete(request, v_account, v_container):
     \r
     be = BackEnd(STORAGE_PATH)\r
     try:\r
-        info = be.get_container_meta(request.user, v_container)\r
+        be.delete_container(request.user, v_container)\r
     except NameError:\r
         raise ItemNotFound()\r
-    \r
-    if info['count'] > 0:\r
+    except IndexError:\r
         raise Conflict()\r
-    \r
-    # TODO: Handle both exceptions.\r
-    try:\r
-        be.delete_container(request.user, v_container)\r
-    except:\r
-        raise ItemNotFound()\r
     return HttpResponse(status = 204)\r
 \r
 @api_method('GET', format_allowed = True)\r
@@ -259,6 +250,7 @@ def object_list(request, v_account, v_container):
     prefix = request.GET.get('prefix')\r
     delimiter = request.GET.get('delimiter')\r
     \r
+    # TODO: Check if the cloudfiles python bindings expect the results with the prefix.\r
     # Path overrides prefix and delimiter.\r
     if path:\r
         prefix = path\r
@@ -282,11 +274,11 @@ def object_list(request, v_account, v_container):
         objects = be.list_objects(request.user, v_container, prefix, delimiter, marker, limit)\r
     except NameError:\r
         raise ItemNotFound()\r
-    # TODO: The cloudfiles python bindings expect 200 if json/xml.\r
-    if len(objects) == 0:\r
-        return HttpResponse(status = 204)\r
     \r
     if request.serialization == 'text':\r
+        if len(objects) == 0:\r
+            # The cloudfiles python bindings expect 200 if json/xml.\r
+            return HttpResponse(status = 204)\r
         return HttpResponse('\n'.join(objects), status = 200)\r
     \r
     # TODO: Do this with a backend parameter?\r
@@ -294,7 +286,10 @@ def object_list(request, v_account, v_container):
         objects = [be.get_object_meta(request.user, v_container, x) for x in objects]\r
     except NameError:\r
         raise ItemNotFound()\r
-    # TODO: Format dates.\r
+    # Format dates.\r
+    for x in objects:\r
+        if x.has_key('last_modified'):\r
+            x['last_modified'] = datetime.datetime.fromtimestamp(x['last_modified']).isoformat()\r
     if request.serialization == 'xml':\r
         data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})\r
     elif request.serialization  == 'json':\r
@@ -320,7 +315,6 @@ def object_meta(request, v_account, v_container, v_object):
     response['Content-Length'] = info['bytes']\r
     response['Content-Type'] = info['content_type']\r
     response['Last-Modified'] = http_date(info['last_modified'])\r
-    # TODO: How should these be encoded for non-ascii?\r
     for k in [x for x in info.keys() if x.startswith('X-Object-Meta-')]:\r
         response[k.encode('utf-8')] = info[k].encode('utf-8')\r
     \r
index 15c6190..3c6dce3 100644 (file)
@@ -5,7 +5,6 @@
 from django.conf.urls.defaults import *\r
 \r
 # TODO: This only works when in this order.\r
-# TODO: Define which characters can be used in each "path" component.\r
 urlpatterns = patterns('pithos.api.functions',\r
     (r'^$', 'authenticate'),\r
     (r'^(?P<v_account>.+?)/(?P<v_container>.+?)/(?P<v_object>.+?)$', 'object_demux'),\r
index c2f07a4..2532205 100644 (file)
 # Copyright (c) 2011 Greek Research and Technology Network\r
 #\r
 \r
-from datetime import timedelta, tzinfo\r
 from functools import wraps\r
-from random import choice\r
-from string import ascii_letters, digits\r
+\r
 from time import time\r
-from traceback import format_exc\r
 from wsgiref.handlers import format_date_time\r
 \r
 from django.conf import settings\r
 from django.http import HttpResponse\r
-from django.template.loader import render_to_string\r
-from django.utils import simplejson as json\r
 \r
-from pithos.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable\r
-#from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata\r
+from pithos.api.faults import Fault, BadRequest, ServiceUnavailable\r
 \r
 import datetime\r
-import dateutil.parser\r
 import logging\r
 \r
-import re\r
-import calendar\r
-\r
-# Part of newer Django versions.\r
-\r
-__D = r'(?P<day>\d{2})'\r
-__D2 = r'(?P<day>[ \d]\d)'\r
-__M = r'(?P<mon>\w{3})'\r
-__Y = r'(?P<year>\d{4})'\r
-__Y2 = r'(?P<year>\d{2})'\r
-__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'\r
-RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))\r
-RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))\r
-ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))\r
-\r
-def parse_http_date(date):\r
-    """\r
-    Parses a date format as specified by HTTP RFC2616 section 3.3.1.\r
-\r
-    The three formats allowed by the RFC are accepted, even if only the first\r
-    one is still in widespread use.\r
-\r
-    Returns an floating point number expressed in seconds since the epoch, in\r
-    UTC.\r
-    """\r
-    # emails.Util.parsedate does the job for RFC1123 dates; unfortunately\r
-    # RFC2616 makes it mandatory to support RFC850 dates too. So we roll\r
-    # our own RFC-compliant parsing.\r
-    for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:\r
-        m = regex.match(date)\r
-        if m is not None:\r
-            break\r
-    else:\r
-        raise ValueError("%r is not in a valid HTTP date format" % date)\r
-    try:\r
-        year = int(m.group('year'))\r
-        if year < 100:\r
-            if year < 70:\r
-                year += 2000\r
-            else:\r
-                year += 1900\r
-        month = MONTHS.index(m.group('mon').lower()) + 1\r
-        day = int(m.group('day'))\r
-        hour = int(m.group('hour'))\r
-        min = int(m.group('min'))\r
-        sec = int(m.group('sec'))\r
-        result = datetime.datetime(year, month, day, hour, min, sec)\r
-        return calendar.timegm(result.utctimetuple())\r
-    except Exception:\r
-        raise ValueError("%r is not a valid date" % date)\r
-\r
-def parse_http_date_safe(date):\r
+def format_meta_key(k):\r
     """\r
-    Same as parse_http_date, but returns None if the input is invalid.\r
+    Convert underscores to dashes and capitalize intra-dash strings.\r
     """\r
-    try:\r
-        return parse_http_date(date)\r
-    except Exception:\r
-        pass\r
-\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 prefix-* request headers in a dict.\r
-    All underscores are converted to dashes.\r
+    Get all prefix-* request headers in a dict. Reformat keys with format_meta_key().\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
@@ -139,57 +70,6 @@ def get_range(request):
     \r
     return (offset, length)\r
 \r
-# def get_vm(server_id):\r
-#     """Return a VirtualMachine instance or raise ItemNotFound."""\r
-#     \r
-#     try:\r
-#         server_id = int(server_id)\r
-#         return VirtualMachine.objects.get(id=server_id)\r
-#     except ValueError:\r
-#         raise BadRequest('Invalid server ID.')\r
-#     except VirtualMachine.DoesNotExist:\r
-#         raise ItemNotFound('Server not found.')\r
-# \r
-# def get_vm_meta(server_id, key):\r
-#     """Return a VirtualMachineMetadata instance or raise ItemNotFound."""\r
-#     \r
-#     try:\r
-#         server_id = int(server_id)\r
-#         return VirtualMachineMetadata.objects.get(meta_key=key, vm=server_id)\r
-#     except VirtualMachineMetadata.DoesNotExist:\r
-#         raise ItemNotFound('Metadata key not found.')\r
-# \r
-# def get_image(image_id):\r
-#     """Return an Image instance or raise ItemNotFound."""\r
-#     \r
-#     try:\r
-#         image_id = int(image_id)\r
-#         return Image.objects.get(id=image_id)\r
-#     except Image.DoesNotExist:\r
-#         raise ItemNotFound('Image not found.')\r
-# \r
-# def get_image_meta(image_id, key):\r
-#     """Return a ImageMetadata instance or raise ItemNotFound."""\r
-# \r
-#     try:\r
-#         image_id = int(image_id)\r
-#         return ImageMetadata.objects.get(meta_key=key, image=image_id)\r
-#     except ImageMetadata.DoesNotExist:\r
-#         raise ItemNotFound('Metadata key not found.')\r
-# \r
-# \r
-# def get_request_dict(request):\r
-#     """Returns data sent by the client as a python dict."""\r
-#     \r
-#     data = request.raw_post_data\r
-#     if request.META.get('CONTENT_TYPE').startswith('application/json'):\r
-#         try:\r
-#             return json.loads(data)\r
-#         except ValueError:\r
-#             raise BadRequest('Invalid JSON data.')\r
-#     else:\r
-#         raise BadRequest('Unsupported Content-Type.')\r
-\r
 def update_response_headers(request, response):\r
     if request.serialization == 'xml':\r
         response['Content-Type'] = 'application/xml; charset=UTF-8'\r
@@ -202,19 +82,9 @@ def update_response_headers(request, response):
         response['Date'] = format_date_time(time())\r
 \r
 def render_fault(request, fault):\r
-    if settings.DEBUG or settings.TEST:\r
-        fault.details = format_exc(fault)\r
-    \r
-#     if request.serialization == 'xml':\r
-#         data = render_to_string('fault.xml', {'fault': fault})\r
-#     else:\r
-#         d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}\r
-#         data = json.dumps(d)\r
-    \r
-#     resp = HttpResponse(data, status=fault.code)\r
-    resp = HttpResponse(status = fault.code)\r
-    update_response_headers(request, resp)\r
-    return resp\r
+    response = HttpResponse(status = fault.code)\r
+    update_response_headers(request, response)\r
+    return response\r
 \r
 def request_serialization(request, format_allowed=False):\r
     """\r
@@ -232,13 +102,14 @@ def request_serialization(request, format_allowed=False):
     elif format == 'xml':\r
         return 'xml'\r
     \r
-    # TODO: Do we care of Accept headers?\r
-#     for item in request.META.get('HTTP_ACCEPT', '').split(','):\r
-#         accept, sep, rest = item.strip().partition(';')\r
-#         if accept == 'application/json':\r
-#             return 'json'\r
-#         elif accept == 'application/xml':\r
-#             return 'xml'\r
+    for item in request.META.get('HTTP_ACCEPT', '').split(','):\r
+        accept, sep, rest = item.strip().partition(';')\r
+        if accept == 'text/plain':\r
+            return 'text'\r
+        elif accept == 'application/json':\r
+            return 'json'\r
+        elif accept == 'application/xml' or accept == 'text/xml':\r
+            return 'xml'\r
     \r
     return 'text'\r
 \r
@@ -251,18 +122,23 @@ def api_method(http_method = None, format_allowed = False):
         @wraps(func)\r
         def wrapper(request, *args, **kwargs):\r
             try:\r
+                if http_method and request.method != http_method:\r
+                    raise BadRequest('Method not allowed.')\r
+\r
+                # The args variable may contain up to (account, container, object).\r
+                if len(args) > 1 and len(args[1]) > 256:\r
+                    raise BadRequest('Container name too large.')\r
+                if len(args) > 2 and len(args[2]) > 1024:\r
+                    raise BadRequest('Object name too large.')\r
+                \r
+                # Fill in custom request variables.\r
                 request.serialization = request_serialization(request, format_allowed)\r
                 # TODO: Authenticate.\r
-                # TODO: Return 401/404 when the account is not found.\r
                 request.user = "test"\r
-                # TODO: Check parameter sizes.\r
-                if http_method and request.method != http_method:\r
-                    raise BadRequest('Method not allowed.')\r
                 \r
-                resp = func(request, *args, **kwargs)\r
-                update_response_headers(request, resp)\r
-                return resp\r
-            \r
+                response = func(request, *args, **kwargs)\r
+                update_response_headers(request, response)\r
+                return response\r
             except Fault, fault:\r
                 return render_fault(request, fault)\r
             except BaseException, e:\r
index d06b91c..efaa99e 100644 (file)
@@ -7,21 +7,12 @@ import hashlib
 import shutil\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
+    def __init__(self, basepath):\r
         self.basepath = basepath\r
         \r
-        # TODO: Manage log_file.\r
-        logger.setLevel(log_level)\r
-        \r
         if not os.path.exists(basepath):\r
             os.makedirs(basepath)\r
         db = os.path.join(basepath, 'db')\r
@@ -85,7 +76,7 @@ class BackEnd:
         if not os.path.exists(fullname):\r
             raise NameError('Container does not exist')\r
         if os.listdir(fullname):\r
-            raise Exception('Container is not empty')\r
+            raise IndexError('Container is not empty')\r
         else:\r
             os.rmdir(fullname)\r
             self.__del_dbpath(os.path.join(account, name))\r
@@ -155,15 +146,16 @@ class BackEnd:
         c = self.con.execute('select * from objects where name like ''?'' order by name', (os.path.join(prefix, '%'),))\r
         objects = [x[0][len(prefix):] for x in c.fetchall()]\r
         if delimiter:\r
-            pseudo_objects = {}\r
+            pseudo_objects = []\r
             for x in objects:\r
                 pseudo_name = x\r
                 i = pseudo_name.find(delimiter)\r
                 if i != -1:\r
                     pseudo_name = pseudo_name[:i]\r
+                if pseudo_name not in pseudo_objects:\r
+                    pseudo_objects.append(pseudo_name)\r
                 # TODO: Virtual directories.\r
-                pseudo_objects[pseudo_name] = x\r
-            objects = pseudo_objects.keys()\r
+            objects = pseudo_objects\r
         \r
         start = 0\r
         if marker:\r
diff --git a/pithos/middleware/__init__.py b/pithos/middleware/__init__.py
new file mode 100644 (file)
index 0000000..c40dcde
--- /dev/null
@@ -0,0 +1,17 @@
+from django.conf import settings
+from django.core.exceptions import MiddlewareNotUsed
+
+import logging
+import logging.handlers
+import logging.config
+
+__all__ = ('LoggingConfigMiddleware',)
+
+class LoggingConfigMiddleware:
+    def __init__(self):
+        '''Initialise the logging setup from settings, called on first request.'''
+        if getattr(settings, 'DEBUG', False):
+            logging.basicConfig(level = logging.DEBUG, format = '%(asctime)s [%(levelname)s] %(name)s %(message)s', datefmt = '%Y-%m-%d %H:%M:%S')
+        else:
+            logging.basicConfig(level = logging.INFO, format = '%(asctime)s [%(levelname)s] %(name)s %(message)s', datefmt = '%Y-%m-%d %H:%M:%S')
+        raise MiddlewareNotUsed('Logging setup only.')