Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ e8886082

History | View | Annotate | Download (25.6 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 'private' in permissions:
178
        ret.append('private')
179
    r = ','.join(permissions.get('read', []))
180
    if r:
181
        ret.append('read=' + r)
182
    w = ','.join(permissions.get('write', []))
183
    if w:
184
        ret.append('write=' + w)
185
    return '; '.join(ret)
186

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

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

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

    
230
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
231
    """Copy or move an object."""
232
    
233
    meta = get_object_meta(request)
234
    permissions = get_sharing(request)
235
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
236
    
237
    try:
238
        if move:
239
            src_meta = backend.get_object_meta(request.user, v_account, src_container, src_name)
240
        else:
241
            src_meta = backend.get_object_meta(request.user, v_account, src_container, src_name, src_version)
242
    except NameError, IndexError:
243
        raise ItemNotFound('Container or object does not exist')
244
    
245
    # Keep previous values of 'Content-Type' (if a new one is absent) and 'hash'.
246
    if 'Content-Type' in meta and 'Content-Type' in src_meta:
247
        del(src_meta['Content-Type'])
248
    for k in ('Content-Type', 'hash'):
249
        if k in src_meta:
250
            meta[k] = src_meta[k]
251
    
252
    try:
253
        if move:
254
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions)
255
        else:
256
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, True, permissions, src_version)
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

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

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

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

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

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

    
400
def raw_input_socket(request):
401
    """Return the socket for reading the rest of the request."""
402
    
403
    server_software = request.META.get('SERVER_SOFTWARE')
404
    if not server_software:
405
        if 'wsgi.input' in request.environ:
406
            return request.environ['wsgi.input']
407
        raise ServiceUnavailable('Unknown server software')
408
    if server_software.startswith('WSGIServer'):
409
        return request.environ['wsgi.input']
410
    elif server_software.startswith('mod_python'):
411
        return request._req
412
    raise ServiceUnavailable('Unknown server software')
413

    
414
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
415

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

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

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

    
586
def put_object_block(hashmap, data, offset):
587
    """Put one block of data at the given offset."""
588
    
589
    bi = int(offset / backend.block_size)
590
    bo = offset % backend.block_size
591
    bl = min(len(data), backend.block_size - bo)
592
    if bi < len(hashmap):
593
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
594
    else:
595
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
596
    return bl # Return ammount of data written.
597

    
598
def hashmap_hash(hashmap):
599
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
600
    
601
    def subhash(d):
602
        h = hashlib.new(backend.hash_algorithm)
603
        h.update(d)
604
        return h.digest()
605
    
606
    if len(hashmap) == 0:
607
        return hexlify(subhash(''))
608
    if len(hashmap) == 1:
609
        return hexlify(subhash(hashmap[0]))
610
    s = 2
611
    while s < len(hashmap):
612
        s = s * 2
613
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
614
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
615
    while len(h) > 1:
616
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
617
    return hexlify(h[0])
618

    
619
def update_response_headers(request, response):
620
    if request.serialization == 'xml':
621
        response['Content-Type'] = 'application/xml; charset=UTF-8'
622
    elif request.serialization == 'json':
623
        response['Content-Type'] = 'application/json; charset=UTF-8'
624
    elif not response['Content-Type']:
625
        response['Content-Type'] = 'text/plain; charset=UTF-8'
626

    
627
    if settings.TEST:
628
        response['Date'] = format_date_time(time())
629

    
630
def render_fault(request, fault):
631
    if settings.DEBUG or settings.TEST:
632
        fault.details = format_exc(fault)
633

    
634
    request.serialization = 'text'
635
    data = '\n'.join((fault.message, fault.details)) + '\n'
636
    response = HttpResponse(data, status=fault.code)
637
    update_response_headers(request, response)
638
    return response
639

    
640
def request_serialization(request, format_allowed=False):
641
    """Return the serialization format requested.
642
    
643
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
644
    """
645
    
646
    if not format_allowed:
647
        return 'text'
648
    
649
    format = request.GET.get('format')
650
    if format == 'json':
651
        return 'json'
652
    elif format == 'xml':
653
        return 'xml'
654
    
655
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
656
        accept, sep, rest = item.strip().partition(';')
657
        if accept == 'application/json':
658
            return 'json'
659
        elif accept == 'application/xml' or accept == 'text/xml':
660
            return 'xml'
661
    
662
    return 'text'
663

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