Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 3ab38c43

History | View | Annotate | Download (26.4 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from functools import wraps
35
from time import time
36
from traceback import format_exc
37
from wsgiref.handlers import format_date_time
38
from binascii import hexlify
39

    
40
from django.conf import settings
41
from django.http import HttpResponse
42
from django.utils.http import http_date, parse_etags
43

    
44
from pithos.api.compat import parse_http_date_safe
45
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound,
46
                                LengthRequired, PreconditionFailed, RangeNotSatisfiable,
47
                                ServiceUnavailable)
48
from pithos.backends import backend
49
from pithos.backends.base import NotAllowedError
50

    
51
import datetime
52
import logging
53
import re
54
import hashlib
55
import uuid
56

    
57

    
58
logger = logging.getLogger(__name__)
59

    
60

    
61
def printable_header_dict(d):
62
    """Format a meta dictionary for printing out json/xml.
63
    
64
    Convert all keys to lower case and replace dashes to underscores.
65
    Change 'modified' key from backend to 'last_modified' and format date.
66
    """
67
    
68
    if 'modified' in d:
69
        d['last_modified'] = datetime.datetime.fromtimestamp(int(d['modified'])).isoformat()
70
        del(d['modified'])
71
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
72

    
73
def format_header_key(k):
74
    """Convert underscores to dashes and capitalize intra-dash strings."""
75
    
76
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
77

    
78
def get_header_prefix(request, prefix):
79
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
80
    
81
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
82
    # TODO: Document or remove '~' replacing.
83
    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)])
84

    
85
def get_account_headers(request):
86
    meta = get_header_prefix(request, 'X-Account-Meta-')
87
    groups = {}
88
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
89
        n = k[16:].lower()
90
        if '-' in n or '_' in n:
91
            raise BadRequest('Bad characters in group name')
92
        groups[n] = v.replace(' ', '').split(',')
93
        if '' in groups[n]:
94
            groups[n].remove('')
95
    return meta, groups
96

    
97
def put_account_headers(response, meta, groups):
98
    response['X-Account-Container-Count'] = meta['count']
99
    response['X-Account-Bytes-Used'] = meta['bytes']
100
    if 'modified' in meta:
101
        response['Last-Modified'] = http_date(int(meta['modified']))
102
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
103
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
104
    if 'until_timestamp' in meta:
105
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
106
    for k, v in groups.iteritems():
107
        response[format_header_key('X-Account-Group-' + k).encode('utf-8')] = (','.join(v)).encode('utf-8')
108

    
109
def get_container_headers(request):
110
    meta = get_header_prefix(request, 'X-Container-Meta-')
111
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
112
    return meta, policy
113

    
114
def put_container_headers(response, meta, policy):
115
    response['X-Container-Object-Count'] = meta['count']
116
    response['X-Container-Bytes-Used'] = meta['bytes']
117
    response['Last-Modified'] = http_date(int(meta['modified']))
118
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
119
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
120
    response['X-Container-Object-Meta'] = [x[14:] for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
121
    response['X-Container-Block-Size'] = backend.block_size
122
    response['X-Container-Block-Hash'] = backend.hash_algorithm
123
    if 'until_timestamp' in meta:
124
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
125
    for k, v in policy.iteritems():
126
        response[format_header_key('X-Container-Policy-' + k).encode('utf-8')] = v.encode('utf-8')
127

    
128
def get_object_headers(request):
129
    meta = get_header_prefix(request, 'X-Object-Meta-')
130
    if request.META.get('CONTENT_TYPE'):
131
        meta['Content-Type'] = request.META['CONTENT_TYPE']
132
    if request.META.get('HTTP_CONTENT_ENCODING'):
133
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
134
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
135
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
136
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
137
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
138
    return meta, get_sharing(request), get_public(request)
139

    
140
def put_object_headers(response, meta, restricted=False):
141
    response['ETag'] = meta['hash']
142
    response['Content-Length'] = meta['bytes']
143
    response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
144
    response['Last-Modified'] = http_date(int(meta['modified']))
145
    if not restricted:
146
        response['X-Object-Modified-By'] = meta['modified_by']
147
        response['X-Object-Version'] = meta['version']
148
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
149
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
150
            response[k.encode('utf-8')] = meta[k].encode('utf-8')
151
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By'):
152
            if k in meta:
153
                response[k] = meta[k]
154
    else:
155
        for k in ('Content-Encoding', 'Content-Disposition'):
156
            if k in meta:
157
                response[k] = meta[k]
158

    
159
def update_manifest_meta(request, v_account, meta):
160
    """Update metadata if the object has an X-Object-Manifest."""
161
    
162
    if 'X-Object-Manifest' in meta:
163
        hash = ''
164
        bytes = 0
165
        try:
166
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
167
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
168
            for x in objects:
169
                src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
170
                hash += src_meta['hash']
171
                bytes += src_meta['bytes']
172
        except:
173
            # Ignore errors.
174
            return
175
        meta['bytes'] = bytes
176
        md5 = hashlib.md5()
177
        md5.update(hash)
178
        meta['hash'] = md5.hexdigest().lower()
179

    
180
def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
181
    if permissions is None:
182
        return
183
    perm_path, perms = permissions
184
    if len(perms) == 0:
185
        return
186
    ret = []
187
    r = ','.join(perms.get('read', []))
188
    if r:
189
        ret.append('read=' + r)
190
    w = ','.join(perms.get('write', []))
191
    if w:
192
        ret.append('write=' + w)
193
    meta['X-Object-Sharing'] = '; '.join(ret)
194
    if '/'.join((v_account, v_container, v_object)) != perm_path:
195
        meta['X-Object-Shared-By'] = perm_path
196

    
197
def validate_modification_preconditions(request, meta):
198
    """Check that the modified timestamp conforms with the preconditions set."""
199
    
200
    if 'modified' not in meta:
201
        return # TODO: Always return?
202
    
203
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
204
    if if_modified_since is not None:
205
        if_modified_since = parse_http_date_safe(if_modified_since)
206
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
207
        raise NotModified('Resource has not been modified')
208
    
209
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
210
    if if_unmodified_since is not None:
211
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
212
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
213
        raise PreconditionFailed('Resource has been modified')
214

    
215
def validate_matching_preconditions(request, meta):
216
    """Check that the ETag conforms with the preconditions set."""
217
    
218
    if 'hash' not in meta:
219
        return # TODO: Always return?
220
    
221
    if_match = request.META.get('HTTP_IF_MATCH')
222
    if if_match is not None and if_match != '*':
223
        if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
224
            raise PreconditionFailed('Resource Etag does not match')
225
    
226
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
227
    if if_none_match is not None:
228
        if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
229
            raise NotModified('Resource Etag matches')
230

    
231
def split_container_object_string(s):
232
    if not len(s) > 0 or s[0] != '/':
233
        raise ValueError
234
    s = s[1:]
235
    pos = s.find('/')
236
    if pos == -1:
237
        raise ValueError
238
    return s[:pos], s[(pos + 1):]
239

    
240
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
241
    """Copy or move an object."""
242
    
243
    meta, permissions, public = get_object_headers(request)
244
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
245
    try:
246
        if move:
247
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
248
        else:
249
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
250
    except NotAllowedError:
251
        raise Unauthorized('Access denied')
252
    except NameError, IndexError:
253
        raise ItemNotFound('Container or object does not exist')
254
    except ValueError:
255
        raise BadRequest('Invalid sharing header')
256
    except AttributeError:
257
        raise Conflict('Sharing already set above or below this path in the hierarchy')
258

    
259
def get_int_parameter(request, name):
260
    p = request.GET.get(name)
261
    if p is not None:
262
        try:
263
            p = int(p)
264
        except ValueError:
265
            return None
266
        if p < 0:
267
            return None
268
    return p
269

    
270
def get_content_length(request):
271
    content_length = request.META.get('CONTENT_LENGTH')
272
    if not content_length:
273
        raise LengthRequired('Missing Content-Length header')
274
    try:
275
        content_length = int(content_length)
276
        if content_length < 0:
277
            raise ValueError
278
    except ValueError:
279
        raise BadRequest('Invalid Content-Length header')
280
    return content_length
281

    
282
def get_range(request, size):
283
    """Parse a Range header from the request.
284
    
285
    Either returns None, when the header is not existent or should be ignored,
286
    or a list of (offset, length) tuples - should be further checked.
287
    """
288
    
289
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
290
    if not ranges.startswith('bytes='):
291
        return None
292
    
293
    ret = []
294
    for r in (x.strip() for x in ranges[6:].split(',')):
295
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
296
        m = p.match(r)
297
        if not m:
298
            return None
299
        offset = m.group('offset')
300
        upto = m.group('upto')
301
        if offset == '' and upto == '':
302
            return None
303
        
304
        if offset != '':
305
            offset = int(offset)
306
            if upto != '':
307
                upto = int(upto)
308
                if offset > upto:
309
                    return None
310
                ret.append((offset, upto - offset + 1))
311
            else:
312
                ret.append((offset, size - offset))
313
        else:
314
            length = int(upto)
315
            ret.append((size - length, length))
316
    
317
    return ret
318

    
319
def get_content_range(request):
320
    """Parse a Content-Range header from the request.
321
    
322
    Either returns None, when the header is not existent or should be ignored,
323
    or an (offset, length, total) tuple - check as length, total may be None.
324
    Returns (None, None, None) if the provided range is '*/*'.
325
    """
326
    
327
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
328
    if not ranges:
329
        return None
330
    
331
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
332
    m = p.match(ranges)
333
    if not m:
334
        if ranges == 'bytes */*':
335
            return (None, None, None)
336
        return None
337
    offset = int(m.group('offset'))
338
    upto = m.group('upto')
339
    total = m.group('total')
340
    if upto != '':
341
        upto = int(upto)
342
    else:
343
        upto = None
344
    if total != '*':
345
        total = int(total)
346
    else:
347
        total = None
348
    if (upto is not None and offset > upto) or \
349
        (total is not None and offset >= total) or \
350
        (total is not None and upto is not None and upto >= total):
351
        return None
352
    
353
    if upto is None:
354
        length = None
355
    else:
356
        length = upto - offset + 1
357
    return (offset, length, total)
358

    
359
def get_sharing(request):
360
    """Parse an X-Object-Sharing header from the request.
361
    
362
    Raises BadRequest on error.
363
    """
364
    
365
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
366
    if permissions is None:
367
        return None
368
    
369
    ret = {}
370
    permissions = permissions.replace(' ', '')
371
    if permissions == '':
372
        return ret
373
    for perm in (x for x in permissions.split(';')):
374
        if perm.startswith('read='):
375
            ret['read'] = [v.replace(' ','').lower() for v in perm[5:].split(',')]
376
            if '' in ret['read']:
377
                ret['read'].remove('')
378
            if '*' in ret['read']:
379
                ret['read'] = ['*']
380
            if len(ret['read']) == 0:
381
                raise BadRequest('Bad X-Object-Sharing header value')
382
        elif perm.startswith('write='):
383
            ret['write'] = [v.replace(' ','').lower() for v in perm[6:].split(',')]
384
            if '' in ret['write']:
385
                ret['write'].remove('')
386
            if '*' in ret['write']:
387
                ret['write'] = ['*']
388
            if len(ret['write']) == 0:
389
                raise BadRequest('Bad X-Object-Sharing header value')
390
        else:
391
            raise BadRequest('Bad X-Object-Sharing header value')
392
    return ret
393

    
394
def get_public(request):
395
    """Parse an X-Object-Public header from the request.
396
    
397
    Raises BadRequest on error.
398
    """
399
    
400
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
401
    if public is None:
402
        return None
403
    
404
    public = public.replace(' ', '').lower()
405
    if public == 'true':
406
        return True
407
    elif public == 'false' or public == '':
408
        return False
409
    raise BadRequest('Bad X-Object-Public header value')
410

    
411
def raw_input_socket(request):
412
    """Return the socket for reading the rest of the request."""
413
    
414
    server_software = request.META.get('SERVER_SOFTWARE')
415
    if not server_software:
416
        if 'wsgi.input' in request.environ:
417
            return request.environ['wsgi.input']
418
        raise ServiceUnavailable('Unknown server software')
419
    if server_software.startswith('WSGIServer'):
420
        return request.environ['wsgi.input']
421
    elif server_software.startswith('mod_python'):
422
        return request._req
423
    raise ServiceUnavailable('Unknown server software')
424

    
425
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
426

    
427
def socket_read_iterator(sock, length=0, blocksize=4096):
428
    """Return a maximum of blocksize data read from the socket in each iteration.
429
    
430
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
431
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
432
    """
433
    
434
    if length < 0: # Chunked transfers
435
        data = ''
436
        while length < MAX_UPLOAD_SIZE:
437
            # Get chunk size.
438
            if hasattr(sock, 'readline'):
439
                chunk_length = sock.readline()
440
            else:
441
                chunk_length = ''
442
                while chunk_length[-1:] != '\n':
443
                    chunk_length += sock.read(1)
444
                chunk_length.strip()
445
            pos = chunk_length.find(';')
446
            if pos >= 0:
447
                chunk_length = chunk_length[:pos]
448
            try:
449
                chunk_length = int(chunk_length, 16)
450
            except Exception, e:
451
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
452
            # Check if done.
453
            if chunk_length == 0:
454
                if len(data) > 0:
455
                    yield data
456
                return
457
            # Get the actual data.
458
            while chunk_length > 0:
459
                chunk = sock.read(min(chunk_length, blocksize))
460
                chunk_length -= len(chunk)
461
                if length > 0:
462
                    length += len(chunk)
463
                data += chunk
464
                if len(data) >= blocksize:
465
                    ret = data[:blocksize]
466
                    data = data[blocksize:]
467
                    yield ret
468
            sock.read(2) # CRLF
469
        # TODO: Raise something to note that maximum size is reached.
470
    else:
471
        if length > MAX_UPLOAD_SIZE:
472
            # TODO: Raise something to note that maximum size is reached.
473
            pass
474
        while length > 0:
475
            data = sock.read(min(length, blocksize))
476
            length -= len(data)
477
            yield data
478

    
479
class ObjectWrapper(object):
480
    """Return the object's data block-per-block in each iteration.
481
    
482
    Read from the object using the offset and length provided in each entry of the range list.
483
    """
484
    
485
    def __init__(self, ranges, sizes, hashmaps, boundary):
486
        self.ranges = ranges
487
        self.sizes = sizes
488
        self.hashmaps = hashmaps
489
        self.boundary = boundary
490
        self.size = sum(self.sizes)
491
        
492
        self.file_index = 0
493
        self.block_index = 0
494
        self.block_hash = -1
495
        self.block = ''
496
        
497
        self.range_index = -1
498
        self.offset, self.length = self.ranges[0]
499
    
500
    def __iter__(self):
501
        return self
502
    
503
    def part_iterator(self):
504
        if self.length > 0:
505
            # Get the file for the current offset.
506
            file_size = self.sizes[self.file_index]
507
            while self.offset >= file_size:
508
                self.offset -= file_size
509
                self.file_index += 1
510
                file_size = self.sizes[self.file_index]
511
            
512
            # Get the block for the current position.
513
            self.block_index = int(self.offset / backend.block_size)
514
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
515
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
516
                try:
517
                    self.block = backend.get_block(self.block_hash)
518
                except NameError:
519
                    raise ItemNotFound('Block does not exist')
520
            
521
            # Get the data from the block.
522
            bo = self.offset % backend.block_size
523
            bl = min(self.length, len(self.block) - bo)
524
            data = self.block[bo:bo + bl]
525
            self.offset += bl
526
            self.length -= bl
527
            return data
528
        else:
529
            raise StopIteration
530
    
531
    def next(self):
532
        if len(self.ranges) == 1:
533
            return self.part_iterator()
534
        if self.range_index == len(self.ranges):
535
            raise StopIteration
536
        try:
537
            if self.range_index == -1:
538
                raise StopIteration
539
            return self.part_iterator()
540
        except StopIteration:
541
            self.range_index += 1
542
            out = []
543
            if self.range_index < len(self.ranges):
544
                # Part header.
545
                self.offset, self.length = self.ranges[self.range_index]
546
                self.file_index = 0
547
                if self.range_index > 0:
548
                    out.append('')
549
                out.append('--' + self.boundary)
550
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
551
                out.append('Content-Transfer-Encoding: binary')
552
                out.append('')
553
                out.append('')
554
                return '\r\n'.join(out)
555
            else:
556
                # Footer.
557
                out.append('')
558
                out.append('--' + self.boundary + '--')
559
                out.append('')
560
                return '\r\n'.join(out)
561

    
562
def object_data_response(request, sizes, hashmaps, meta, public=False):
563
    """Get the HttpResponse object for replying with the object's data."""
564
    
565
    # Range handling.
566
    size = sum(sizes)
567
    ranges = get_range(request, size)
568
    if ranges is None:
569
        ranges = [(0, size)]
570
        ret = 200
571
    else:
572
        check = [True for offset, length in ranges if
573
                    length <= 0 or length > size or
574
                    offset < 0 or offset >= size or
575
                    offset + length > size]
576
        if len(check) > 0:
577
            raise RangeNotSatisfiable('Requested range exceeds object limits')        
578
        ret = 206
579
    
580
    if ret == 206 and len(ranges) > 1:
581
        boundary = uuid.uuid4().hex
582
    else:
583
        boundary = ''
584
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
585
    response = HttpResponse(wrapper, status=ret)
586
    put_object_headers(response, meta, public)
587
    if ret == 206:
588
        if len(ranges) == 1:
589
            offset, length = ranges[0]
590
            response['Content-Length'] = length # Update with the correct length.
591
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
592
        else:
593
            del(response['Content-Length'])
594
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
595
    return response
596

    
597
def put_object_block(hashmap, data, offset):
598
    """Put one block of data at the given offset."""
599
    
600
    bi = int(offset / backend.block_size)
601
    bo = offset % backend.block_size
602
    bl = min(len(data), backend.block_size - bo)
603
    if bi < len(hashmap):
604
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
605
    else:
606
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
607
    return bl # Return ammount of data written.
608

    
609
def hashmap_hash(hashmap):
610
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
611
    
612
    def subhash(d):
613
        h = hashlib.new(backend.hash_algorithm)
614
        h.update(d)
615
        return h.digest()
616
    
617
    if len(hashmap) == 0:
618
        return hexlify(subhash(''))
619
    if len(hashmap) == 1:
620
        return hexlify(subhash(hashmap[0]))
621
    s = 2
622
    while s < len(hashmap):
623
        s = s * 2
624
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
625
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
626
    while len(h) > 1:
627
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
628
    return hexlify(h[0])
629

    
630
def update_response_headers(request, response):
631
    if request.serialization == 'xml':
632
        response['Content-Type'] = 'application/xml; charset=UTF-8'
633
    elif request.serialization == 'json':
634
        response['Content-Type'] = 'application/json; charset=UTF-8'
635
    elif not response['Content-Type']:
636
        response['Content-Type'] = 'text/plain; charset=UTF-8'
637

    
638
    if settings.TEST:
639
        response['Date'] = format_date_time(time())
640

    
641
def render_fault(request, fault):
642
    if settings.DEBUG or settings.TEST:
643
        fault.details = format_exc(fault)
644

    
645
    request.serialization = 'text'
646
    data = '\n'.join((fault.message, fault.details)) + '\n'
647
    response = HttpResponse(data, status=fault.code)
648
    update_response_headers(request, response)
649
    return response
650

    
651
def request_serialization(request, format_allowed=False):
652
    """Return the serialization format requested.
653
    
654
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
655
    """
656
    
657
    if not format_allowed:
658
        return 'text'
659
    
660
    format = request.GET.get('format')
661
    if format == 'json':
662
        return 'json'
663
    elif format == 'xml':
664
        return 'xml'
665
    
666
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
667
        accept, sep, rest = item.strip().partition(';')
668
        if accept == 'application/json':
669
            return 'json'
670
        elif accept == 'application/xml' or accept == 'text/xml':
671
            return 'xml'
672
    
673
    return 'text'
674

    
675
def api_method(http_method=None, format_allowed=False):
676
    """Decorator function for views that implement an API method."""
677
    
678
    def decorator(func):
679
        @wraps(func)
680
        def wrapper(request, *args, **kwargs):
681
            try:
682
                if http_method and request.method != http_method:
683
                    raise BadRequest('Method not allowed.')
684
                
685
                # The args variable may contain up to (account, container, object).
686
                if len(args) > 1 and len(args[1]) > 256:
687
                    raise BadRequest('Container name too large.')
688
                if len(args) > 2 and len(args[2]) > 1024:
689
                    raise BadRequest('Object name too large.')
690
                
691
                # Fill in custom request variables.
692
                request.serialization = request_serialization(request, format_allowed)
693
                
694
                response = func(request, *args, **kwargs)
695
                update_response_headers(request, response)
696
                return response
697
            except Fault, fault:
698
                return render_fault(request, fault)
699
            except BaseException, e:
700
                logger.exception('Unexpected error: %s' % e)
701
                fault = ServiceUnavailable('Unexpected error')
702
                return render_fault(request, fault)
703
        return wrapper
704
    return decorator