Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 3436eeb0

History | View | Annotate | Download (25.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
45
from pithos.api.faults import (Fault, NotModified, BadRequest, ItemNotFound, LengthRequired,
46
                                PreconditionFailed, RangeNotSatisfiable, ServiceUnavailable)
47
from pithos.backends import backend
48

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

    
55

    
56
logger = logging.getLogger(__name__)
57

    
58

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

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

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

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

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

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

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

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

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

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

    
175
def format_permissions(permissions):
176
    ret = []
177
    if 'public' in permissions:
178
        ret.append('public')
179
    if 'private' in permissions:
180
        ret.append('private')
181
    r = ','.join(permissions.get('read', []))
182
    if r:
183
        ret.append('read=' + r)
184
    w = ','.join(permissions.get('write', []))
185
    if w:
186
        ret.append('write=' + w)
187
    return '; '.join(ret)
188

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

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

    
223
def split_container_object_string(s):
224
    if not len(s) > 0 or s[0] != '/':
225
        raise ValueError
226
    s = s[1:]
227
    pos = s.find('/')
228
    if pos == -1:
229
        raise ValueError
230
    return s[:pos], s[(pos + 1):]
231

    
232
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
233
    """Copy or move an object."""
234
    
235
    meta = get_object_meta(request)
236
    permissions = get_sharing(request)
237
    # Keep previous values of 'Content-Type' (if a new one is absent) and 'hash'.
238
    try:
239
        src_meta = backend.get_object_meta(request.user, v_account, src_container, src_name)
240
    except NameError:
241
        raise ItemNotFound('Container or object does not exist')
242
    if 'Content-Type' in meta and 'Content-Type' in src_meta:
243
        del(src_meta['Content-Type'])
244
    for k in ('Content-Type', 'hash'):
245
        if k in src_meta:
246
            meta[k] = src_meta[k]
247
    
248
    try:
249
        if move:
250
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions)
251
        else:
252
            src_version = request.META.get('HTTP_X_SOURCE_VERSION')
253
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions, src_version)
254
    except NameError:
255
        raise ItemNotFound('Container or object does not exist')
256
    except ValueError:
257
        raise BadRequest('Invalid sharing header')
258
    except AttributeError:
259
        raise Conflict('Sharing already set above or below this path in the hierarchy')
260

    
261
def get_int_parameter(request, name):
262
    p = request.GET.get(name)
263
    if p is not None:
264
        try:
265
            p = int(p)
266
        except ValueError:
267
            return None
268
        if p < 0:
269
            return None
270
    return p
271

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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