* Account groups.
[pithos] / pithos / api / util.py
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, Unauthorized, ItemNotFound,
46                                 LengthRequired, PreconditionFailed, RangeNotSatisfiable,
47                                 ServiceUnavailable)
48 from pithos.backends import backend
49 from pithos.backends.base import NotAllowedError
50
51 import datetime
52 import logging
53 import re
54 import hashlib
55 import uuid
56
57
58 logger = logging.getLogger(__name__)
59
60
61 def printable_header_dict(d):
62     """Format a meta dictionary for printing out json/xml.
63     
64     Convert all keys to lower case and replace dashes to underscores.
65     Change 'modified' key from backend to 'last_modified' and format date.
66     """
67     
68     if 'modified' in d:
69         d['last_modified'] = datetime.datetime.fromtimestamp(int(d['modified'])).isoformat()
70         del(d['modified'])
71     return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
72
73 def format_header_key(k):
74     """Convert underscores to dashes and capitalize intra-dash strings."""
75     
76     return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
77
78 def get_header_prefix(request, prefix):
79     """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
80     
81     prefix = 'HTTP_' + prefix.upper().replace('-', '_')
82     # TODO: Document or remove '~' replacing.
83     return dict([(format_header_key(k[5:]), v.replace('~', '')) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
84
85 def get_account_headers(request):
86     meta = get_header_prefix(request, 'X-Account-Meta-')
87     groups = {}
88     for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
89         n = k[16:].lower()
90         if '-' in n or '_' in n:
91             raise BadRequest('Bad characters in group name')
92         groups[n] = v.replace(' ', '').split(',')
93         if '' in groups[n]:
94             groups[n].remove('')
95     return meta, groups
96
97 def put_account_headers(response, meta, groups):
98     response['X-Account-Container-Count'] = meta['count']
99     response['X-Account-Bytes-Used'] = meta['bytes']
100     if 'modified' in meta:
101         response['Last-Modified'] = http_date(int(meta['modified']))
102     for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
103         response[k.encode('utf-8')] = meta[k].encode('utf-8')
104     if 'until_timestamp' in meta:
105         response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
106     for k, v in groups.iteritems():
107         response[format_header_key('X-Account-Group-' + k).encode('utf-8')] = (','.join(v)).encode('utf-8')
108
109 def get_container_headers(request):
110     meta = get_header_prefix(request, 'X-Container-Meta-')
111     policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
112     return meta, policy
113
114 def put_container_headers(response, meta, policy):
115     response['X-Container-Object-Count'] = meta['count']
116     response['X-Container-Bytes-Used'] = meta['bytes']
117     response['Last-Modified'] = http_date(int(meta['modified']))
118     for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
119         response[k.encode('utf-8')] = meta[k].encode('utf-8')
120     response['X-Container-Object-Meta'] = [x[14:] for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
121     response['X-Container-Block-Size'] = backend.block_size
122     response['X-Container-Block-Hash'] = backend.hash_algorithm
123     if 'until_timestamp' in meta:
124         response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
125     for k, v in policy.iteritems():
126         response[format_header_key('X-Container-Policy-' + k).encode('utf-8')] = v.encode('utf-8')
127
128 def get_object_headers(request):
129     meta = get_header_prefix(request, 'X-Object-Meta-')
130     if request.META.get('CONTENT_TYPE'):
131         meta['Content-Type'] = request.META['CONTENT_TYPE']
132     if request.META.get('HTTP_CONTENT_ENCODING'):
133         meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
134     if request.META.get('HTTP_CONTENT_DISPOSITION'):
135         meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
136     if request.META.get('HTTP_X_OBJECT_MANIFEST'):
137         meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
138     return meta, get_sharing(request), get_public(request)
139
140 def put_object_headers(response, meta, restricted=False):
141     response['ETag'] = meta['hash']
142     response['Content-Length'] = meta['bytes']
143     response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
144     response['Last-Modified'] = http_date(int(meta['modified']))
145     if not restricted:
146         response['X-Object-Modified-By'] = meta['modified_by']
147         response['X-Object-Version'] = meta['version']
148         response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
149         for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
150             response[k.encode('utf-8')] = meta[k].encode('utf-8')
151         for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By'):
152             if k in meta:
153                 response[k] = meta[k]
154     else:
155         for k in ('Content-Encoding', 'Content-Disposition'):
156             if k in meta:
157                 response[k] = meta[k]
158
159 def update_manifest_meta(request, v_account, meta):
160     """Update metadata if the object has an X-Object-Manifest."""
161     
162     if 'X-Object-Manifest' in meta:
163         hash = ''
164         bytes = 0
165         try:
166             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
167             objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
168             for x in objects:
169                 src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
170                 hash += src_meta['hash']
171                 bytes += src_meta['bytes']
172         except:
173             # Ignore errors.
174             return
175         meta['bytes'] = bytes
176         md5 = hashlib.md5()
177         md5.update(hash)
178         meta['hash'] = md5.hexdigest().lower()
179
180 def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
181     if permissions is None:
182         return
183     perm_path, perms = permissions
184     if len(perms) == 0:
185         return
186     ret = []
187     r = ','.join(perms.get('read', []))
188     if r:
189         ret.append('read=' + r)
190     w = ','.join(perms.get('write', []))
191     if w:
192         ret.append('write=' + w)
193     meta['X-Object-Sharing'] = '; '.join(ret)
194     if '/'.join((v_account, v_container, v_object)) != perm_path:
195         meta['X-Object-Shared-By'] = perm_path
196
197 def validate_modification_preconditions(request, meta):
198     """Check that the modified timestamp conforms with the preconditions set."""
199     
200     if 'modified' not in meta:
201         return # TODO: Always return?
202     
203     if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
204     if if_modified_since is not None:
205         if_modified_since = parse_http_date_safe(if_modified_since)
206     if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
207         raise NotModified('Resource has not been modified')
208     
209     if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
210     if if_unmodified_since is not None:
211         if_unmodified_since = parse_http_date_safe(if_unmodified_since)
212     if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
213         raise PreconditionFailed('Resource has been modified')
214
215 def validate_matching_preconditions(request, meta):
216     """Check that the ETag conforms with the preconditions set."""
217     
218     if 'hash' not in meta:
219         return # TODO: Always return?
220     
221     if_match = request.META.get('HTTP_IF_MATCH')
222     if if_match is not None and if_match != '*':
223         if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
224             raise PreconditionFailed('Resource Etag does not match')
225     
226     if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
227     if if_none_match is not None:
228         if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
229             raise NotModified('Resource Etag matches')
230
231 def split_container_object_string(s):
232     if not len(s) > 0 or s[0] != '/':
233         raise ValueError
234     s = s[1:]
235     pos = s.find('/')
236     if pos == -1:
237         raise ValueError
238     return s[:pos], s[(pos + 1):]
239
240 def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
241     """Copy or move an object."""
242     
243     meta, permissions, public = get_object_headers(request)
244     src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
245     try:
246         if move:
247             backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
248         else:
249             backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
250     except NotAllowedError:
251         raise Unauthorized('Access denied')
252     except NameError, IndexError:
253         raise ItemNotFound('Container or object does not exist')
254     except ValueError:
255         raise BadRequest('Invalid sharing header')
256     except AttributeError:
257         raise Conflict('Sharing already set above or below this path in the hierarchy')
258
259 def get_int_parameter(request, name):
260     p = request.GET.get(name)
261     if p is not None:
262         try:
263             p = int(p)
264         except ValueError:
265             return None
266         if p < 0:
267             return None
268     return p
269
270 def get_content_length(request):
271     content_length = request.META.get('CONTENT_LENGTH')
272     if not content_length:
273         raise LengthRequired('Missing Content-Length header')
274     try:
275         content_length = int(content_length)
276         if content_length < 0:
277             raise ValueError
278     except ValueError:
279         raise BadRequest('Invalid Content-Length header')
280     return content_length
281
282 def get_range(request, size):
283     """Parse a Range header from the request.
284     
285     Either returns None, when the header is not existent or should be ignored,
286     or a list of (offset, length) tuples - should be further checked.
287     """
288     
289     ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
290     if not ranges.startswith('bytes='):
291         return None
292     
293     ret = []
294     for r in (x.strip() for x in ranges[6:].split(',')):
295         p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
296         m = p.match(r)
297         if not m:
298             return None
299         offset = m.group('offset')
300         upto = m.group('upto')
301         if offset == '' and upto == '':
302             return None
303         
304         if offset != '':
305             offset = int(offset)
306             if upto != '':
307                 upto = int(upto)
308                 if offset > upto:
309                     return None
310                 ret.append((offset, upto - offset + 1))
311             else:
312                 ret.append((offset, size - offset))
313         else:
314             length = int(upto)
315             ret.append((size - length, length))
316     
317     return ret
318
319 def get_content_range(request):
320     """Parse a Content-Range header from the request.
321     
322     Either returns None, when the header is not existent or should be ignored,
323     or an (offset, length, total) tuple - check as length, total may be None.
324     Returns (None, None, None) if the provided range is '*/*'.
325     """
326     
327     ranges = request.META.get('HTTP_CONTENT_RANGE', '')
328     if not ranges:
329         return None
330     
331     p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
332     m = p.match(ranges)
333     if not m:
334         if ranges == 'bytes */*':
335             return (None, None, None)
336         return None
337     offset = int(m.group('offset'))
338     upto = m.group('upto')
339     total = m.group('total')
340     if upto != '':
341         upto = int(upto)
342     else:
343         upto = None
344     if total != '*':
345         total = int(total)
346     else:
347         total = None
348     if (upto is not None and offset > upto) or \
349         (total is not None and offset >= total) or \
350         (total is not None and upto is not None and upto >= total):
351         return None
352     
353     if upto is None:
354         length = None
355     else:
356         length = upto - offset + 1
357     return (offset, length, total)
358
359 def get_sharing(request):
360     """Parse an X-Object-Sharing header from the request.
361     
362     Raises BadRequest on error.
363     """
364     
365     permissions = request.META.get('HTTP_X_OBJECT_SHARING')
366     if permissions is None:
367         return None
368     
369     ret = {}
370     permissions = permissions.replace(' ', '')
371     if permissions == '':
372         return ret
373     for perm in (x for x in permissions.split(';')):
374         if perm.startswith('read='):
375             ret['read'] = [v.replace(' ','').lower() for v in perm[5:].split(',')]
376             if '' in ret['read']:
377                 ret['read'].remove('')
378             if '*' in ret['read']:
379                 ret['read'] = ['*']
380             if len(ret['read']) == 0:
381                 raise BadRequest('Bad X-Object-Sharing header value')
382         elif perm.startswith('write='):
383             ret['write'] = [v.replace(' ','').lower() for v in perm[6:].split(',')]
384             if '' in ret['write']:
385                 ret['write'].remove('')
386             if '*' in ret['write']:
387                 ret['write'] = ['*']
388             if len(ret['write']) == 0:
389                 raise BadRequest('Bad X-Object-Sharing header value')
390         else:
391             raise BadRequest('Bad X-Object-Sharing header value')
392     return ret
393
394 def get_public(request):
395     """Parse an X-Object-Public header from the request.
396     
397     Raises BadRequest on error.
398     """
399     
400     public = request.META.get('HTTP_X_OBJECT_PUBLIC')
401     if public is None:
402         return None
403     
404     public = public.replace(' ', '').lower()
405     if public == 'true':
406         return True
407     elif public == 'false' or public == '':
408         return False
409     raise BadRequest('Bad X-Object-Public header value')
410
411 def raw_input_socket(request):
412     """Return the socket for reading the rest of the request."""
413     
414     server_software = request.META.get('SERVER_SOFTWARE')
415     if not server_software:
416         if 'wsgi.input' in request.environ:
417             return request.environ['wsgi.input']
418         raise ServiceUnavailable('Unknown server software')
419     if server_software.startswith('WSGIServer'):
420         return request.environ['wsgi.input']
421     elif server_software.startswith('mod_python'):
422         return request._req
423     raise ServiceUnavailable('Unknown server software')
424
425 MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
426
427 def socket_read_iterator(sock, length=0, blocksize=4096):
428     """Return a maximum of blocksize data read from the socket in each iteration.
429     
430     Read up to 'length'. If 'length' is negative, will attempt a chunked read.
431     The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
432     """
433     
434     if length < 0: # Chunked transfers
435         data = ''
436         while length < MAX_UPLOAD_SIZE:
437             # Get chunk size.
438             if hasattr(sock, 'readline'):
439                 chunk_length = sock.readline()
440             else:
441                 chunk_length = ''
442                 while chunk_length[-1:] != '\n':
443                     chunk_length += sock.read(1)
444                 chunk_length.strip()
445             pos = chunk_length.find(';')
446             if pos >= 0:
447                 chunk_length = chunk_length[:pos]
448             try:
449                 chunk_length = int(chunk_length, 16)
450             except Exception, e:
451                 raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
452             # Check if done.
453             if chunk_length == 0:
454                 if len(data) > 0:
455                     yield data
456                 return
457             # Get the actual data.
458             while chunk_length > 0:
459                 chunk = sock.read(min(chunk_length, blocksize))
460                 chunk_length -= len(chunk)
461                 if length > 0:
462                     length += len(chunk)
463                 data += chunk
464                 if len(data) >= blocksize:
465                     ret = data[:blocksize]
466                     data = data[blocksize:]
467                     yield ret
468             sock.read(2) # CRLF
469         # TODO: Raise something to note that maximum size is reached.
470     else:
471         if length > MAX_UPLOAD_SIZE:
472             # TODO: Raise something to note that maximum size is reached.
473             pass
474         while length > 0:
475             data = sock.read(min(length, blocksize))
476             length -= len(data)
477             yield data
478
479 class ObjectWrapper(object):
480     """Return the object's data block-per-block in each iteration.
481     
482     Read from the object using the offset and length provided in each entry of the range list.
483     """
484     
485     def __init__(self, ranges, sizes, hashmaps, boundary):
486         self.ranges = ranges
487         self.sizes = sizes
488         self.hashmaps = hashmaps
489         self.boundary = boundary
490         self.size = sum(self.sizes)
491         
492         self.file_index = 0
493         self.block_index = 0
494         self.block_hash = -1
495         self.block = ''
496         
497         self.range_index = -1
498         self.offset, self.length = self.ranges[0]
499     
500     def __iter__(self):
501         return self
502     
503     def part_iterator(self):
504         if self.length > 0:
505             # Get the file for the current offset.
506             file_size = self.sizes[self.file_index]
507             while self.offset >= file_size:
508                 self.offset -= file_size
509                 self.file_index += 1
510                 file_size = self.sizes[self.file_index]
511             
512             # Get the block for the current position.
513             self.block_index = int(self.offset / backend.block_size)
514             if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
515                 self.block_hash = self.hashmaps[self.file_index][self.block_index]
516                 try:
517                     self.block = backend.get_block(self.block_hash)
518                 except NameError:
519                     raise ItemNotFound('Block does not exist')
520             
521             # Get the data from the block.
522             bo = self.offset % backend.block_size
523             bl = min(self.length, len(self.block) - bo)
524             data = self.block[bo:bo + bl]
525             self.offset += bl
526             self.length -= bl
527             return data
528         else:
529             raise StopIteration
530     
531     def next(self):
532         if len(self.ranges) == 1:
533             return self.part_iterator()
534         if self.range_index == len(self.ranges):
535             raise StopIteration
536         try:
537             if self.range_index == -1:
538                 raise StopIteration
539             return self.part_iterator()
540         except StopIteration:
541             self.range_index += 1
542             out = []
543             if self.range_index < len(self.ranges):
544                 # Part header.
545                 self.offset, self.length = self.ranges[self.range_index]
546                 self.file_index = 0
547                 if self.range_index > 0:
548                     out.append('')
549                 out.append('--' + self.boundary)
550                 out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
551                 out.append('Content-Transfer-Encoding: binary')
552                 out.append('')
553                 out.append('')
554                 return '\r\n'.join(out)
555             else:
556                 # Footer.
557                 out.append('')
558                 out.append('--' + self.boundary + '--')
559                 out.append('')
560                 return '\r\n'.join(out)
561
562 def object_data_response(request, sizes, hashmaps, meta, public=False):
563     """Get the HttpResponse object for replying with the object's data."""
564     
565     # Range handling.
566     size = sum(sizes)
567     ranges = get_range(request, size)
568     if ranges is None:
569         ranges = [(0, size)]
570         ret = 200
571     else:
572         check = [True for offset, length in ranges if
573                     length <= 0 or length > size or
574                     offset < 0 or offset >= size or
575                     offset + length > size]
576         if len(check) > 0:
577             raise RangeNotSatisfiable('Requested range exceeds object limits')        
578         ret = 206
579     
580     if ret == 206 and len(ranges) > 1:
581         boundary = uuid.uuid4().hex
582     else:
583         boundary = ''
584     wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
585     response = HttpResponse(wrapper, status=ret)
586     put_object_headers(response, meta, public)
587     if ret == 206:
588         if len(ranges) == 1:
589             offset, length = ranges[0]
590             response['Content-Length'] = length # Update with the correct length.
591             response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
592         else:
593             del(response['Content-Length'])
594             response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
595     return response
596
597 def put_object_block(hashmap, data, offset):
598     """Put one block of data at the given offset."""
599     
600     bi = int(offset / backend.block_size)
601     bo = offset % backend.block_size
602     bl = min(len(data), backend.block_size - bo)
603     if bi < len(hashmap):
604         hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
605     else:
606         hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
607     return bl # Return ammount of data written.
608
609 def hashmap_hash(hashmap):
610     """Produce the root hash, treating the hashmap as a Merkle-like tree."""
611     
612     def subhash(d):
613         h = hashlib.new(backend.hash_algorithm)
614         h.update(d)
615         return h.digest()
616     
617     if len(hashmap) == 0:
618         return hexlify(subhash(''))
619     if len(hashmap) == 1:
620         return hexlify(subhash(hashmap[0]))
621     s = 2
622     while s < len(hashmap):
623         s = s * 2
624     h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
625     h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
626     while len(h) > 1:
627         h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
628     return hexlify(h[0])
629
630 def update_response_headers(request, response):
631     if request.serialization == 'xml':
632         response['Content-Type'] = 'application/xml; charset=UTF-8'
633     elif request.serialization == 'json':
634         response['Content-Type'] = 'application/json; charset=UTF-8'
635     elif not response['Content-Type']:
636         response['Content-Type'] = 'text/plain; charset=UTF-8'
637
638     if settings.TEST:
639         response['Date'] = format_date_time(time())
640
641 def render_fault(request, fault):
642     if settings.DEBUG or settings.TEST:
643         fault.details = format_exc(fault)
644
645     request.serialization = 'text'
646     data = '\n'.join((fault.message, fault.details)) + '\n'
647     response = HttpResponse(data, status=fault.code)
648     update_response_headers(request, response)
649     return response
650
651 def request_serialization(request, format_allowed=False):
652     """Return the serialization format requested.
653     
654     Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
655     """
656     
657     if not format_allowed:
658         return 'text'
659     
660     format = request.GET.get('format')
661     if format == 'json':
662         return 'json'
663     elif format == 'xml':
664         return 'xml'
665     
666     for item in request.META.get('HTTP_ACCEPT', '').split(','):
667         accept, sep, rest = item.strip().partition(';')
668         if accept == 'application/json':
669             return 'json'
670         elif accept == 'application/xml' or accept == 'text/xml':
671             return 'xml'
672     
673     return 'text'
674
675 def api_method(http_method=None, format_allowed=False):
676     """Decorator function for views that implement an API method."""
677     
678     def decorator(func):
679         @wraps(func)
680         def wrapper(request, *args, **kwargs):
681             try:
682                 if http_method and request.method != http_method:
683                     raise BadRequest('Method not allowed.')
684                 
685                 # The args variable may contain up to (account, container, object).
686                 if len(args) > 1 and len(args[1]) > 256:
687                     raise BadRequest('Container name too large.')
688                 if len(args) > 2 and len(args[2]) > 1024:
689                     raise BadRequest('Object name too large.')
690                 
691                 # Fill in custom request variables.
692                 request.serialization = request_serialization(request, format_allowed)
693                 
694                 response = func(request, *args, **kwargs)
695                 update_response_headers(request, response)
696                 return response
697             except Fault, fault:
698                 return render_fault(request, fault)
699             except BaseException, e:
700                 logger.exception('Unexpected error: %s' % e)
701                 fault = ServiceUnavailable('Unexpected error')
702                 return render_fault(request, fault)
703         return wrapper
704     return decorator