Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ ab2e317e

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

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

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

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

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

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

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

    
198
def update_public_meta(public, meta):
199
    if not public:
200
        return
201
    meta['X-Object-Public'] = public
202

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

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

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

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

    
272
def get_int_parameter(request, name):
273
    p = request.GET.get(name)
274
    if p is not None:
275
        try:
276
            p = int(p)
277
        except ValueError:
278
            return None
279
        if p < 0:
280
            return None
281
    return p
282

    
283
def get_content_length(request):
284
    content_length = request.META.get('CONTENT_LENGTH')
285
    if not content_length:
286
        raise LengthRequired('Missing Content-Length header')
287
    try:
288
        content_length = int(content_length)
289
        if content_length < 0:
290
            raise ValueError
291
    except ValueError:
292
        raise BadRequest('Invalid Content-Length header')
293
    return content_length
294

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

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

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

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

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

    
438
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
439

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

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

    
575
def object_data_response(request, sizes, hashmaps, meta, public=False):
576
    """Get the HttpResponse object for replying with the object's data."""
577
    
578
    # Range handling.
579
    size = sum(sizes)
580
    ranges = get_range(request, size)
581
    if ranges is None:
582
        ranges = [(0, size)]
583
        ret = 200
584
    else:
585
        check = [True for offset, length in ranges if
586
                    length <= 0 or length > size or
587
                    offset < 0 or offset >= size or
588
                    offset + length > size]
589
        if len(check) > 0:
590
            raise RangeNotSatisfiable('Requested range exceeds object limits')
591
        ret = 206
592
        if_range = request.META.get('HTTP_IF_RANGE', '')
593
        if if_range and if_range.startswith('If-Range:'):
594
            if_range = if_range.split('If-Range:')[1]
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 settings.TEST:
665
        response['Date'] = format_date_time(time())
666

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

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

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