Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ fc1b2a75

History | View | Annotate | Download (27.2 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
from functools import wraps
35
from time import time
36
from traceback import format_exc
37
from wsgiref.handlers import format_date_time
38
from binascii import hexlify
39

    
40
from django.conf import settings
41
from django.http import HttpResponse
42
from django.utils import simplejson as json
43
from django.utils.http import http_date, parse_etags
44

    
45
from pithos.api.compat import parse_http_date_safe, parse_http_date
46
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound,
47
                                Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable,
48
                                ServiceUnavailable)
49
from pithos.backends import backend
50
from pithos.backends.base import NotAllowedError
51

    
52
import datetime
53
import logging
54
import re
55
import hashlib
56
import uuid
57

    
58

    
59
logger = logging.getLogger(__name__)
60

    
61

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

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

    
79
def get_header_prefix(request, prefix):
80
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
81
    
82
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
83
    # TODO: Document or remove '~' replacing.
84
    return dict([(format_header_key(k[5:]), v.replace('~', '')) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
85

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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