Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 038f1ae9

History | View | Annotate | Download (27.4 kB)

1 5635f9ef Antony Chazapis
# Copyright 2011 GRNET S.A. All rights reserved.
2 5635f9ef Antony Chazapis
# 
3 5635f9ef Antony Chazapis
# Redistribution and use in source and binary forms, with or
4 5635f9ef Antony Chazapis
# without modification, are permitted provided that the following
5 5635f9ef Antony Chazapis
# conditions are met:
6 5635f9ef Antony Chazapis
# 
7 5635f9ef Antony Chazapis
#   1. Redistributions of source code must retain the above
8 5635f9ef Antony Chazapis
#      copyright notice, this list of conditions and the following
9 5635f9ef Antony Chazapis
#      disclaimer.
10 5635f9ef Antony Chazapis
# 
11 5635f9ef Antony Chazapis
#   2. Redistributions in binary form must reproduce the above
12 5635f9ef Antony Chazapis
#      copyright notice, this list of conditions and the following
13 5635f9ef Antony Chazapis
#      disclaimer in the documentation and/or other materials
14 5635f9ef Antony Chazapis
#      provided with the distribution.
15 5635f9ef Antony Chazapis
# 
16 5635f9ef Antony Chazapis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 5635f9ef Antony Chazapis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 5635f9ef Antony Chazapis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 5635f9ef Antony Chazapis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 5635f9ef Antony Chazapis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 5635f9ef Antony Chazapis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 5635f9ef Antony Chazapis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 5635f9ef Antony Chazapis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 5635f9ef Antony Chazapis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 5635f9ef Antony Chazapis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 5635f9ef Antony Chazapis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 5635f9ef Antony Chazapis
# POSSIBILITY OF SUCH DAMAGE.
28 5635f9ef Antony Chazapis
# 
29 5635f9ef Antony Chazapis
# The views and conclusions contained in the software and
30 5635f9ef Antony Chazapis
# documentation are those of the authors and should not be
31 5635f9ef Antony Chazapis
# interpreted as representing official policies, either expressed
32 5635f9ef Antony Chazapis
# or implied, of GRNET S.A.
33 5635f9ef Antony Chazapis
34 b956618e Antony Chazapis
from functools import wraps
35 b956618e Antony Chazapis
from time import time
36 b956618e Antony Chazapis
from traceback import format_exc
37 b956618e Antony Chazapis
from wsgiref.handlers import format_date_time
38 49133392 Antony Chazapis
from binascii import hexlify
39 b956618e Antony Chazapis
40 b956618e Antony Chazapis
from django.conf import settings
41 b956618e Antony Chazapis
from django.http import HttpResponse
42 1993fea9 Antony Chazapis
from django.utils import simplejson as json
43 22dab079 Antony Chazapis
from django.utils.http import http_date, parse_etags
44 b956618e Antony Chazapis
45 6d1e6dce Sofia Papagiannaki
from pithos.api.compat import parse_http_date_safe, parse_http_date
46 02c0c3fa Antony Chazapis
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound,
47 1c7fc132 Antony Chazapis
                                Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable,
48 02c0c3fa Antony Chazapis
                                ServiceUnavailable)
49 b956618e Antony Chazapis
from pithos.backends import backend
50 cca6c617 Antony Chazapis
from pithos.backends.base import NotAllowedError
51 b956618e Antony Chazapis
52 b956618e Antony Chazapis
import datetime
53 b956618e Antony Chazapis
import logging
54 22dab079 Antony Chazapis
import re
55 cbfb6636 Sofia Papagiannaki
import hashlib
56 7bef5750 Antony Chazapis
import uuid
57 b956618e Antony Chazapis
58 b956618e Antony Chazapis
59 b956618e Antony Chazapis
logger = logging.getLogger(__name__)
60 b956618e Antony Chazapis
61 b956618e Antony Chazapis
62 02c0c3fa Antony Chazapis
def printable_header_dict(d):
63 b956618e Antony Chazapis
    """Format a meta dictionary for printing out json/xml.
64 b956618e Antony Chazapis
    
65 b956618e Antony Chazapis
    Convert all keys to lower case and replace dashes to underscores.
66 b956618e Antony Chazapis
    Change 'modified' key from backend to 'last_modified' and format date.
67 b956618e Antony Chazapis
    """
68 83dd59c5 Antony Chazapis
    
69 b956618e Antony Chazapis
    if 'modified' in d:
70 b956618e Antony Chazapis
        d['last_modified'] = datetime.datetime.fromtimestamp(int(d['modified'])).isoformat()
71 b956618e Antony Chazapis
        del(d['modified'])
72 b956618e Antony Chazapis
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
73 b956618e Antony Chazapis
74 02c0c3fa Antony Chazapis
def format_header_key(k):
75 58a6c894 Antony Chazapis
    """Convert underscores to dashes and capitalize intra-dash strings."""
76 83dd59c5 Antony Chazapis
    
77 b956618e Antony Chazapis
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
78 b956618e Antony Chazapis
79 02c0c3fa Antony Chazapis
def get_header_prefix(request, prefix):
80 02c0c3fa Antony Chazapis
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
81 83dd59c5 Antony Chazapis
    
82 b956618e Antony Chazapis
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
83 3ab38c43 Antony Chazapis
    # TODO: Document or remove '~' replacing.
84 3ab38c43 Antony Chazapis
    return dict([(format_header_key(k[5:]), v.replace('~', '')) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
85 02c0c3fa Antony Chazapis
86 02c0c3fa Antony Chazapis
def get_account_headers(request):
87 02c0c3fa Antony Chazapis
    meta = get_header_prefix(request, 'X-Account-Meta-')
88 02c0c3fa Antony Chazapis
    groups = {}
89 02c0c3fa Antony Chazapis
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
90 02c0c3fa Antony Chazapis
        n = k[16:].lower()
91 02c0c3fa Antony Chazapis
        if '-' in n or '_' in n:
92 02c0c3fa Antony Chazapis
            raise BadRequest('Bad characters in group name')
93 02c0c3fa Antony Chazapis
        groups[n] = v.replace(' ', '').split(',')
94 02c0c3fa Antony Chazapis
        if '' in groups[n]:
95 02c0c3fa Antony Chazapis
            groups[n].remove('')
96 02c0c3fa Antony Chazapis
    return meta, groups
97 02c0c3fa Antony Chazapis
98 02c0c3fa Antony Chazapis
def put_account_headers(response, meta, groups):
99 b956618e Antony Chazapis
    response['X-Account-Container-Count'] = meta['count']
100 b956618e Antony Chazapis
    response['X-Account-Bytes-Used'] = meta['bytes']
101 b956618e Antony Chazapis
    if 'modified' in meta:
102 b956618e Antony Chazapis
        response['Last-Modified'] = http_date(int(meta['modified']))
103 b956618e Antony Chazapis
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
104 b956618e Antony Chazapis
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
105 83dd59c5 Antony Chazapis
    if 'until_timestamp' in meta:
106 83dd59c5 Antony Chazapis
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
107 02c0c3fa Antony Chazapis
    for k, v in groups.iteritems():
108 02c0c3fa Antony Chazapis
        response[format_header_key('X-Account-Group-' + k).encode('utf-8')] = (','.join(v)).encode('utf-8')
109 b956618e Antony Chazapis
110 02c0c3fa Antony Chazapis
def get_container_headers(request):
111 02c0c3fa Antony Chazapis
    meta = get_header_prefix(request, 'X-Container-Meta-')
112 3ab38c43 Antony Chazapis
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
113 3ab38c43 Antony Chazapis
    return meta, policy
114 b956618e Antony Chazapis
115 3ab38c43 Antony Chazapis
def put_container_headers(response, meta, policy):
116 b956618e Antony Chazapis
    response['X-Container-Object-Count'] = meta['count']
117 b956618e Antony Chazapis
    response['X-Container-Bytes-Used'] = meta['bytes']
118 58a6c894 Antony Chazapis
    response['Last-Modified'] = http_date(int(meta['modified']))
119 b956618e Antony Chazapis
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
120 b956618e Antony Chazapis
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
121 22dab079 Antony Chazapis
    response['X-Container-Object-Meta'] = [x[14:] for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
122 6bc372a4 Antony Chazapis
    response['X-Container-Block-Size'] = backend.block_size
123 6bc372a4 Antony Chazapis
    response['X-Container-Block-Hash'] = backend.hash_algorithm
124 83dd59c5 Antony Chazapis
    if 'until_timestamp' in meta:
125 83dd59c5 Antony Chazapis
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
126 3ab38c43 Antony Chazapis
    for k, v in policy.iteritems():
127 3ab38c43 Antony Chazapis
        response[format_header_key('X-Container-Policy-' + k).encode('utf-8')] = v.encode('utf-8')
128 b956618e Antony Chazapis
129 02c0c3fa Antony Chazapis
def get_object_headers(request):
130 02c0c3fa Antony Chazapis
    meta = get_header_prefix(request, 'X-Object-Meta-')
131 b956618e Antony Chazapis
    if request.META.get('CONTENT_TYPE'):
132 b956618e Antony Chazapis
        meta['Content-Type'] = request.META['CONTENT_TYPE']
133 b956618e Antony Chazapis
    if request.META.get('HTTP_CONTENT_ENCODING'):
134 b956618e Antony Chazapis
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
135 22dab079 Antony Chazapis
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
136 22dab079 Antony Chazapis
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
137 b956618e Antony Chazapis
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
138 b956618e Antony Chazapis
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
139 3ab38c43 Antony Chazapis
    return meta, get_sharing(request), get_public(request)
140 b956618e Antony Chazapis
141 3ab38c43 Antony Chazapis
def put_object_headers(response, meta, restricted=False):
142 b956618e Antony Chazapis
    response['ETag'] = meta['hash']
143 b956618e Antony Chazapis
    response['Content-Length'] = meta['bytes']
144 b956618e Antony Chazapis
    response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
145 b956618e Antony Chazapis
    response['Last-Modified'] = http_date(int(meta['modified']))
146 3ab38c43 Antony Chazapis
    if not restricted:
147 104626e3 Antony Chazapis
        response['X-Object-Modified-By'] = meta['modified_by']
148 7bef5750 Antony Chazapis
        response['X-Object-Version'] = meta['version']
149 104626e3 Antony Chazapis
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
150 7bef5750 Antony Chazapis
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
151 7bef5750 Antony Chazapis
            response[k.encode('utf-8')] = meta[k].encode('utf-8')
152 e0f916bb Antony Chazapis
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Public'):
153 7bef5750 Antony Chazapis
            if k in meta:
154 7bef5750 Antony Chazapis
                response[k] = meta[k]
155 7bef5750 Antony Chazapis
    else:
156 c9af0703 Antony Chazapis
        for k in ('Content-Encoding', 'Content-Disposition'):
157 7bef5750 Antony Chazapis
            if k in meta:
158 7bef5750 Antony Chazapis
                response[k] = meta[k]
159 b956618e Antony Chazapis
160 8cb45c13 Antony Chazapis
def update_manifest_meta(request, v_account, meta):
161 8cb45c13 Antony Chazapis
    """Update metadata if the object has an X-Object-Manifest."""
162 8cb45c13 Antony Chazapis
    
163 8cb45c13 Antony Chazapis
    if 'X-Object-Manifest' in meta:
164 8cb45c13 Antony Chazapis
        hash = ''
165 8cb45c13 Antony Chazapis
        bytes = 0
166 8cb45c13 Antony Chazapis
        try:
167 6d817842 Antony Chazapis
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
168 8cb45c13 Antony Chazapis
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
169 8cb45c13 Antony Chazapis
            for x in objects:
170 8cb45c13 Antony Chazapis
                src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
171 8cb45c13 Antony Chazapis
                hash += src_meta['hash']
172 8cb45c13 Antony Chazapis
                bytes += src_meta['bytes']
173 8cb45c13 Antony Chazapis
        except:
174 8cb45c13 Antony Chazapis
            # Ignore errors.
175 8cb45c13 Antony Chazapis
            return
176 8cb45c13 Antony Chazapis
        meta['bytes'] = bytes
177 8cb45c13 Antony Chazapis
        md5 = hashlib.md5()
178 8cb45c13 Antony Chazapis
        md5.update(hash)
179 8cb45c13 Antony Chazapis
        meta['hash'] = md5.hexdigest().lower()
180 8cb45c13 Antony Chazapis
181 cca6c617 Antony Chazapis
def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
182 cca6c617 Antony Chazapis
    if permissions is None:
183 cca6c617 Antony Chazapis
        return
184 cca6c617 Antony Chazapis
    perm_path, perms = permissions
185 cca6c617 Antony Chazapis
    if len(perms) == 0:
186 cca6c617 Antony Chazapis
        return
187 3436eeb0 Antony Chazapis
    ret = []
188 cca6c617 Antony Chazapis
    r = ','.join(perms.get('read', []))
189 3436eeb0 Antony Chazapis
    if r:
190 3436eeb0 Antony Chazapis
        ret.append('read=' + r)
191 cca6c617 Antony Chazapis
    w = ','.join(perms.get('write', []))
192 3436eeb0 Antony Chazapis
    if w:
193 3436eeb0 Antony Chazapis
        ret.append('write=' + w)
194 cca6c617 Antony Chazapis
    meta['X-Object-Sharing'] = '; '.join(ret)
195 cca6c617 Antony Chazapis
    if '/'.join((v_account, v_container, v_object)) != perm_path:
196 cca6c617 Antony Chazapis
        meta['X-Object-Shared-By'] = perm_path
197 3436eeb0 Antony Chazapis
198 e0f916bb Antony Chazapis
def update_public_meta(public, meta):
199 e0f916bb Antony Chazapis
    if not public:
200 e0f916bb Antony Chazapis
        return
201 e0f916bb Antony Chazapis
    meta['X-Object-Public'] = public
202 e0f916bb Antony Chazapis
203 b956618e Antony Chazapis
def validate_modification_preconditions(request, meta):
204 58a6c894 Antony Chazapis
    """Check that the modified timestamp conforms with the preconditions set."""
205 83dd59c5 Antony Chazapis
    
206 22dab079 Antony Chazapis
    if 'modified' not in meta:
207 22dab079 Antony Chazapis
        return # TODO: Always return?
208 22dab079 Antony Chazapis
    
209 b956618e Antony Chazapis
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
210 b956618e Antony Chazapis
    if if_modified_since is not None:
211 b956618e Antony Chazapis
        if_modified_since = parse_http_date_safe(if_modified_since)
212 22dab079 Antony Chazapis
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
213 3e5d2f38 Antony Chazapis
        raise NotModified('Resource has not been modified')
214 b956618e Antony Chazapis
    
215 b956618e Antony Chazapis
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
216 b956618e Antony Chazapis
    if if_unmodified_since is not None:
217 b956618e Antony Chazapis
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
218 22dab079 Antony Chazapis
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
219 3e5d2f38 Antony Chazapis
        raise PreconditionFailed('Resource has been modified')
220 b956618e Antony Chazapis
221 22dab079 Antony Chazapis
def validate_matching_preconditions(request, meta):
222 58a6c894 Antony Chazapis
    """Check that the ETag conforms with the preconditions set."""
223 83dd59c5 Antony Chazapis
    
224 22dab079 Antony Chazapis
    if 'hash' not in meta:
225 22dab079 Antony Chazapis
        return # TODO: Always return?
226 22dab079 Antony Chazapis
    
227 22dab079 Antony Chazapis
    if_match = request.META.get('HTTP_IF_MATCH')
228 22dab079 Antony Chazapis
    if if_match is not None and if_match != '*':
229 22dab079 Antony Chazapis
        if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
230 3e5d2f38 Antony Chazapis
            raise PreconditionFailed('Resource Etag does not match')
231 22dab079 Antony Chazapis
    
232 22dab079 Antony Chazapis
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
233 22dab079 Antony Chazapis
    if if_none_match is not None:
234 22dab079 Antony Chazapis
        if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
235 3e5d2f38 Antony Chazapis
            raise NotModified('Resource Etag matches')
236 22dab079 Antony Chazapis
237 83dd59c5 Antony Chazapis
def split_container_object_string(s):
238 6d817842 Antony Chazapis
    if not len(s) > 0 or s[0] != '/':
239 6d817842 Antony Chazapis
        raise ValueError
240 6d817842 Antony Chazapis
    s = s[1:]
241 8cb45c13 Antony Chazapis
    pos = s.find('/')
242 8cb45c13 Antony Chazapis
    if pos == -1:
243 83dd59c5 Antony Chazapis
        raise ValueError
244 8cb45c13 Antony Chazapis
    return s[:pos], s[(pos + 1):]
245 83dd59c5 Antony Chazapis
246 83dd59c5 Antony Chazapis
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
247 58a6c894 Antony Chazapis
    """Copy or move an object."""
248 22dab079 Antony Chazapis
    
249 3ab38c43 Antony Chazapis
    meta, permissions, public = get_object_headers(request)
250 a6eb13e9 Antony Chazapis
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
251 b956618e Antony Chazapis
    try:
252 b956618e Antony Chazapis
        if move:
253 a6eb13e9 Antony Chazapis
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
254 b956618e Antony Chazapis
        else:
255 a6eb13e9 Antony Chazapis
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
256 cca6c617 Antony Chazapis
    except NotAllowedError:
257 cca6c617 Antony Chazapis
        raise Unauthorized('Access denied')
258 e8886082 Antony Chazapis
    except NameError, IndexError:
259 b956618e Antony Chazapis
        raise ItemNotFound('Container or object does not exist')
260 3436eeb0 Antony Chazapis
    except ValueError:
261 3436eeb0 Antony Chazapis
        raise BadRequest('Invalid sharing header')
262 1993fea9 Antony Chazapis
    except AttributeError, e:
263 1993fea9 Antony Chazapis
        raise Conflict(json.dumps(e.data))
264 e0f916bb Antony Chazapis
    if public is not None:
265 e0f916bb Antony Chazapis
        try:
266 e0f916bb Antony Chazapis
            backend.update_object_public(request.user, v_account, v_container, v_object, public)
267 e0f916bb Antony Chazapis
        except NotAllowedError:
268 e0f916bb Antony Chazapis
            raise Unauthorized('Access denied')
269 e0f916bb Antony Chazapis
        except NameError:
270 e0f916bb Antony Chazapis
            raise ItemNotFound('Object does not exist')
271 b956618e Antony Chazapis
272 83dd59c5 Antony Chazapis
def get_int_parameter(request, name):
273 83dd59c5 Antony Chazapis
    p = request.GET.get(name)
274 83dd59c5 Antony Chazapis
    if p is not None:
275 58a6c894 Antony Chazapis
        try:
276 83dd59c5 Antony Chazapis
            p = int(p)
277 58a6c894 Antony Chazapis
        except ValueError:
278 58a6c894 Antony Chazapis
            return None
279 83dd59c5 Antony Chazapis
        if p < 0:
280 58a6c894 Antony Chazapis
            return None
281 83dd59c5 Antony Chazapis
    return p
282 58a6c894 Antony Chazapis
283 22dab079 Antony Chazapis
def get_content_length(request):
284 22dab079 Antony Chazapis
    content_length = request.META.get('CONTENT_LENGTH')
285 22dab079 Antony Chazapis
    if not content_length:
286 22dab079 Antony Chazapis
        raise LengthRequired('Missing Content-Length header')
287 22dab079 Antony Chazapis
    try:
288 22dab079 Antony Chazapis
        content_length = int(content_length)
289 22dab079 Antony Chazapis
        if content_length < 0:
290 22dab079 Antony Chazapis
            raise ValueError
291 22dab079 Antony Chazapis
    except ValueError:
292 22dab079 Antony Chazapis
        raise BadRequest('Invalid Content-Length header')
293 22dab079 Antony Chazapis
    return content_length
294 22dab079 Antony Chazapis
295 22dab079 Antony Chazapis
def get_range(request, size):
296 58a6c894 Antony Chazapis
    """Parse a Range header from the request.
297 b956618e Antony Chazapis
    
298 22dab079 Antony Chazapis
    Either returns None, when the header is not existent or should be ignored,
299 22dab079 Antony Chazapis
    or a list of (offset, length) tuples - should be further checked.
300 b956618e Antony Chazapis
    """
301 83dd59c5 Antony Chazapis
    
302 22dab079 Antony Chazapis
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
303 22dab079 Antony Chazapis
    if not ranges.startswith('bytes='):
304 b956618e Antony Chazapis
        return None
305 b956618e Antony Chazapis
    
306 22dab079 Antony Chazapis
    ret = []
307 22dab079 Antony Chazapis
    for r in (x.strip() for x in ranges[6:].split(',')):
308 22dab079 Antony Chazapis
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
309 22dab079 Antony Chazapis
        m = p.match(r)
310 22dab079 Antony Chazapis
        if not m:
311 22dab079 Antony Chazapis
            return None
312 22dab079 Antony Chazapis
        offset = m.group('offset')
313 22dab079 Antony Chazapis
        upto = m.group('upto')
314 22dab079 Antony Chazapis
        if offset == '' and upto == '':
315 b956618e Antony Chazapis
            return None
316 b956618e Antony Chazapis
        
317 22dab079 Antony Chazapis
        if offset != '':
318 22dab079 Antony Chazapis
            offset = int(offset)
319 22dab079 Antony Chazapis
            if upto != '':
320 b956618e Antony Chazapis
                upto = int(upto)
321 22dab079 Antony Chazapis
                if offset > upto:
322 22dab079 Antony Chazapis
                    return None
323 22dab079 Antony Chazapis
                ret.append((offset, upto - offset + 1))
324 22dab079 Antony Chazapis
            else:
325 22dab079 Antony Chazapis
                ret.append((offset, size - offset))
326 b956618e Antony Chazapis
        else:
327 22dab079 Antony Chazapis
            length = int(upto)
328 22dab079 Antony Chazapis
            ret.append((size - length, length))
329 22dab079 Antony Chazapis
    
330 22dab079 Antony Chazapis
    return ret
331 22dab079 Antony Chazapis
332 22dab079 Antony Chazapis
def get_content_range(request):
333 58a6c894 Antony Chazapis
    """Parse a Content-Range header from the request.
334 22dab079 Antony Chazapis
    
335 22dab079 Antony Chazapis
    Either returns None, when the header is not existent or should be ignored,
336 22dab079 Antony Chazapis
    or an (offset, length, total) tuple - check as length, total may be None.
337 22dab079 Antony Chazapis
    Returns (None, None, None) if the provided range is '*/*'.
338 22dab079 Antony Chazapis
    """
339 22dab079 Antony Chazapis
    
340 22dab079 Antony Chazapis
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
341 22dab079 Antony Chazapis
    if not ranges:
342 22dab079 Antony Chazapis
        return None
343 22dab079 Antony Chazapis
    
344 22dab079 Antony Chazapis
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
345 22dab079 Antony Chazapis
    m = p.match(ranges)
346 22dab079 Antony Chazapis
    if not m:
347 22dab079 Antony Chazapis
        if ranges == 'bytes */*':
348 22dab079 Antony Chazapis
            return (None, None, None)
349 22dab079 Antony Chazapis
        return None
350 22dab079 Antony Chazapis
    offset = int(m.group('offset'))
351 22dab079 Antony Chazapis
    upto = m.group('upto')
352 22dab079 Antony Chazapis
    total = m.group('total')
353 22dab079 Antony Chazapis
    if upto != '':
354 22dab079 Antony Chazapis
        upto = int(upto)
355 b956618e Antony Chazapis
    else:
356 22dab079 Antony Chazapis
        upto = None
357 22dab079 Antony Chazapis
    if total != '*':
358 22dab079 Antony Chazapis
        total = int(total)
359 22dab079 Antony Chazapis
    else:
360 22dab079 Antony Chazapis
        total = None
361 70e526a0 Antony Chazapis
    if (upto is not None and offset > upto) or \
362 70e526a0 Antony Chazapis
        (total is not None and offset >= total) or \
363 70e526a0 Antony Chazapis
        (total is not None and upto is not None and upto >= total):
364 22dab079 Antony Chazapis
        return None
365 22dab079 Antony Chazapis
    
366 70e526a0 Antony Chazapis
    if upto is None:
367 22dab079 Antony Chazapis
        length = None
368 22dab079 Antony Chazapis
    else:
369 22dab079 Antony Chazapis
        length = upto - offset + 1
370 22dab079 Antony Chazapis
    return (offset, length, total)
371 b956618e Antony Chazapis
372 3436eeb0 Antony Chazapis
def get_sharing(request):
373 3436eeb0 Antony Chazapis
    """Parse an X-Object-Sharing header from the request.
374 3436eeb0 Antony Chazapis
    
375 3436eeb0 Antony Chazapis
    Raises BadRequest on error.
376 3436eeb0 Antony Chazapis
    """
377 3436eeb0 Antony Chazapis
    
378 3436eeb0 Antony Chazapis
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
379 cca6c617 Antony Chazapis
    if permissions is None:
380 3436eeb0 Antony Chazapis
        return None
381 3436eeb0 Antony Chazapis
    
382 3436eeb0 Antony Chazapis
    ret = {}
383 cca6c617 Antony Chazapis
    permissions = permissions.replace(' ', '')
384 cca6c617 Antony Chazapis
    if permissions == '':
385 cca6c617 Antony Chazapis
        return ret
386 cca6c617 Antony Chazapis
    for perm in (x for x in permissions.split(';')):
387 cca6c617 Antony Chazapis
        if perm.startswith('read='):
388 02c0c3fa Antony Chazapis
            ret['read'] = [v.replace(' ','').lower() for v in perm[5:].split(',')]
389 02c0c3fa Antony Chazapis
            if '' in ret['read']:
390 02c0c3fa Antony Chazapis
                ret['read'].remove('')
391 e8886082 Antony Chazapis
            if '*' in ret['read']:
392 e8886082 Antony Chazapis
                ret['read'] = ['*']
393 3436eeb0 Antony Chazapis
            if len(ret['read']) == 0:
394 3436eeb0 Antony Chazapis
                raise BadRequest('Bad X-Object-Sharing header value')
395 3436eeb0 Antony Chazapis
        elif perm.startswith('write='):
396 02c0c3fa Antony Chazapis
            ret['write'] = [v.replace(' ','').lower() for v in perm[6:].split(',')]
397 02c0c3fa Antony Chazapis
            if '' in ret['write']:
398 02c0c3fa Antony Chazapis
                ret['write'].remove('')
399 e8886082 Antony Chazapis
            if '*' in ret['write']:
400 e8886082 Antony Chazapis
                ret['write'] = ['*']
401 3436eeb0 Antony Chazapis
            if len(ret['write']) == 0:
402 3436eeb0 Antony Chazapis
                raise BadRequest('Bad X-Object-Sharing header value')
403 3436eeb0 Antony Chazapis
        else:
404 3436eeb0 Antony Chazapis
            raise BadRequest('Bad X-Object-Sharing header value')
405 3436eeb0 Antony Chazapis
    return ret
406 3436eeb0 Antony Chazapis
407 3ab38c43 Antony Chazapis
def get_public(request):
408 3ab38c43 Antony Chazapis
    """Parse an X-Object-Public header from the request.
409 3ab38c43 Antony Chazapis
    
410 3ab38c43 Antony Chazapis
    Raises BadRequest on error.
411 3ab38c43 Antony Chazapis
    """
412 3ab38c43 Antony Chazapis
    
413 3ab38c43 Antony Chazapis
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
414 3ab38c43 Antony Chazapis
    if public is None:
415 3ab38c43 Antony Chazapis
        return None
416 3ab38c43 Antony Chazapis
    
417 3ab38c43 Antony Chazapis
    public = public.replace(' ', '').lower()
418 3ab38c43 Antony Chazapis
    if public == 'true':
419 3ab38c43 Antony Chazapis
        return True
420 3ab38c43 Antony Chazapis
    elif public == 'false' or public == '':
421 3ab38c43 Antony Chazapis
        return False
422 3ab38c43 Antony Chazapis
    raise BadRequest('Bad X-Object-Public header value')
423 3ab38c43 Antony Chazapis
424 b956618e Antony Chazapis
def raw_input_socket(request):
425 58a6c894 Antony Chazapis
    """Return the socket for reading the rest of the request."""
426 83dd59c5 Antony Chazapis
    
427 b956618e Antony Chazapis
    server_software = request.META.get('SERVER_SOFTWARE')
428 b956618e Antony Chazapis
    if not server_software:
429 b956618e Antony Chazapis
        if 'wsgi.input' in request.environ:
430 b956618e Antony Chazapis
            return request.environ['wsgi.input']
431 b956618e Antony Chazapis
        raise ServiceUnavailable('Unknown server software')
432 b956618e Antony Chazapis
    if server_software.startswith('WSGIServer'):
433 b956618e Antony Chazapis
        return request.environ['wsgi.input']
434 b956618e Antony Chazapis
    elif server_software.startswith('mod_python'):
435 b956618e Antony Chazapis
        return request._req
436 b956618e Antony Chazapis
    raise ServiceUnavailable('Unknown server software')
437 b956618e Antony Chazapis
438 b956618e Antony Chazapis
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
439 b956618e Antony Chazapis
440 22dab079 Antony Chazapis
def socket_read_iterator(sock, length=0, blocksize=4096):
441 58a6c894 Antony Chazapis
    """Return a maximum of blocksize data read from the socket in each iteration.
442 b956618e Antony Chazapis
    
443 22dab079 Antony Chazapis
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
444 b956618e Antony Chazapis
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
445 b956618e Antony Chazapis
    """
446 83dd59c5 Antony Chazapis
    
447 b956618e Antony Chazapis
    if length < 0: # Chunked transfers
448 22dab079 Antony Chazapis
        data = ''
449 b956618e Antony Chazapis
        while length < MAX_UPLOAD_SIZE:
450 22dab079 Antony Chazapis
            # Get chunk size.
451 22dab079 Antony Chazapis
            if hasattr(sock, 'readline'):
452 22dab079 Antony Chazapis
                chunk_length = sock.readline()
453 22dab079 Antony Chazapis
            else:
454 22dab079 Antony Chazapis
                chunk_length = ''
455 22dab079 Antony Chazapis
                while chunk_length[-1:] != '\n':
456 22dab079 Antony Chazapis
                    chunk_length += sock.read(1)
457 22dab079 Antony Chazapis
                chunk_length.strip()
458 b956618e Antony Chazapis
            pos = chunk_length.find(';')
459 b956618e Antony Chazapis
            if pos >= 0:
460 b956618e Antony Chazapis
                chunk_length = chunk_length[:pos]
461 b956618e Antony Chazapis
            try:
462 b956618e Antony Chazapis
                chunk_length = int(chunk_length, 16)
463 b956618e Antony Chazapis
            except Exception, e:
464 b956618e Antony Chazapis
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
465 22dab079 Antony Chazapis
            # Check if done.
466 b956618e Antony Chazapis
            if chunk_length == 0:
467 22dab079 Antony Chazapis
                if len(data) > 0:
468 22dab079 Antony Chazapis
                    yield data
469 b956618e Antony Chazapis
                return
470 22dab079 Antony Chazapis
            # Get the actual data.
471 b956618e Antony Chazapis
            while chunk_length > 0:
472 22dab079 Antony Chazapis
                chunk = sock.read(min(chunk_length, blocksize))
473 22dab079 Antony Chazapis
                chunk_length -= len(chunk)
474 623a0cf4 Sofia Papagiannaki
                if length > 0:
475 623a0cf4 Sofia Papagiannaki
                    length += len(chunk)
476 22dab079 Antony Chazapis
                data += chunk
477 22dab079 Antony Chazapis
                if len(data) >= blocksize:
478 22dab079 Antony Chazapis
                    ret = data[:blocksize]
479 22dab079 Antony Chazapis
                    data = data[blocksize:]
480 22dab079 Antony Chazapis
                    yield ret
481 22dab079 Antony Chazapis
            sock.read(2) # CRLF
482 b956618e Antony Chazapis
        # TODO: Raise something to note that maximum size is reached.
483 b956618e Antony Chazapis
    else:
484 b956618e Antony Chazapis
        if length > MAX_UPLOAD_SIZE:
485 b956618e Antony Chazapis
            # TODO: Raise something to note that maximum size is reached.
486 b956618e Antony Chazapis
            pass
487 b956618e Antony Chazapis
        while length > 0:
488 b956618e Antony Chazapis
            data = sock.read(min(length, blocksize))
489 b956618e Antony Chazapis
            length -= len(data)
490 b956618e Antony Chazapis
            yield data
491 b956618e Antony Chazapis
492 22dab079 Antony Chazapis
class ObjectWrapper(object):
493 58a6c894 Antony Chazapis
    """Return the object's data block-per-block in each iteration.
494 22dab079 Antony Chazapis
    
495 22dab079 Antony Chazapis
    Read from the object using the offset and length provided in each entry of the range list.
496 22dab079 Antony Chazapis
    """
497 22dab079 Antony Chazapis
    
498 8cb45c13 Antony Chazapis
    def __init__(self, ranges, sizes, hashmaps, boundary):
499 22dab079 Antony Chazapis
        self.ranges = ranges
500 8cb45c13 Antony Chazapis
        self.sizes = sizes
501 8cb45c13 Antony Chazapis
        self.hashmaps = hashmaps
502 22dab079 Antony Chazapis
        self.boundary = boundary
503 8cb45c13 Antony Chazapis
        self.size = sum(self.sizes)
504 22dab079 Antony Chazapis
        
505 8cb45c13 Antony Chazapis
        self.file_index = 0
506 8cb45c13 Antony Chazapis
        self.block_index = 0
507 8cb45c13 Antony Chazapis
        self.block_hash = -1
508 22dab079 Antony Chazapis
        self.block = ''
509 22dab079 Antony Chazapis
        
510 22dab079 Antony Chazapis
        self.range_index = -1
511 22dab079 Antony Chazapis
        self.offset, self.length = self.ranges[0]
512 22dab079 Antony Chazapis
    
513 22dab079 Antony Chazapis
    def __iter__(self):
514 22dab079 Antony Chazapis
        return self
515 22dab079 Antony Chazapis
    
516 22dab079 Antony Chazapis
    def part_iterator(self):
517 22dab079 Antony Chazapis
        if self.length > 0:
518 8cb45c13 Antony Chazapis
            # Get the file for the current offset.
519 8cb45c13 Antony Chazapis
            file_size = self.sizes[self.file_index]
520 8cb45c13 Antony Chazapis
            while self.offset >= file_size:
521 8cb45c13 Antony Chazapis
                self.offset -= file_size
522 8cb45c13 Antony Chazapis
                self.file_index += 1
523 8cb45c13 Antony Chazapis
                file_size = self.sizes[self.file_index]
524 8cb45c13 Antony Chazapis
            
525 8cb45c13 Antony Chazapis
            # Get the block for the current position.
526 8cb45c13 Antony Chazapis
            self.block_index = int(self.offset / backend.block_size)
527 8cb45c13 Antony Chazapis
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
528 8cb45c13 Antony Chazapis
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
529 22dab079 Antony Chazapis
                try:
530 8cb45c13 Antony Chazapis
                    self.block = backend.get_block(self.block_hash)
531 22dab079 Antony Chazapis
                except NameError:
532 22dab079 Antony Chazapis
                    raise ItemNotFound('Block does not exist')
533 8cb45c13 Antony Chazapis
            
534 22dab079 Antony Chazapis
            # Get the data from the block.
535 22dab079 Antony Chazapis
            bo = self.offset % backend.block_size
536 8cb45c13 Antony Chazapis
            bl = min(self.length, len(self.block) - bo)
537 22dab079 Antony Chazapis
            data = self.block[bo:bo + bl]
538 22dab079 Antony Chazapis
            self.offset += bl
539 22dab079 Antony Chazapis
            self.length -= bl
540 22dab079 Antony Chazapis
            return data
541 22dab079 Antony Chazapis
        else:
542 22dab079 Antony Chazapis
            raise StopIteration
543 22dab079 Antony Chazapis
    
544 22dab079 Antony Chazapis
    def next(self):
545 22dab079 Antony Chazapis
        if len(self.ranges) == 1:
546 22dab079 Antony Chazapis
            return self.part_iterator()
547 22dab079 Antony Chazapis
        if self.range_index == len(self.ranges):
548 22dab079 Antony Chazapis
            raise StopIteration
549 22dab079 Antony Chazapis
        try:
550 22dab079 Antony Chazapis
            if self.range_index == -1:
551 22dab079 Antony Chazapis
                raise StopIteration
552 22dab079 Antony Chazapis
            return self.part_iterator()
553 22dab079 Antony Chazapis
        except StopIteration:
554 22dab079 Antony Chazapis
            self.range_index += 1
555 22dab079 Antony Chazapis
            out = []
556 22dab079 Antony Chazapis
            if self.range_index < len(self.ranges):
557 22dab079 Antony Chazapis
                # Part header.
558 22dab079 Antony Chazapis
                self.offset, self.length = self.ranges[self.range_index]
559 8cb45c13 Antony Chazapis
                self.file_index = 0
560 22dab079 Antony Chazapis
                if self.range_index > 0:
561 22dab079 Antony Chazapis
                    out.append('')
562 22dab079 Antony Chazapis
                out.append('--' + self.boundary)
563 22dab079 Antony Chazapis
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
564 22dab079 Antony Chazapis
                out.append('Content-Transfer-Encoding: binary')
565 22dab079 Antony Chazapis
                out.append('')
566 22dab079 Antony Chazapis
                out.append('')
567 22dab079 Antony Chazapis
                return '\r\n'.join(out)
568 22dab079 Antony Chazapis
            else:
569 22dab079 Antony Chazapis
                # Footer.
570 22dab079 Antony Chazapis
                out.append('')
571 22dab079 Antony Chazapis
                out.append('--' + self.boundary + '--')
572 22dab079 Antony Chazapis
                out.append('')
573 22dab079 Antony Chazapis
                return '\r\n'.join(out)
574 22dab079 Antony Chazapis
575 8cb45c13 Antony Chazapis
def object_data_response(request, sizes, hashmaps, meta, public=False):
576 7bef5750 Antony Chazapis
    """Get the HttpResponse object for replying with the object's data."""
577 7bef5750 Antony Chazapis
    
578 7bef5750 Antony Chazapis
    # Range handling.
579 8cb45c13 Antony Chazapis
    size = sum(sizes)
580 7bef5750 Antony Chazapis
    ranges = get_range(request, size)
581 7bef5750 Antony Chazapis
    if ranges is None:
582 7bef5750 Antony Chazapis
        ranges = [(0, size)]
583 7bef5750 Antony Chazapis
        ret = 200
584 7bef5750 Antony Chazapis
    else:
585 7bef5750 Antony Chazapis
        check = [True for offset, length in ranges if
586 7bef5750 Antony Chazapis
                    length <= 0 or length > size or
587 7bef5750 Antony Chazapis
                    offset < 0 or offset >= size or
588 7bef5750 Antony Chazapis
                    offset + length > size]
589 7bef5750 Antony Chazapis
        if len(check) > 0:
590 6d1e6dce Sofia Papagiannaki
            raise RangeNotSatisfiable('Requested range exceeds object limits')
591 7bef5750 Antony Chazapis
        ret = 206
592 6d1e6dce Sofia Papagiannaki
        if_range = request.META.get('HTTP_IF_RANGE', '')
593 6d1e6dce Sofia Papagiannaki
        if if_range and if_range.startswith('If-Range:'):
594 6d1e6dce Sofia Papagiannaki
            if_range = if_range.split('If-Range:')[1]
595 6d1e6dce Sofia Papagiannaki
            try:
596 6d1e6dce Sofia Papagiannaki
                # modification time has passed instead
597 6d1e6dce Sofia Papagiannaki
                last_modified = parse_http_date(if_range)
598 6d1e6dce Sofia Papagiannaki
                if last_modified != meta['modified']:
599 6d1e6dce Sofia Papagiannaki
                    ranges = [(0, size)]
600 6d1e6dce Sofia Papagiannaki
                    ret = 200
601 6d1e6dce Sofia Papagiannaki
            except ValueError:
602 6d1e6dce Sofia Papagiannaki
                if if_range != meta['hash']:
603 6d1e6dce Sofia Papagiannaki
                    ranges = [(0, size)]
604 6d1e6dce Sofia Papagiannaki
                    ret = 200
605 7bef5750 Antony Chazapis
    
606 7bef5750 Antony Chazapis
    if ret == 206 and len(ranges) > 1:
607 7bef5750 Antony Chazapis
        boundary = uuid.uuid4().hex
608 7bef5750 Antony Chazapis
    else:
609 7bef5750 Antony Chazapis
        boundary = ''
610 8cb45c13 Antony Chazapis
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
611 7bef5750 Antony Chazapis
    response = HttpResponse(wrapper, status=ret)
612 02c0c3fa Antony Chazapis
    put_object_headers(response, meta, public)
613 7bef5750 Antony Chazapis
    if ret == 206:
614 7bef5750 Antony Chazapis
        if len(ranges) == 1:
615 7bef5750 Antony Chazapis
            offset, length = ranges[0]
616 7bef5750 Antony Chazapis
            response['Content-Length'] = length # Update with the correct length.
617 7bef5750 Antony Chazapis
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
618 7bef5750 Antony Chazapis
        else:
619 7bef5750 Antony Chazapis
            del(response['Content-Length'])
620 7bef5750 Antony Chazapis
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
621 7bef5750 Antony Chazapis
    return response
622 7bef5750 Antony Chazapis
623 cb146cf9 Antony Chazapis
def put_object_block(hashmap, data, offset):
624 cb146cf9 Antony Chazapis
    """Put one block of data at the given offset."""
625 cb146cf9 Antony Chazapis
    
626 cb146cf9 Antony Chazapis
    bi = int(offset / backend.block_size)
627 cb146cf9 Antony Chazapis
    bo = offset % backend.block_size
628 cb146cf9 Antony Chazapis
    bl = min(len(data), backend.block_size - bo)
629 cb146cf9 Antony Chazapis
    if bi < len(hashmap):
630 cb146cf9 Antony Chazapis
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
631 cb146cf9 Antony Chazapis
    else:
632 cb146cf9 Antony Chazapis
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
633 cb146cf9 Antony Chazapis
    return bl # Return ammount of data written.
634 cb146cf9 Antony Chazapis
635 49133392 Antony Chazapis
def hashmap_hash(hashmap):
636 58a6c894 Antony Chazapis
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
637 49133392 Antony Chazapis
    
638 49133392 Antony Chazapis
    def subhash(d):
639 49133392 Antony Chazapis
        h = hashlib.new(backend.hash_algorithm)
640 49133392 Antony Chazapis
        h.update(d)
641 49133392 Antony Chazapis
        return h.digest()
642 49133392 Antony Chazapis
    
643 6ecccd3a Antony Chazapis
    if len(hashmap) == 0:
644 6ecccd3a Antony Chazapis
        return hexlify(subhash(''))
645 6ecccd3a Antony Chazapis
    if len(hashmap) == 1:
646 6ecccd3a Antony Chazapis
        return hexlify(subhash(hashmap[0]))
647 6ecccd3a Antony Chazapis
    s = 2
648 6ecccd3a Antony Chazapis
    while s < len(hashmap):
649 6ecccd3a Antony Chazapis
        s = s * 2
650 6ecccd3a Antony Chazapis
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
651 49133392 Antony Chazapis
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
652 49133392 Antony Chazapis
    while len(h) > 1:
653 49133392 Antony Chazapis
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
654 49133392 Antony Chazapis
    return hexlify(h[0])
655 49133392 Antony Chazapis
656 b956618e Antony Chazapis
def update_response_headers(request, response):
657 b956618e Antony Chazapis
    if request.serialization == 'xml':
658 b956618e Antony Chazapis
        response['Content-Type'] = 'application/xml; charset=UTF-8'
659 b956618e Antony Chazapis
    elif request.serialization == 'json':
660 b956618e Antony Chazapis
        response['Content-Type'] = 'application/json; charset=UTF-8'
661 22dab079 Antony Chazapis
    elif not response['Content-Type']:
662 b956618e Antony Chazapis
        response['Content-Type'] = 'text/plain; charset=UTF-8'
663 b956618e Antony Chazapis
664 b956618e Antony Chazapis
    if settings.TEST:
665 b956618e Antony Chazapis
        response['Date'] = format_date_time(time())
666 b956618e Antony Chazapis
667 b956618e Antony Chazapis
def render_fault(request, fault):
668 b956618e Antony Chazapis
    if settings.DEBUG or settings.TEST:
669 b956618e Antony Chazapis
        fault.details = format_exc(fault)
670 1993fea9 Antony Chazapis
    
671 b956618e Antony Chazapis
    request.serialization = 'text'
672 b956618e Antony Chazapis
    data = '\n'.join((fault.message, fault.details)) + '\n'
673 b956618e Antony Chazapis
    response = HttpResponse(data, status=fault.code)
674 b956618e Antony Chazapis
    update_response_headers(request, response)
675 b956618e Antony Chazapis
    return response
676 b956618e Antony Chazapis
677 b956618e Antony Chazapis
def request_serialization(request, format_allowed=False):
678 58a6c894 Antony Chazapis
    """Return the serialization format requested.
679 b956618e Antony Chazapis
    
680 b956618e Antony Chazapis
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
681 b956618e Antony Chazapis
    """
682 83dd59c5 Antony Chazapis
    
683 b956618e Antony Chazapis
    if not format_allowed:
684 b956618e Antony Chazapis
        return 'text'
685 b956618e Antony Chazapis
    
686 b956618e Antony Chazapis
    format = request.GET.get('format')
687 b956618e Antony Chazapis
    if format == 'json':
688 b956618e Antony Chazapis
        return 'json'
689 b956618e Antony Chazapis
    elif format == 'xml':
690 b956618e Antony Chazapis
        return 'xml'
691 b956618e Antony Chazapis
    
692 b956618e Antony Chazapis
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
693 b956618e Antony Chazapis
        accept, sep, rest = item.strip().partition(';')
694 22dab079 Antony Chazapis
        if accept == 'application/json':
695 b956618e Antony Chazapis
            return 'json'
696 b956618e Antony Chazapis
        elif accept == 'application/xml' or accept == 'text/xml':
697 b956618e Antony Chazapis
            return 'xml'
698 b956618e Antony Chazapis
    
699 b956618e Antony Chazapis
    return 'text'
700 b956618e Antony Chazapis
701 b956618e Antony Chazapis
def api_method(http_method=None, format_allowed=False):
702 58a6c894 Antony Chazapis
    """Decorator function for views that implement an API method."""
703 83dd59c5 Antony Chazapis
    
704 b956618e Antony Chazapis
    def decorator(func):
705 b956618e Antony Chazapis
        @wraps(func)
706 b956618e Antony Chazapis
        def wrapper(request, *args, **kwargs):
707 b956618e Antony Chazapis
            try:
708 b956618e Antony Chazapis
                if http_method and request.method != http_method:
709 b956618e Antony Chazapis
                    raise BadRequest('Method not allowed.')
710 2c22e4ac Antony Chazapis
                
711 b956618e Antony Chazapis
                # The args variable may contain up to (account, container, object).
712 b956618e Antony Chazapis
                if len(args) > 1 and len(args[1]) > 256:
713 b956618e Antony Chazapis
                    raise BadRequest('Container name too large.')
714 b956618e Antony Chazapis
                if len(args) > 2 and len(args[2]) > 1024:
715 b956618e Antony Chazapis
                    raise BadRequest('Object name too large.')
716 b956618e Antony Chazapis
                
717 b956618e Antony Chazapis
                # Fill in custom request variables.
718 b956618e Antony Chazapis
                request.serialization = request_serialization(request, format_allowed)
719 b956618e Antony Chazapis
                
720 b956618e Antony Chazapis
                response = func(request, *args, **kwargs)
721 b956618e Antony Chazapis
                update_response_headers(request, response)
722 b956618e Antony Chazapis
                return response
723 b956618e Antony Chazapis
            except Fault, fault:
724 b956618e Antony Chazapis
                return render_fault(request, fault)
725 b956618e Antony Chazapis
            except BaseException, e:
726 b956618e Antony Chazapis
                logger.exception('Unexpected error: %s' % e)
727 b956618e Antony Chazapis
                fault = ServiceUnavailable('Unexpected error')
728 b956618e Antony Chazapis
                return render_fault(request, fault)
729 b956618e Antony Chazapis
        return wrapper
730 b956618e Antony Chazapis
    return decorator