Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ e0f916bb

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.http import http_date, parse_etags
43

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

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

    
57

    
58
logger = logging.getLogger(__name__)
59

    
60

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

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

    
78
def get_header_prefix(request, prefix):
79
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
80
    
81
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
82
    # TODO: Document or remove '~' replacing.
83
    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)])
84

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

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

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

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

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

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

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

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

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

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

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

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

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

    
271
def get_int_parameter(request, name):
272
    p = request.GET.get(name)
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 = request.META.get('CONTENT_LENGTH')
284
    if not content_length:
285
        raise LengthRequired('Missing Content-Length header')
286
    try:
287
        content_length = int(content_length)
288
        if content_length < 0:
289
            raise ValueError
290
    except ValueError:
291
        raise BadRequest('Invalid Content-Length header')
292
    return content_length
293

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

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

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

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

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

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

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

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

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

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

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

    
655
def update_response_headers(request, response):
656
    if request.serialization == 'xml':
657
        response['Content-Type'] = 'application/xml; charset=UTF-8'
658
    elif request.serialization == 'json':
659
        response['Content-Type'] = 'application/json; charset=UTF-8'
660
    elif not response['Content-Type']:
661
        response['Content-Type'] = 'text/plain; charset=UTF-8'
662

    
663
    if settings.TEST:
664
        response['Date'] = format_date_time(time())
665

    
666
def render_fault(request, fault):
667
    if settings.DEBUG or settings.TEST:
668
        fault.details = format_exc(fault)
669

    
670
    request.serialization = 'text'
671
    data = '\n'.join((fault.message, fault.details)) + '\n'
672
    response = HttpResponse(data, status=fault.code)
673
    update_response_headers(request, response)
674
    return response
675

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

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