Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ aa4fac11

History | View | Annotate | Download (25.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.http import http_date, parse_etags
43

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

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

    
56

    
57
logger = logging.getLogger(__name__)
58

    
59

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

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

    
77
def get_meta_prefix(request, prefix):
78
    """Get all prefix-* request headers in a dict. Reformat keys with format_meta_key()."""
79
    
80
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
81
    return dict([(format_meta_key(k[5:]), v) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
82

    
83
def get_account_meta(request):
84
    """Get metadata from an account request."""
85
    
86
    meta = get_meta_prefix(request, 'X-Account-Meta-')    
87
    return meta
88

    
89
def put_account_meta(response, meta):
90
    """Put metadata in an account response."""
91
    
92
    response['X-Account-Container-Count'] = meta['count']
93
    response['X-Account-Bytes-Used'] = meta['bytes']
94
    if 'modified' in meta:
95
        response['Last-Modified'] = http_date(int(meta['modified']))
96
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
97
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
98
    if 'until_timestamp' in meta:
99
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
100

    
101
def get_container_meta(request):
102
    """Get metadata from a container request."""
103
    
104
    meta = get_meta_prefix(request, 'X-Container-Meta-')
105
    return meta
106

    
107
def put_container_meta(response, meta):
108
    """Put metadata in a container response."""
109
    
110
    response['X-Container-Object-Count'] = meta['count']
111
    response['X-Container-Bytes-Used'] = meta['bytes']
112
    response['Last-Modified'] = http_date(int(meta['modified']))
113
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
114
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
115
    response['X-Container-Object-Meta'] = [x[14:] for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
116
    response['X-Container-Block-Size'] = backend.block_size
117
    response['X-Container-Block-Hash'] = backend.hash_algorithm
118
    if 'until_timestamp' in meta:
119
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
120

    
121
def get_object_meta(request):
122
    """Get metadata from an object request."""
123
    
124
    meta = get_meta_prefix(request, 'X-Object-Meta-')
125
    if request.META.get('CONTENT_TYPE'):
126
        meta['Content-Type'] = request.META['CONTENT_TYPE']
127
    if request.META.get('HTTP_CONTENT_ENCODING'):
128
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
129
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
130
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
131
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
132
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
133
    return meta
134

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

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

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

    
194
def validate_modification_preconditions(request, meta):
195
    """Check that the modified timestamp conforms with the preconditions set."""
196
    
197
    if 'modified' not in meta:
198
        return # TODO: Always return?
199
    
200
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
201
    if if_modified_since is not None:
202
        if_modified_since = parse_http_date_safe(if_modified_since)
203
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
204
        raise NotModified('Resource has not been modified')
205
    
206
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
207
    if if_unmodified_since is not None:
208
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
209
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
210
        raise PreconditionFailed('Resource has been modified')
211

    
212
def validate_matching_preconditions(request, meta):
213
    """Check that the ETag conforms with the preconditions set."""
214
    
215
    if 'hash' not in meta:
216
        return # TODO: Always return?
217
    
218
    if_match = request.META.get('HTTP_IF_MATCH')
219
    if if_match is not None and if_match != '*':
220
        if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
221
            raise PreconditionFailed('Resource Etag does not match')
222
    
223
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
224
    if if_none_match is not None:
225
        if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
226
            raise NotModified('Resource Etag matches')
227

    
228
def split_container_object_string(s):
229
    if not len(s) > 0 or s[0] != '/':
230
        raise ValueError
231
    s = s[1:]
232
    pos = s.find('/')
233
    if pos == -1:
234
        raise ValueError
235
    return s[:pos], s[(pos + 1):]
236

    
237
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
238
    """Copy or move an object."""
239
    
240
    meta = get_object_meta(request)
241
    permissions = get_sharing(request)
242
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
243
    try:
244
        if move:
245
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
246
        else:
247
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
248
    except NotAllowedError:
249
        raise Unauthorized('Access denied')
250
    except NameError, IndexError:
251
        raise ItemNotFound('Container or object does not exist')
252
    except ValueError:
253
        raise BadRequest('Invalid sharing header')
254
    except AttributeError:
255
        raise Conflict('Sharing already set above or below this path in the hierarchy')
256

    
257
def get_int_parameter(request, name):
258
    p = request.GET.get(name)
259
    if p is not None:
260
        try:
261
            p = int(p)
262
        except ValueError:
263
            return None
264
        if p < 0:
265
            return None
266
    return p
267

    
268
def get_content_length(request):
269
    content_length = request.META.get('CONTENT_LENGTH')
270
    if not content_length:
271
        raise LengthRequired('Missing Content-Length header')
272
    try:
273
        content_length = int(content_length)
274
        if content_length < 0:
275
            raise ValueError
276
    except ValueError:
277
        raise BadRequest('Invalid Content-Length header')
278
    return content_length
279

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

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

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

    
390
def raw_input_socket(request):
391
    """Return the socket for reading the rest of the request."""
392
    
393
    server_software = request.META.get('SERVER_SOFTWARE')
394
    if not server_software:
395
        if 'wsgi.input' in request.environ:
396
            return request.environ['wsgi.input']
397
        raise ServiceUnavailable('Unknown server software')
398
    if server_software.startswith('WSGIServer'):
399
        return request.environ['wsgi.input']
400
    elif server_software.startswith('mod_python'):
401
        return request._req
402
    raise ServiceUnavailable('Unknown server software')
403

    
404
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
405

    
406
def socket_read_iterator(sock, length=0, blocksize=4096):
407
    """Return a maximum of blocksize data read from the socket in each iteration.
408
    
409
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
410
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
411
    """
412
    
413
    if length < 0: # Chunked transfers
414
        data = ''
415
        while length < MAX_UPLOAD_SIZE:
416
            # Get chunk size.
417
            if hasattr(sock, 'readline'):
418
                chunk_length = sock.readline()
419
            else:
420
                chunk_length = ''
421
                while chunk_length[-1:] != '\n':
422
                    chunk_length += sock.read(1)
423
                chunk_length.strip()
424
            pos = chunk_length.find(';')
425
            if pos >= 0:
426
                chunk_length = chunk_length[:pos]
427
            try:
428
                chunk_length = int(chunk_length, 16)
429
            except Exception, e:
430
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
431
            # Check if done.
432
            if chunk_length == 0:
433
                if len(data) > 0:
434
                    yield data
435
                return
436
            # Get the actual data.
437
            while chunk_length > 0:
438
                chunk = sock.read(min(chunk_length, blocksize))
439
                chunk_length -= len(chunk)
440
                if length > 0:
441
                    length += len(chunk)
442
                data += chunk
443
                if len(data) >= blocksize:
444
                    ret = data[:blocksize]
445
                    data = data[blocksize:]
446
                    yield ret
447
            sock.read(2) # CRLF
448
        # TODO: Raise something to note that maximum size is reached.
449
    else:
450
        if length > MAX_UPLOAD_SIZE:
451
            # TODO: Raise something to note that maximum size is reached.
452
            pass
453
        while length > 0:
454
            data = sock.read(min(length, blocksize))
455
            length -= len(data)
456
            yield data
457

    
458
class ObjectWrapper(object):
459
    """Return the object's data block-per-block in each iteration.
460
    
461
    Read from the object using the offset and length provided in each entry of the range list.
462
    """
463
    
464
    def __init__(self, ranges, sizes, hashmaps, boundary):
465
        self.ranges = ranges
466
        self.sizes = sizes
467
        self.hashmaps = hashmaps
468
        self.boundary = boundary
469
        self.size = sum(self.sizes)
470
        
471
        self.file_index = 0
472
        self.block_index = 0
473
        self.block_hash = -1
474
        self.block = ''
475
        
476
        self.range_index = -1
477
        self.offset, self.length = self.ranges[0]
478
    
479
    def __iter__(self):
480
        return self
481
    
482
    def part_iterator(self):
483
        if self.length > 0:
484
            # Get the file for the current offset.
485
            file_size = self.sizes[self.file_index]
486
            while self.offset >= file_size:
487
                self.offset -= file_size
488
                self.file_index += 1
489
                file_size = self.sizes[self.file_index]
490
            
491
            # Get the block for the current position.
492
            self.block_index = int(self.offset / backend.block_size)
493
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
494
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
495
                try:
496
                    self.block = backend.get_block(self.block_hash)
497
                except NameError:
498
                    raise ItemNotFound('Block does not exist')
499
            
500
            # Get the data from the block.
501
            bo = self.offset % backend.block_size
502
            bl = min(self.length, len(self.block) - bo)
503
            data = self.block[bo:bo + bl]
504
            self.offset += bl
505
            self.length -= bl
506
            return data
507
        else:
508
            raise StopIteration
509
    
510
    def next(self):
511
        if len(self.ranges) == 1:
512
            return self.part_iterator()
513
        if self.range_index == len(self.ranges):
514
            raise StopIteration
515
        try:
516
            if self.range_index == -1:
517
                raise StopIteration
518
            return self.part_iterator()
519
        except StopIteration:
520
            self.range_index += 1
521
            out = []
522
            if self.range_index < len(self.ranges):
523
                # Part header.
524
                self.offset, self.length = self.ranges[self.range_index]
525
                self.file_index = 0
526
                if self.range_index > 0:
527
                    out.append('')
528
                out.append('--' + self.boundary)
529
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
530
                out.append('Content-Transfer-Encoding: binary')
531
                out.append('')
532
                out.append('')
533
                return '\r\n'.join(out)
534
            else:
535
                # Footer.
536
                out.append('')
537
                out.append('--' + self.boundary + '--')
538
                out.append('')
539
                return '\r\n'.join(out)
540

    
541
def object_data_response(request, sizes, hashmaps, meta, public=False):
542
    """Get the HttpResponse object for replying with the object's data."""
543
    
544
    # Range handling.
545
    size = sum(sizes)
546
    ranges = get_range(request, size)
547
    if ranges is None:
548
        ranges = [(0, size)]
549
        ret = 200
550
    else:
551
        check = [True for offset, length in ranges if
552
                    length <= 0 or length > size or
553
                    offset < 0 or offset >= size or
554
                    offset + length > size]
555
        if len(check) > 0:
556
            raise RangeNotSatisfiable('Requested range exceeds object limits')        
557
        ret = 206
558
    
559
    if ret == 206 and len(ranges) > 1:
560
        boundary = uuid.uuid4().hex
561
    else:
562
        boundary = ''
563
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
564
    response = HttpResponse(wrapper, status=ret)
565
    put_object_meta(response, meta, public)
566
    if ret == 206:
567
        if len(ranges) == 1:
568
            offset, length = ranges[0]
569
            response['Content-Length'] = length # Update with the correct length.
570
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
571
        else:
572
            del(response['Content-Length'])
573
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
574
    return response
575

    
576
def put_object_block(hashmap, data, offset):
577
    """Put one block of data at the given offset."""
578
    
579
    bi = int(offset / backend.block_size)
580
    bo = offset % backend.block_size
581
    bl = min(len(data), backend.block_size - bo)
582
    if bi < len(hashmap):
583
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
584
    else:
585
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
586
    return bl # Return ammount of data written.
587

    
588
def hashmap_hash(hashmap):
589
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
590
    
591
    def subhash(d):
592
        h = hashlib.new(backend.hash_algorithm)
593
        h.update(d)
594
        return h.digest()
595
    
596
    if len(hashmap) == 0:
597
        return hexlify(subhash(''))
598
    if len(hashmap) == 1:
599
        return hexlify(subhash(hashmap[0]))
600
    s = 2
601
    while s < len(hashmap):
602
        s = s * 2
603
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
604
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
605
    while len(h) > 1:
606
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
607
    return hexlify(h[0])
608

    
609
def update_response_headers(request, response):
610
    if request.serialization == 'xml':
611
        response['Content-Type'] = 'application/xml; charset=UTF-8'
612
    elif request.serialization == 'json':
613
        response['Content-Type'] = 'application/json; charset=UTF-8'
614
    elif not response['Content-Type']:
615
        response['Content-Type'] = 'text/plain; charset=UTF-8'
616

    
617
    if settings.TEST:
618
        response['Date'] = format_date_time(time())
619

    
620
def render_fault(request, fault):
621
    if settings.DEBUG or settings.TEST:
622
        fault.details = format_exc(fault)
623

    
624
    request.serialization = 'text'
625
    data = '\n'.join((fault.message, fault.details)) + '\n'
626
    response = HttpResponse(data, status=fault.code)
627
    update_response_headers(request, response)
628
    return response
629

    
630
def request_serialization(request, format_allowed=False):
631
    """Return the serialization format requested.
632
    
633
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
634
    """
635
    
636
    if not format_allowed:
637
        return 'text'
638
    
639
    format = request.GET.get('format')
640
    if format == 'json':
641
        return 'json'
642
    elif format == 'xml':
643
        return 'xml'
644
    
645
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
646
        accept, sep, rest = item.strip().partition(';')
647
        if accept == 'application/json':
648
            return 'json'
649
        elif accept == 'application/xml' or accept == 'text/xml':
650
            return 'xml'
651
    
652
    return 'text'
653

    
654
def api_method(http_method=None, format_allowed=False):
655
    """Decorator function for views that implement an API method."""
656
    
657
    def decorator(func):
658
        @wraps(func)
659
        def wrapper(request, *args, **kwargs):
660
            try:
661
                if http_method and request.method != http_method:
662
                    raise BadRequest('Method not allowed.')
663
                
664
                # The args variable may contain up to (account, container, object).
665
                if len(args) > 1 and len(args[1]) > 256:
666
                    raise BadRequest('Container name too large.')
667
                if len(args) > 2 and len(args[2]) > 1024:
668
                    raise BadRequest('Object name too large.')
669
                
670
                # Fill in custom request variables.
671
                request.serialization = request_serialization(request, format_allowed)
672
                
673
                response = func(request, *args, **kwargs)
674
                update_response_headers(request, response)
675
                return response
676
            except Fault, fault:
677
                return render_fault(request, fault)
678
            except BaseException, e:
679
                logger.exception('Unexpected error: %s' % e)
680
                fault = ServiceUnavailable('Unexpected error')
681
                return render_fault(request, fault)
682
        return wrapper
683
    return decorator