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