Support public files.
authorAntony Chazapis <chazapis@gmail.com>
Thu, 9 Jun 2011 13:03:16 +0000 (16:03 +0300)
committerAntony Chazapis <chazapis@gmail.com>
Thu, 9 Jun 2011 13:03:16 +0000 (16:03 +0300)
Use 'X-Object-Public' to mark files as public. Then they can be accessed via the /public URL without authentication.

Refs #595

pithos/api/util.py
pithos/public/__init__.py [new file with mode: 0644]
pithos/public/functions.py [new file with mode: 0644]
pithos/public/models.py [new file with mode: 0644]
pithos/public/tests.py [new file with mode: 0644]
pithos/public/urls.py [new file with mode: 0644]
pithos/public/views.py [new file with mode: 0644]
pithos/settings.py.dist
pithos/urls.py

index 1dd48ab..3eec0b6 100644 (file)
@@ -116,6 +116,8 @@ def get_object_meta(request):
         meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
     if request.META.get('HTTP_X_OBJECT_MANIFEST'):
         meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
+    if request.META.get('HTTP_X_OBJECT_PUBLIC'):
+        meta['X-Object-Public'] = request.META['HTTP_X_OBJECT_PUBLIC']
     return meta
 
 def put_object_meta(response, meta):
@@ -128,7 +130,7 @@ def put_object_meta(response, meta):
     response['X-Object-Version-Timestamp'] = meta['version_timestamp']
     for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
         response[k.encode('utf-8')] = meta[k].encode('utf-8')
-    for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest'):
+    for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Public'):
         if k in meta:
             response[k] = meta[k]
 
diff --git a/pithos/public/__init__.py b/pithos/public/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/pithos/public/functions.py b/pithos/public/functions.py
new file mode 100644 (file)
index 0000000..21b62d1
--- /dev/null
@@ -0,0 +1,144 @@
+# Copyright 2011 GRNET S.A. 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.
+# 
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``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 GRNET S.A 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.
+# 
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+import logging
+import uuid
+
+from django.http import HttpResponse
+
+from pithos.api.faults import (Fault, BadRequest, ItemNotFound, RangeNotSatisfiable)
+from pithos.api.util import (put_object_meta, validate_modification_preconditions,
+    validate_matching_preconditions, get_range, ObjectWrapper, api_method)
+from pithos.backends import backend
+
+
+logger = logging.getLogger(__name__)
+
+
+def object_demux(request, v_account, v_container, v_object):
+    if request.method == 'HEAD':
+        return object_meta(request, v_account, v_container, v_object)
+    elif request.method == 'GET':
+        return object_read(request, v_account, v_container, v_object)
+    else:
+        return method_not_allowed(request)
+
+# TODO: Use a version of api_method that does not check for a token.
+
+@api_method('HEAD')
+def object_meta(request, v_account, v_container, v_object):
+    # Normal Response Codes: 204
+    # Error Response Codes: serviceUnavailable (503),
+    #                       itemNotFound (404),
+    #                       unauthorized (401),
+    #                       badRequest (400)
+    
+    try:
+        meta = backend.get_object_meta(request.user, v_container, v_object)
+    except NameError:
+        raise ItemNotFound('Object does not exist')
+    
+    if 'X-Object-Public' not in meta:
+        raise ItemNotFound('Object does not exist')
+    
+    response = HttpResponse(status=204)
+    put_object_meta(response, meta)
+    return response
+
+@api_method('GET')
+def object_read(request, v_account, v_container, v_object):
+    # Normal Response Codes: 200, 206
+    # Error Response Codes: serviceUnavailable (503),
+    #                       rangeNotSatisfiable (416),
+    #                       preconditionFailed (412),
+    #                       itemNotFound (404),
+    #                       unauthorized (401),
+    #                       badRequest (400),
+    #                       notModified (304)
+    
+    try:
+        meta = backend.get_object_meta(request.user, v_container, v_object)
+    except NameError:
+        raise ItemNotFound('Object does not exist')
+    
+    if 'X-Object-Public' not in meta:
+        raise ItemNotFound('Object does not exist')
+    
+    # Evaluate conditions.
+    validate_modification_preconditions(request, meta)
+    try:
+        validate_matching_preconditions(request, meta)
+    except NotModified:
+        response = HttpResponse(status=304)
+        response['ETag'] = meta['hash']
+        return response
+    
+    try:
+        size, hashmap = backend.get_object_hashmap(request.user, v_container, v_object)
+    except NameError:
+        raise ItemNotFound('Object does not exist')
+    
+    # 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
+
+@api_method()
+def method_not_allowed(request):
+    raise ItemNotFound('Object does not exist')
diff --git a/pithos/public/models.py b/pithos/public/models.py
new file mode 100644 (file)
index 0000000..71a8362
--- /dev/null
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/pithos/public/tests.py b/pithos/public/tests.py
new file mode 100644 (file)
index 0000000..2247054
--- /dev/null
@@ -0,0 +1,23 @@
+"""
+This file demonstrates two different styles of tests (one doctest and one
+unittest). These will both pass when you run "manage.py test".
+
+Replace these with more appropriate tests for your application.
+"""
+
+from django.test import TestCase
+
+class SimpleTest(TestCase):
+    def test_basic_addition(self):
+        """
+        Tests that 1 + 1 always equals 2.
+        """
+        self.failUnlessEqual(1 + 1, 2)
+
+__test__ = {"doctest": """
+Another way to test that 1 + 1 is equal to 2.
+
+>>> 1 + 1 == 2
+True
+"""}
+
diff --git a/pithos/public/urls.py b/pithos/public/urls.py
new file mode 100644 (file)
index 0000000..bc541d0
--- /dev/null
@@ -0,0 +1,42 @@
+# Copyright 2011 GRNET S.A. 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.
+# 
+# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``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 GRNET S.A 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.
+# 
+# The views and conclusions contained in the software and
+# documentation are those of the authors and should not be
+# interpreted as representing official policies, either expressed
+# or implied, of GRNET S.A.
+
+from django.conf.urls.defaults import *
+
+# TODO: This only works when in this order.
+urlpatterns = patterns('pithos.public.functions',
+    (r'^$', 'method_not_allowed'),
+    (r'^(?P<v_account>.+?)/(?P<v_container>.+?)/(?P<v_object>.+?)$', 'object_demux'),
+    (r'^(?P<v_account>.+?)/(?P<v_container>.+?)$', 'method_not_allowed'),
+    (r'^(?P<v_account>.+?)$', 'method_not_allowed')
+)
diff --git a/pithos/public/views.py b/pithos/public/views.py
new file mode 100644 (file)
index 0000000..60f00ef
--- /dev/null
@@ -0,0 +1 @@
+# Create your views here.
index cfb5001..6ea557e 100644 (file)
@@ -142,5 +142,6 @@ INSTALLED_APPS = (
     'django.contrib.messages',
 #    'django.contrib.admin',
 #    'django.contrib.admindocs',
-    'api'
+    'api',
+       'public'
 )
index b98241f..ab88434 100644 (file)
@@ -36,4 +36,5 @@ from django.conf.urls.defaults import *
 urlpatterns = patterns('',
     (r'^v1(?:$|/)', include('pithos.api.urls')),
     (r'^v1\.0(?:$|/)', include('pithos.api.urls')),
+    (r'^public(?:$|/)', include('pithos.public.urls')),
 )