Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ f6c97079

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

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

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

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

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

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

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

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

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

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

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

    
202
def update_public_meta(public, meta):
203
    if not public:
204
        return
205
    meta['X-Object-Public'] = public
206

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

    
225
def validate_matching_preconditions(request, meta):
226
    """Check that the ETag conforms with the preconditions set."""
227
    
228
    if 'hash' not in meta:
229
        return # TODO: Always return?
230
    
231
    if_match = request.META.get('HTTP_IF_MATCH')
232
    if if_match is not None and if_match != '*':
233
        if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
234
            raise PreconditionFailed('Resource Etag does not match')
235
    
236
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
237
    if if_none_match is not None:
238
        if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
239
            raise NotModified('Resource Etag matches')
240

    
241
def split_container_object_string(s):
242
    if not len(s) > 0 or s[0] != '/':
243
        raise ValueError
244
    s = s[1:]
245
    pos = s.find('/')
246
    if pos == -1:
247
        raise ValueError
248
    return s[:pos], s[(pos + 1):]
249

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

    
276
def get_int_parameter(p):
277
    if p is not None:
278
        try:
279
            p = int(p)
280
        except ValueError:
281
            return None
282
        if p < 0:
283
            return None
284
    return p
285

    
286
def get_content_length(request):
287
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
288
    if content_length is None:
289
        raise LengthRequired('Missing or invalid Content-Length header')
290
    return content_length
291

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

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

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

    
404
def get_public(request):
405
    """Parse an X-Object-Public header from the request.
406
    
407
    Raises BadRequest on error.
408
    """
409
    
410
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
411
    if public is None:
412
        return None
413
    
414
    public = public.replace(' ', '').lower()
415
    if public == 'true':
416
        return True
417
    elif public == 'false' or public == '':
418
        return False
419
    raise BadRequest('Bad X-Object-Public header value')
420

    
421
def raw_input_socket(request):
422
    """Return the socket for reading the rest of the request."""
423
    
424
    server_software = request.META.get('SERVER_SOFTWARE')
425
    if not server_software:
426
        if 'wsgi.input' in request.environ:
427
            return request.environ['wsgi.input']
428
        raise ServiceUnavailable('Unknown server software')
429
    if server_software.startswith('WSGIServer'):
430
        return request.environ['wsgi.input']
431
    elif server_software.startswith('mod_python'):
432
        return request._req
433
    raise ServiceUnavailable('Unknown server software')
434

    
435
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
436

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

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

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

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

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

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

    
665
def render_fault(request, fault):
666
    if settings.DEBUG or settings.TEST:
667
        fault.details = format_exc(fault)
668
    
669
    request.serialization = 'text'
670
    data = '\n'.join((fault.message, fault.details)) + '\n'
671
    response = HttpResponse(data, status=fault.code)
672
    update_response_headers(request, response)
673
    return response
674

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

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