Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ b0a2d1a6

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'] = list(set([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'] = list(set([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
        raise BadRequest('Maximum size is reached')
476
    else:
477
        if length > MAX_UPLOAD_SIZE:
478
            raise BadRequest('Maximum size is reached')
479
        while length > 0:
480
            data = sock.read(min(length, blocksize))
481
            length -= len(data)
482
            yield data
483

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

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

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

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

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

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

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

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