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