Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 1495b972

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
649
def update_response_headers(request, response):
650
    if request.serialization == 'xml':
651
        response['Content-Type'] = 'application/xml; charset=UTF-8'
652
    elif request.serialization == 'json':
653
        response['Content-Type'] = 'application/json; charset=UTF-8'
654
    elif not response['Content-Type']:
655
        response['Content-Type'] = 'text/plain; charset=UTF-8'
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