430cac570fcba963d70e2bde3343a13cb7389fc0
[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                                 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, v_container, v_object, 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(request, name):
273     p = request.GET.get(name)
274     if p is not None:
275         try:
276             p = int(p)
277         except ValueError:
278             return None
279         if p < 0:
280             return None
281     return p
282
283 def get_content_length(request):
284     content_length = request.META.get('CONTENT_LENGTH')
285     if not content_length:
286         raise LengthRequired('Missing Content-Length header')
287     try:
288         content_length = int(content_length)
289         if content_length < 0:
290             raise ValueError
291     except ValueError:
292         raise BadRequest('Invalid Content-Length header')
293     return content_length
294
295 def get_range(request, size):
296     """Parse a Range header from the request.
297     
298     Either returns None, when the header is not existent or should be ignored,
299     or a list of (offset, length) tuples - should be further checked.
300     """
301     
302     ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
303     if not ranges.startswith('bytes='):
304         return None
305     
306     ret = []
307     for r in (x.strip() for x in ranges[6:].split(',')):
308         p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
309         m = p.match(r)
310         if not m:
311             return None
312         offset = m.group('offset')
313         upto = m.group('upto')
314         if offset == '' and upto == '':
315             return None
316         
317         if offset != '':
318             offset = int(offset)
319             if upto != '':
320                 upto = int(upto)
321                 if offset > upto:
322                     return None
323                 ret.append((offset, upto - offset + 1))
324             else:
325                 ret.append((offset, size - offset))
326         else:
327             length = int(upto)
328             ret.append((size - length, length))
329     
330     return ret
331
332 def get_content_range(request):
333     """Parse a Content-Range header from the request.
334     
335     Either returns None, when the header is not existent or should be ignored,
336     or an (offset, length, total) tuple - check as length, total may be None.
337     Returns (None, None, None) if the provided range is '*/*'.
338     """
339     
340     ranges = request.META.get('HTTP_CONTENT_RANGE', '')
341     if not ranges:
342         return None
343     
344     p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
345     m = p.match(ranges)
346     if not m:
347         if ranges == 'bytes */*':
348             return (None, None, None)
349         return None
350     offset = int(m.group('offset'))
351     upto = m.group('upto')
352     total = m.group('total')
353     if upto != '':
354         upto = int(upto)
355     else:
356         upto = None
357     if total != '*':
358         total = int(total)
359     else:
360         total = None
361     if (upto is not None and offset > upto) or \
362         (total is not None and offset >= total) or \
363         (total is not None and upto is not None and upto >= total):
364         return None
365     
366     if upto is None:
367         length = None
368     else:
369         length = upto - offset + 1
370     return (offset, length, total)
371
372 def get_sharing(request):
373     """Parse an X-Object-Sharing header from the request.
374     
375     Raises BadRequest on error.
376     """
377     
378     permissions = request.META.get('HTTP_X_OBJECT_SHARING')
379     if permissions is None:
380         return None
381     
382     ret = {}
383     permissions = permissions.replace(' ', '')
384     if permissions == '':
385         return ret
386     for perm in (x for x in permissions.split(';')):
387         if perm.startswith('read='):
388             ret['read'] = [v.replace(' ','').lower() for v in perm[5:].split(',')]
389             if '' in ret['read']:
390                 ret['read'].remove('')
391             if '*' in ret['read']:
392                 ret['read'] = ['*']
393             if len(ret['read']) == 0:
394                 raise BadRequest('Bad X-Object-Sharing header value')
395         elif perm.startswith('write='):
396             ret['write'] = [v.replace(' ','').lower() for v in perm[6:].split(',')]
397             if '' in ret['write']:
398                 ret['write'].remove('')
399             if '*' in ret['write']:
400                 ret['write'] = ['*']
401             if len(ret['write']) == 0:
402                 raise BadRequest('Bad X-Object-Sharing header value')
403         else:
404             raise BadRequest('Bad X-Object-Sharing header value')
405     return ret
406
407 def get_public(request):
408     """Parse an X-Object-Public header from the request.
409     
410     Raises BadRequest on error.
411     """
412     
413     public = request.META.get('HTTP_X_OBJECT_PUBLIC')
414     if public is None:
415         return None
416     
417     public = public.replace(' ', '').lower()
418     if public == 'true':
419         return True
420     elif public == 'false' or public == '':
421         return False
422     raise BadRequest('Bad X-Object-Public header value')
423
424 def raw_input_socket(request):
425     """Return the socket for reading the rest of the request."""
426     
427     server_software = request.META.get('SERVER_SOFTWARE')
428     if not server_software:
429         if 'wsgi.input' in request.environ:
430             return request.environ['wsgi.input']
431         raise ServiceUnavailable('Unknown server software')
432     if server_software.startswith('WSGIServer'):
433         return request.environ['wsgi.input']
434     elif server_software.startswith('mod_python'):
435         return request._req
436     raise ServiceUnavailable('Unknown server software')
437
438 MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
439
440 def socket_read_iterator(sock, length=0, blocksize=4096):
441     """Return a maximum of blocksize data read from the socket in each iteration.
442     
443     Read up to 'length'. If 'length' is negative, will attempt a chunked read.
444     The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
445     """
446     
447     if length < 0: # Chunked transfers
448         data = ''
449         while length < MAX_UPLOAD_SIZE:
450             # Get chunk size.
451             if hasattr(sock, 'readline'):
452                 chunk_length = sock.readline()
453             else:
454                 chunk_length = ''
455                 while chunk_length[-1:] != '\n':
456                     chunk_length += sock.read(1)
457                 chunk_length.strip()
458             pos = chunk_length.find(';')
459             if pos >= 0:
460                 chunk_length = chunk_length[:pos]
461             try:
462                 chunk_length = int(chunk_length, 16)
463             except Exception, e:
464                 raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
465             # Check if done.
466             if chunk_length == 0:
467                 if len(data) > 0:
468                     yield data
469                 return
470             # Get the actual data.
471             while chunk_length > 0:
472                 chunk = sock.read(min(chunk_length, blocksize))
473                 chunk_length -= len(chunk)
474                 if length > 0:
475                     length += len(chunk)
476                 data += chunk
477                 if len(data) >= blocksize:
478                     ret = data[:blocksize]
479                     data = data[blocksize:]
480                     yield ret
481             sock.read(2) # CRLF
482         # TODO: Raise something to note that maximum size is reached.
483     else:
484         if length > MAX_UPLOAD_SIZE:
485             # TODO: Raise something to note that maximum size is reached.
486             pass
487         while length > 0:
488             data = sock.read(min(length, blocksize))
489             length -= len(data)
490             yield data
491
492 class ObjectWrapper(object):
493     """Return the object's data block-per-block in each iteration.
494     
495     Read from the object using the offset and length provided in each entry of the range list.
496     """
497     
498     def __init__(self, ranges, sizes, hashmaps, boundary):
499         self.ranges = ranges
500         self.sizes = sizes
501         self.hashmaps = hashmaps
502         self.boundary = boundary
503         self.size = sum(self.sizes)
504         
505         self.file_index = 0
506         self.block_index = 0
507         self.block_hash = -1
508         self.block = ''
509         
510         self.range_index = -1
511         self.offset, self.length = self.ranges[0]
512     
513     def __iter__(self):
514         return self
515     
516     def part_iterator(self):
517         if self.length > 0:
518             # Get the file for the current offset.
519             file_size = self.sizes[self.file_index]
520             while self.offset >= file_size:
521                 self.offset -= file_size
522                 self.file_index += 1
523                 file_size = self.sizes[self.file_index]
524             
525             # Get the block for the current position.
526             self.block_index = int(self.offset / backend.block_size)
527             if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
528                 self.block_hash = self.hashmaps[self.file_index][self.block_index]
529                 try:
530                     self.block = backend.get_block(self.block_hash)
531                 except NameError:
532                     raise ItemNotFound('Block does not exist')
533             
534             # Get the data from the block.
535             bo = self.offset % backend.block_size
536             bl = min(self.length, len(self.block) - bo)
537             data = self.block[bo:bo + bl]
538             self.offset += bl
539             self.length -= bl
540             return data
541         else:
542             raise StopIteration
543     
544     def next(self):
545         if len(self.ranges) == 1:
546             return self.part_iterator()
547         if self.range_index == len(self.ranges):
548             raise StopIteration
549         try:
550             if self.range_index == -1:
551                 raise StopIteration
552             return self.part_iterator()
553         except StopIteration:
554             self.range_index += 1
555             out = []
556             if self.range_index < len(self.ranges):
557                 # Part header.
558                 self.offset, self.length = self.ranges[self.range_index]
559                 self.file_index = 0
560                 if self.range_index > 0:
561                     out.append('')
562                 out.append('--' + self.boundary)
563                 out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
564                 out.append('Content-Transfer-Encoding: binary')
565                 out.append('')
566                 out.append('')
567                 return '\r\n'.join(out)
568             else:
569                 # Footer.
570                 out.append('')
571                 out.append('--' + self.boundary + '--')
572                 out.append('')
573                 return '\r\n'.join(out)
574
575 def object_data_response(request, sizes, hashmaps, meta, public=False):
576     """Get the HttpResponse object for replying with the object's data."""
577     
578     # Range handling.
579     size = sum(sizes)
580     ranges = get_range(request, size)
581     if ranges is None:
582         ranges = [(0, size)]
583         ret = 200
584     else:
585         check = [True for offset, length in ranges if
586                     length <= 0 or length > size or
587                     offset < 0 or offset >= size or
588                     offset + length > size]
589         if len(check) > 0:
590             raise RangeNotSatisfiable('Requested range exceeds object limits')
591         ret = 206
592         if_range = request.META.get('HTTP_IF_RANGE', '')
593         if if_range and if_range.startswith('If-Range:'):
594             if_range = if_range.split('If-Range:')[1]
595             try:
596                 # modification time has passed instead
597                 last_modified = parse_http_date(if_range)
598                 if last_modified != meta['modified']:
599                     ranges = [(0, size)]
600                     ret = 200
601             except ValueError:
602                 if if_range != meta['hash']:
603                     ranges = [(0, size)]
604                     ret = 200
605     
606     if ret == 206 and len(ranges) > 1:
607         boundary = uuid.uuid4().hex
608     else:
609         boundary = ''
610     wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
611     response = HttpResponse(wrapper, status=ret)
612     put_object_headers(response, meta, public)
613     if ret == 206:
614         if len(ranges) == 1:
615             offset, length = ranges[0]
616             response['Content-Length'] = length # Update with the correct length.
617             response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
618         else:
619             del(response['Content-Length'])
620             response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
621     return response
622
623 def put_object_block(hashmap, data, offset):
624     """Put one block of data at the given offset."""
625     
626     bi = int(offset / backend.block_size)
627     bo = offset % backend.block_size
628     bl = min(len(data), backend.block_size - bo)
629     if bi < len(hashmap):
630         hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
631     else:
632         hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
633     return bl # Return ammount of data written.
634
635 def hashmap_hash(hashmap):
636     """Produce the root hash, treating the hashmap as a Merkle-like tree."""
637     
638     def subhash(d):
639         h = hashlib.new(backend.hash_algorithm)
640         h.update(d)
641         return h.digest()
642     
643     if len(hashmap) == 0:
644         return hexlify(subhash(''))
645     if len(hashmap) == 1:
646         return hexlify(subhash(hashmap[0]))
647     s = 2
648     while s < len(hashmap):
649         s = s * 2
650     h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
651     h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
652     while len(h) > 1:
653         h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
654     return hexlify(h[0])
655
656 def update_response_headers(request, response):
657     if request.serialization == 'xml':
658         response['Content-Type'] = 'application/xml; charset=UTF-8'
659     elif request.serialization == 'json':
660         response['Content-Type'] = 'application/json; charset=UTF-8'
661     elif not response['Content-Type']:
662         response['Content-Type'] = 'text/plain; charset=UTF-8'
663
664     if settings.TEST:
665         response['Date'] = format_date_time(time())
666
667 def render_fault(request, fault):
668     if settings.DEBUG or settings.TEST:
669         fault.details = format_exc(fault)
670     
671     request.serialization = 'text'
672     data = '\n'.join((fault.message, fault.details)) + '\n'
673     response = HttpResponse(data, status=fault.code)
674     update_response_headers(request, response)
675     return response
676
677 def request_serialization(request, format_allowed=False):
678     """Return the serialization format requested.
679     
680     Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
681     """
682     
683     if not format_allowed:
684         return 'text'
685     
686     format = request.GET.get('format')
687     if format == 'json':
688         return 'json'
689     elif format == 'xml':
690         return 'xml'
691     
692     for item in request.META.get('HTTP_ACCEPT', '').split(','):
693         accept, sep, rest = item.strip().partition(';')
694         if accept == 'application/json':
695             return 'json'
696         elif accept == 'application/xml' or accept == 'text/xml':
697             return 'xml'
698     
699     return 'text'
700
701 def api_method(http_method=None, format_allowed=False):
702     """Decorator function for views that implement an API method."""
703     
704     def decorator(func):
705         @wraps(func)
706         def wrapper(request, *args, **kwargs):
707             try:
708                 if http_method and request.method != http_method:
709                     raise BadRequest('Method not allowed.')
710                 
711                 # The args variable may contain up to (account, container, object).
712                 if len(args) > 1 and len(args[1]) > 256:
713                     raise BadRequest('Container name too large.')
714                 if len(args) > 2 and len(args[2]) > 1024:
715                     raise BadRequest('Object name too large.')
716                 
717                 # Fill in custom request variables.
718                 request.serialization = request_serialization(request, format_allowed)
719                 
720                 response = func(request, *args, **kwargs)
721                 update_response_headers(request, response)
722                 return response
723             except Fault, fault:
724                 return render_fault(request, fault)
725             except BaseException, e:
726                 logger.exception('Unexpected error: %s' % e)
727                 fault = ServiceUnavailable('Unexpected error')
728                 return render_fault(request, fault)
729         return wrapper
730     return decorator