Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 15d465b8

History | View | Annotate | Download (27.3 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(p):
273
    if p is not None:
274
        try:
275
            p = int(p)
276
        except ValueError:
277
            return None
278
        if p < 0:
279
            return None
280
    return p
281

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

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

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

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

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

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

    
431
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
432

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

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

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

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

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

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

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

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

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