Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ a8326bef

History | View | Annotate | Download (27.6 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 import simplejson as json
43
from django.utils.http import http_date, parse_etags
44

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

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

    
58

    
59
logger = logging.getLogger(__name__)
60

    
61

    
62
def rename_meta_key(d, old, new):
63
    if old not in d:
64
        return
65
    d[new] = d[old]
66
    del(d[old])
67

    
68
def printable_header_dict(d):
69
    """Format a meta dictionary for printing out json/xml.
70
    
71
    Convert all keys to lower case and replace dashes with underscores.
72
    Format 'last_modified' timestamp.
73
    """
74
    
75
    d['last_modified'] = datetime.datetime.fromtimestamp(int(d['last_modified'])).isoformat()
76
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
77

    
78
def format_header_key(k):
79
    """Convert underscores to dashes and capitalize intra-dash strings."""
80
    
81
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
82

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

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

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

    
115
def get_container_headers(request):
116
    meta = get_header_prefix(request, 'X-Container-Meta-')
117
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
118
    return meta, policy
119

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

    
136
def get_object_headers(request):
137
    meta = get_header_prefix(request, 'X-Object-Meta-')
138
    if request.META.get('CONTENT_TYPE'):
139
        meta['Content-Type'] = request.META['CONTENT_TYPE']
140
    if request.META.get('HTTP_CONTENT_ENCODING'):
141
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
142
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
143
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
144
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
145
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
146
    return meta, get_sharing(request), get_public(request)
147

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

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

    
188
def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
189
    if permissions is None:
190
        return
191
    perm_path, perms = permissions
192
    if len(perms) == 0:
193
        return
194
    ret = []
195
    r = ','.join(perms.get('read', []))
196
    if r:
197
        ret.append('read=' + r)
198
    w = ','.join(perms.get('write', []))
199
    if w:
200
        ret.append('write=' + w)
201
    meta['X-Object-Sharing'] = '; '.join(ret)
202
    if '/'.join((v_account, v_container, v_object)) != perm_path:
203
        meta['X-Object-Shared-By'] = perm_path
204

    
205
def update_public_meta(public, meta):
206
    if not public:
207
        return
208
    meta['X-Object-Public'] = public
209

    
210
def validate_modification_preconditions(request, meta):
211
    """Check that the modified timestamp conforms with the preconditions set."""
212
    
213
    if 'modified' not in meta:
214
        return # TODO: Always return?
215
    
216
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
217
    if if_modified_since is not None:
218
        if_modified_since = parse_http_date_safe(if_modified_since)
219
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
220
        raise NotModified('Resource has not been modified')
221
    
222
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
223
    if if_unmodified_since is not None:
224
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
225
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
226
        raise PreconditionFailed('Resource has been modified')
227

    
228
def validate_matching_preconditions(request, meta):
229
    """Check that the ETag conforms with the preconditions set."""
230
    
231
    hash = meta.get('hash', None)
232
    
233
    if_match = request.META.get('HTTP_IF_MATCH')
234
    if if_match is not None:
235
        if hash is None:
236
            raise PreconditionFailed('Resource does not exist')
237
        if if_match != '*' and hash not in [x.lower() for x in parse_etags(if_match)]:
238
            raise PreconditionFailed('Resource ETag does not match')
239
    
240
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
241
    if if_none_match is not None:
242
        # TODO: If this passes, must ignore If-Modified-Since header.
243
        if hash is not None:
244
            if if_none_match == '*' or hash in [x.lower() for x in parse_etags(if_none_match)]:
245
                # TODO: Continue if an If-Modified-Since header is present.
246
                if request.method in ('HEAD', 'GET'):
247
                    raise NotModified('Resource ETag matches')
248
                raise PreconditionFailed('Resource exists or ETag matches')
249

    
250
def split_container_object_string(s):
251
    if not len(s) > 0 or s[0] != '/':
252
        raise ValueError
253
    s = s[1:]
254
    pos = s.find('/')
255
    if pos == -1:
256
        raise ValueError
257
    return s[:pos], s[(pos + 1):]
258

    
259
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
260
    """Copy or move an object."""
261
    
262
    meta, permissions, public = get_object_headers(request)
263
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
264
    try:
265
        if move:
266
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
267
        else:
268
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
269
    except NotAllowedError:
270
        raise Unauthorized('Access denied')
271
    except (NameError, IndexError):
272
        raise ItemNotFound('Container or object does not exist')
273
    except ValueError:
274
        raise BadRequest('Invalid sharing header')
275
    except AttributeError, e:
276
        raise Conflict(json.dumps(e.data))
277
    if public is not None:
278
        try:
279
            backend.update_object_public(request.user, v_account, dest_container, dest_name, public)
280
        except NotAllowedError:
281
            raise Unauthorized('Access denied')
282
        except NameError:
283
            raise ItemNotFound('Object does not exist')
284

    
285
def get_int_parameter(p):
286
    if p is not None:
287
        try:
288
            p = int(p)
289
        except ValueError:
290
            return None
291
        if p < 0:
292
            return None
293
    return p
294

    
295
def get_content_length(request):
296
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
297
    if content_length is None:
298
        raise LengthRequired('Missing or invalid Content-Length header')
299
    return content_length
300

    
301
def get_range(request, size):
302
    """Parse a Range header from the request.
303
    
304
    Either returns None, when the header is not existent or should be ignored,
305
    or a list of (offset, length) tuples - should be further checked.
306
    """
307
    
308
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
309
    if not ranges.startswith('bytes='):
310
        return None
311
    
312
    ret = []
313
    for r in (x.strip() for x in ranges[6:].split(',')):
314
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
315
        m = p.match(r)
316
        if not m:
317
            return None
318
        offset = m.group('offset')
319
        upto = m.group('upto')
320
        if offset == '' and upto == '':
321
            return None
322
        
323
        if offset != '':
324
            offset = int(offset)
325
            if upto != '':
326
                upto = int(upto)
327
                if offset > upto:
328
                    return None
329
                ret.append((offset, upto - offset + 1))
330
            else:
331
                ret.append((offset, size - offset))
332
        else:
333
            length = int(upto)
334
            ret.append((size - length, length))
335
    
336
    return ret
337

    
338
def get_content_range(request):
339
    """Parse a Content-Range header from the request.
340
    
341
    Either returns None, when the header is not existent or should be ignored,
342
    or an (offset, length, total) tuple - check as length, total may be None.
343
    Returns (None, None, None) if the provided range is '*/*'.
344
    """
345
    
346
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
347
    if not ranges:
348
        return None
349
    
350
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
351
    m = p.match(ranges)
352
    if not m:
353
        if ranges == 'bytes */*':
354
            return (None, None, None)
355
        return None
356
    offset = int(m.group('offset'))
357
    upto = m.group('upto')
358
    total = m.group('total')
359
    if upto != '':
360
        upto = int(upto)
361
    else:
362
        upto = None
363
    if total != '*':
364
        total = int(total)
365
    else:
366
        total = None
367
    if (upto is not None and offset > upto) or \
368
        (total is not None and offset >= total) or \
369
        (total is not None and upto is not None and upto >= total):
370
        return None
371
    
372
    if upto is None:
373
        length = None
374
    else:
375
        length = upto - offset + 1
376
    return (offset, length, total)
377

    
378
def get_sharing(request):
379
    """Parse an X-Object-Sharing header from the request.
380
    
381
    Raises BadRequest on error.
382
    """
383
    
384
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
385
    if permissions is None:
386
        return None
387
    
388
    ret = {}
389
    permissions = permissions.replace(' ', '')
390
    if permissions == '':
391
        return ret
392
    for perm in (x for x in permissions.split(';')):
393
        if perm.startswith('read='):
394
            ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
395
            if '' in ret['read']:
396
                ret['read'].remove('')
397
            if '*' in ret['read']:
398
                ret['read'] = ['*']
399
            if len(ret['read']) == 0:
400
                raise BadRequest('Bad X-Object-Sharing header value')
401
        elif perm.startswith('write='):
402
            ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
403
            if '' in ret['write']:
404
                ret['write'].remove('')
405
            if '*' in ret['write']:
406
                ret['write'] = ['*']
407
            if len(ret['write']) == 0:
408
                raise BadRequest('Bad X-Object-Sharing header value')
409
        else:
410
            raise BadRequest('Bad X-Object-Sharing header value')
411
    return ret
412

    
413
def get_public(request):
414
    """Parse an X-Object-Public header from the request.
415
    
416
    Raises BadRequest on error.
417
    """
418
    
419
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
420
    if public is None:
421
        return None
422
    
423
    public = public.replace(' ', '').lower()
424
    if public == 'true':
425
        return True
426
    elif public == 'false' or public == '':
427
        return False
428
    raise BadRequest('Bad X-Object-Public header value')
429

    
430
def raw_input_socket(request):
431
    """Return the socket for reading the rest of the request."""
432
    
433
    server_software = request.META.get('SERVER_SOFTWARE')
434
    if server_software and server_software.startswith('mod_python'):
435
        return request._req
436
    if 'wsgi.input' in request.environ:
437
        return request.environ['wsgi.input']
438
    raise ServiceUnavailable('Unknown server software')
439

    
440
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
441

    
442
def socket_read_iterator(sock, length=0, blocksize=4096):
443
    """Return a maximum of blocksize data read from the socket in each iteration.
444
    
445
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
446
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
447
    """
448
    
449
    if length < 0: # Chunked transfers
450
        data = ''
451
        while length < MAX_UPLOAD_SIZE:
452
            # Get chunk size.
453
            if hasattr(sock, 'readline'):
454
                chunk_length = sock.readline()
455
            else:
456
                chunk_length = ''
457
                while chunk_length[-1:] != '\n':
458
                    chunk_length += sock.read(1)
459
                chunk_length.strip()
460
            pos = chunk_length.find(';')
461
            if pos >= 0:
462
                chunk_length = chunk_length[:pos]
463
            try:
464
                chunk_length = int(chunk_length, 16)
465
            except Exception, e:
466
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
467
            # Check if done.
468
            if chunk_length == 0:
469
                if len(data) > 0:
470
                    yield data
471
                return
472
            # Get the actual data.
473
            while chunk_length > 0:
474
                chunk = sock.read(min(chunk_length, blocksize))
475
                chunk_length -= len(chunk)
476
                if length > 0:
477
                    length += len(chunk)
478
                data += chunk
479
                if len(data) >= blocksize:
480
                    ret = data[:blocksize]
481
                    data = data[blocksize:]
482
                    yield ret
483
            sock.read(2) # CRLF
484
        raise BadRequest('Maximum size is reached')
485
    else:
486
        if length > MAX_UPLOAD_SIZE:
487
            raise BadRequest('Maximum size is reached')
488
        while length > 0:
489
            data = sock.read(min(length, blocksize))
490
            length -= len(data)
491
            yield data
492

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

    
576
def object_data_response(request, sizes, hashmaps, meta, public=False):
577
    """Get the HttpResponse object for replying with the object's data."""
578
    
579
    # Range handling.
580
    size = sum(sizes)
581
    ranges = get_range(request, size)
582
    if ranges is None:
583
        ranges = [(0, size)]
584
        ret = 200
585
    else:
586
        check = [True for offset, length in ranges if
587
                    length <= 0 or length > size or
588
                    offset < 0 or offset >= size or
589
                    offset + length > size]
590
        if len(check) > 0:
591
            raise RangeNotSatisfiable('Requested range exceeds object limits')
592
        ret = 206
593
        if_range = request.META.get('HTTP_IF_RANGE')
594
        if if_range:
595
            try:
596
                # Modification time has passed instead.
597
                last_modified = parse_http_date(if_range)
598
                if last_modified != meta['modified']:
599
                    ranges = [(0, size)]
600
                    ret = 200
601
            except ValueError:
602
                if if_range != meta['hash']:
603
                    ranges = [(0, size)]
604
                    ret = 200
605
    
606
    if ret == 206 and len(ranges) > 1:
607
        boundary = uuid.uuid4().hex
608
    else:
609
        boundary = ''
610
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
611
    response = HttpResponse(wrapper, status=ret)
612
    put_object_headers(response, meta, public)
613
    if ret == 206:
614
        if len(ranges) == 1:
615
            offset, length = ranges[0]
616
            response['Content-Length'] = length # Update with the correct length.
617
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
618
        else:
619
            del(response['Content-Length'])
620
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
621
    return response
622

    
623
def put_object_block(hashmap, data, offset):
624
    """Put one block of data at the given offset."""
625
    
626
    bi = int(offset / backend.block_size)
627
    bo = offset % backend.block_size
628
    bl = min(len(data), backend.block_size - bo)
629
    if bi < len(hashmap):
630
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
631
    else:
632
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
633
    return bl # Return ammount of data written.
634

    
635
def hashmap_hash(hashmap):
636
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
637
    
638
    def subhash(d):
639
        h = hashlib.new(backend.hash_algorithm)
640
        h.update(d)
641
        return h.digest()
642
    
643
    if len(hashmap) == 0:
644
        return hexlify(subhash(''))
645
    if len(hashmap) == 1:
646
        return hexlify(subhash(hashmap[0]))
647
    s = 2
648
    while s < len(hashmap):
649
        s = s * 2
650
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
651
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
652
    while len(h) > 1:
653
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
654
    return hexlify(h[0])
655

    
656
def update_response_headers(request, response):
657
    if request.serialization == 'xml':
658
        response['Content-Type'] = 'application/xml; charset=UTF-8'
659
    elif request.serialization == 'json':
660
        response['Content-Type'] = 'application/json; charset=UTF-8'
661
    elif not response['Content-Type']:
662
        response['Content-Type'] = 'text/plain; charset=UTF-8'
663
    
664
    if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
665
        response['Content-Length'] = len(response.content)
666
    
667
    if settings.TEST:
668
        response['Date'] = format_date_time(time())
669

    
670
def render_fault(request, fault):
671
    if settings.DEBUG or settings.TEST:
672
        fault.details = format_exc(fault)
673
    
674
    request.serialization = 'text'
675
    data = '\n'.join((fault.message, fault.details)) + '\n'
676
    response = HttpResponse(data, status=fault.code)
677
    update_response_headers(request, response)
678
    return response
679

    
680
def request_serialization(request, format_allowed=False):
681
    """Return the serialization format requested.
682
    
683
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
684
    """
685
    
686
    if not format_allowed:
687
        return 'text'
688
    
689
    format = request.GET.get('format')
690
    if format == 'json':
691
        return 'json'
692
    elif format == 'xml':
693
        return 'xml'
694
    
695
#     for item in request.META.get('HTTP_ACCEPT', '').split(','):
696
#         accept, sep, rest = item.strip().partition(';')
697
#         if accept == 'application/json':
698
#             return 'json'
699
#         elif accept == 'application/xml' or accept == 'text/xml':
700
#             return 'xml'
701
    
702
    return 'text'
703

    
704
def api_method(http_method=None, format_allowed=False):
705
    """Decorator function for views that implement an API method."""
706
    
707
    def decorator(func):
708
        @wraps(func)
709
        def wrapper(request, *args, **kwargs):
710
            try:
711
                if http_method and request.method != http_method:
712
                    raise BadRequest('Method not allowed.')
713
                
714
                # The args variable may contain up to (account, container, object).
715
                if len(args) > 1 and len(args[1]) > 256:
716
                    raise BadRequest('Container name too large.')
717
                if len(args) > 2 and len(args[2]) > 1024:
718
                    raise BadRequest('Object name too large.')
719
                
720
                # Fill in custom request variables.
721
                request.serialization = request_serialization(request, format_allowed)
722
                
723
                response = func(request, *args, **kwargs)
724
                update_response_headers(request, response)
725
                return response
726
            except Fault, fault:
727
                return render_fault(request, fault)
728
            except BaseException, e:
729
                logger.exception('Unexpected error: %s' % e)
730
                fault = ServiceUnavailable('Unexpected error')
731
                return render_fault(request, fault)
732
        return wrapper
733
    return decorator