Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 7912c32e

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
    response['Last-Modified'] = http_date(int(meta['modified']))
104
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
105
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
106
    if 'until_timestamp' in meta:
107
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
108
    for k, v in groups.iteritems():
109
        response[format_header_key('X-Account-Group-' + k).encode('utf-8')] = (','.join(v)).encode('utf-8')
110

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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