Fix web server compatibility.
[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'] = ','.join([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 server_software and server_software.startswith('mod_python'):
425         return request._req
426     if 'wsgi.input' in request.environ:
427         return request.environ['wsgi.input']
428     raise ServiceUnavailable('Unknown server software')
429
430 MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
431
432 def socket_read_iterator(sock, length=0, blocksize=4096):
433     """Return a maximum of blocksize data read from the socket in each iteration.
434     
435     Read up to 'length'. If 'length' is negative, will attempt a chunked read.
436     The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
437     """
438     
439     if length < 0: # Chunked transfers
440         data = ''
441         while length < MAX_UPLOAD_SIZE:
442             # Get chunk size.
443             if hasattr(sock, 'readline'):
444                 chunk_length = sock.readline()
445             else:
446                 chunk_length = ''
447                 while chunk_length[-1:] != '\n':
448                     chunk_length += sock.read(1)
449                 chunk_length.strip()
450             pos = chunk_length.find(';')
451             if pos >= 0:
452                 chunk_length = chunk_length[:pos]
453             try:
454                 chunk_length = int(chunk_length, 16)
455             except Exception, e:
456                 raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
457             # Check if done.
458             if chunk_length == 0:
459                 if len(data) > 0:
460                     yield data
461                 return
462             # Get the actual data.
463             while chunk_length > 0:
464                 chunk = sock.read(min(chunk_length, blocksize))
465                 chunk_length -= len(chunk)
466                 if length > 0:
467                     length += len(chunk)
468                 data += chunk
469                 if len(data) >= blocksize:
470                     ret = data[:blocksize]
471                     data = data[blocksize:]
472                     yield ret
473             sock.read(2) # CRLF
474         raise BadRequest('Maximum size is reached')
475     else:
476         if length > MAX_UPLOAD_SIZE:
477             raise BadRequest('Maximum size is reached')
478         while length > 0:
479             data = sock.read(min(length, blocksize))
480             length -= len(data)
481             yield data
482
483 class ObjectWrapper(object):
484     """Return the object's data block-per-block in each iteration.
485     
486     Read from the object using the offset and length provided in each entry of the range list.
487     """
488     
489     def __init__(self, ranges, sizes, hashmaps, boundary):
490         self.ranges = ranges
491         self.sizes = sizes
492         self.hashmaps = hashmaps
493         self.boundary = boundary
494         self.size = sum(self.sizes)
495         
496         self.file_index = 0
497         self.block_index = 0
498         self.block_hash = -1
499         self.block = ''
500         
501         self.range_index = -1
502         self.offset, self.length = self.ranges[0]
503     
504     def __iter__(self):
505         return self
506     
507     def part_iterator(self):
508         if self.length > 0:
509             # Get the file for the current offset.
510             file_size = self.sizes[self.file_index]
511             while self.offset >= file_size:
512                 self.offset -= file_size
513                 self.file_index += 1
514                 file_size = self.sizes[self.file_index]
515             
516             # Get the block for the current position.
517             self.block_index = int(self.offset / backend.block_size)
518             if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
519                 self.block_hash = self.hashmaps[self.file_index][self.block_index]
520                 try:
521                     self.block = backend.get_block(self.block_hash)
522                 except NameError:
523                     raise ItemNotFound('Block does not exist')
524             
525             # Get the data from the block.
526             bo = self.offset % backend.block_size
527             bl = min(self.length, len(self.block) - bo)
528             data = self.block[bo:bo + bl]
529             self.offset += bl
530             self.length -= bl
531             return data
532         else:
533             raise StopIteration
534     
535     def next(self):
536         if len(self.ranges) == 1:
537             return self.part_iterator()
538         if self.range_index == len(self.ranges):
539             raise StopIteration
540         try:
541             if self.range_index == -1:
542                 raise StopIteration
543             return self.part_iterator()
544         except StopIteration:
545             self.range_index += 1
546             out = []
547             if self.range_index < len(self.ranges):
548                 # Part header.
549                 self.offset, self.length = self.ranges[self.range_index]
550                 self.file_index = 0
551                 if self.range_index > 0:
552                     out.append('')
553                 out.append('--' + self.boundary)
554                 out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
555                 out.append('Content-Transfer-Encoding: binary')
556                 out.append('')
557                 out.append('')
558                 return '\r\n'.join(out)
559             else:
560                 # Footer.
561                 out.append('')
562                 out.append('--' + self.boundary + '--')
563                 out.append('')
564                 return '\r\n'.join(out)
565
566 def object_data_response(request, sizes, hashmaps, meta, public=False):
567     """Get the HttpResponse object for replying with the object's data."""
568     
569     # Range handling.
570     size = sum(sizes)
571     ranges = get_range(request, size)
572     if ranges is None:
573         ranges = [(0, size)]
574         ret = 200
575     else:
576         check = [True for offset, length in ranges if
577                     length <= 0 or length > size or
578                     offset < 0 or offset >= size or
579                     offset + length > size]
580         if len(check) > 0:
581             raise RangeNotSatisfiable('Requested range exceeds object limits')
582         ret = 206
583         if_range = request.META.get('HTTP_IF_RANGE')
584         if if_range:
585             try:
586                 # Modification time has passed instead.
587                 last_modified = parse_http_date(if_range)
588                 if last_modified != meta['modified']:
589                     ranges = [(0, size)]
590                     ret = 200
591             except ValueError:
592                 if if_range != meta['hash']:
593                     ranges = [(0, size)]
594                     ret = 200
595     
596     if ret == 206 and len(ranges) > 1:
597         boundary = uuid.uuid4().hex
598     else:
599         boundary = ''
600     wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
601     response = HttpResponse(wrapper, status=ret)
602     put_object_headers(response, meta, public)
603     if ret == 206:
604         if len(ranges) == 1:
605             offset, length = ranges[0]
606             response['Content-Length'] = length # Update with the correct length.
607             response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
608         else:
609             del(response['Content-Length'])
610             response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
611     return response
612
613 def put_object_block(hashmap, data, offset):
614     """Put one block of data at the given offset."""
615     
616     bi = int(offset / backend.block_size)
617     bo = offset % backend.block_size
618     bl = min(len(data), backend.block_size - bo)
619     if bi < len(hashmap):
620         hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
621     else:
622         hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
623     return bl # Return ammount of data written.
624
625 def hashmap_hash(hashmap):
626     """Produce the root hash, treating the hashmap as a Merkle-like tree."""
627     
628     def subhash(d):
629         h = hashlib.new(backend.hash_algorithm)
630         h.update(d)
631         return h.digest()
632     
633     if len(hashmap) == 0:
634         return hexlify(subhash(''))
635     if len(hashmap) == 1:
636         return hexlify(subhash(hashmap[0]))
637     s = 2
638     while s < len(hashmap):
639         s = s * 2
640     h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
641     h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
642     while len(h) > 1:
643         h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
644     return hexlify(h[0])
645
646 def update_response_headers(request, response):
647     if request.serialization == 'xml':
648         response['Content-Type'] = 'application/xml; charset=UTF-8'
649     elif request.serialization == 'json':
650         response['Content-Type'] = 'application/json; charset=UTF-8'
651     elif not response['Content-Type']:
652         response['Content-Type'] = 'text/plain; charset=UTF-8'
653     
654     if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
655         response['Content-Length'] = len(response.content)
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