Fix metadata keys in extended replies.
[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 rename_meta_key(d, old, new):
63     if old not in d:
64         return
65     d[new] = d[old]
66     del(d[old])
67
68 def printable_header_dict(d):
69     """Format a meta dictionary for printing out json/xml.
70     
71     Convert all keys to lower case and replace dashes with underscores.
72     Format 'last_modified' timestamp.
73     """
74     
75     d['last_modified'] = datetime.datetime.fromtimestamp(int(d['last_modified'])).isoformat()
76     return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
77
78 def format_header_key(k):
79     """Convert underscores to dashes and capitalize intra-dash strings."""
80     
81     return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
82
83 def get_header_prefix(request, prefix):
84     """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
85     
86     prefix = 'HTTP_' + prefix.upper().replace('-', '_')
87     # TODO: Document or remove '~' replacing.
88     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)])
89
90 def get_account_headers(request):
91     meta = get_header_prefix(request, 'X-Account-Meta-')
92     groups = {}
93     for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
94         n = k[16:].lower()
95         if '-' in n or '_' in n:
96             raise BadRequest('Bad characters in group name')
97         groups[n] = v.replace(' ', '').split(',')
98         if '' in groups[n]:
99             groups[n].remove('')
100     return meta, groups
101
102 def put_account_headers(response, meta, groups):
103     if 'count' in meta:
104         response['X-Account-Container-Count'] = meta['count']
105     if 'bytes' in meta:
106         response['X-Account-Bytes-Used'] = meta['bytes']
107     response['Last-Modified'] = http_date(int(meta['modified']))
108     for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
109         response[k.encode('utf-8')] = meta[k].encode('utf-8')
110     if 'until_timestamp' in meta:
111         response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
112     for k, v in groups.iteritems():
113         response[format_header_key('X-Account-Group-' + k).encode('utf-8')] = (','.join(v)).encode('utf-8')
114
115 def get_container_headers(request):
116     meta = get_header_prefix(request, 'X-Container-Meta-')
117     policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
118     return meta, policy
119
120 def put_container_headers(response, meta, policy):
121     if 'count' in meta:
122         response['X-Container-Object-Count'] = meta['count']
123     if 'bytes' in meta:
124         response['X-Container-Bytes-Used'] = meta['bytes']
125     response['Last-Modified'] = http_date(int(meta['modified']))
126     for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
127         response[k.encode('utf-8')] = meta[k].encode('utf-8')
128     response['X-Container-Object-Meta'] = ','.join([x[14:] for x in meta['object_meta'] if x.startswith('X-Object-Meta-')])
129     response['X-Container-Block-Size'] = backend.block_size
130     response['X-Container-Block-Hash'] = backend.hash_algorithm
131     if 'until_timestamp' in meta:
132         response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
133     for k, v in policy.iteritems():
134         response[format_header_key('X-Container-Policy-' + k).encode('utf-8')] = v.encode('utf-8')
135
136 def get_object_headers(request):
137     meta = get_header_prefix(request, 'X-Object-Meta-')
138     if request.META.get('CONTENT_TYPE'):
139         meta['Content-Type'] = request.META['CONTENT_TYPE']
140     if request.META.get('HTTP_CONTENT_ENCODING'):
141         meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
142     if request.META.get('HTTP_CONTENT_DISPOSITION'):
143         meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
144     if request.META.get('HTTP_X_OBJECT_MANIFEST'):
145         meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
146     return meta, get_sharing(request), get_public(request)
147
148 def put_object_headers(response, meta, restricted=False):
149     response['ETag'] = meta['hash']
150     response['Content-Length'] = meta['bytes']
151     response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
152     response['Last-Modified'] = http_date(int(meta['modified']))
153     if not restricted:
154         response['X-Object-Modified-By'] = meta['modified_by']
155         response['X-Object-Version'] = meta['version']
156         response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
157         for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
158             response[k.encode('utf-8')] = meta[k].encode('utf-8')
159         for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Public'):
160             if k in meta:
161                 response[k] = meta[k]
162     else:
163         for k in ('Content-Encoding', 'Content-Disposition'):
164             if k in meta:
165                 response[k] = meta[k]
166
167 def update_manifest_meta(request, v_account, meta):
168     """Update metadata if the object has an X-Object-Manifest."""
169     
170     if 'X-Object-Manifest' in meta:
171         hash = ''
172         bytes = 0
173         try:
174             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
175             objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
176             for x in objects:
177                 src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
178                 hash += src_meta['hash']
179                 bytes += src_meta['bytes']
180         except:
181             # Ignore errors.
182             return
183         meta['bytes'] = bytes
184         md5 = hashlib.md5()
185         md5.update(hash)
186         meta['hash'] = md5.hexdigest().lower()
187
188 def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
189     if permissions is None:
190         return
191     perm_path, perms = permissions
192     if len(perms) == 0:
193         return
194     ret = []
195     r = ','.join(perms.get('read', []))
196     if r:
197         ret.append('read=' + r)
198     w = ','.join(perms.get('write', []))
199     if w:
200         ret.append('write=' + w)
201     meta['X-Object-Sharing'] = '; '.join(ret)
202     if '/'.join((v_account, v_container, v_object)) != perm_path:
203         meta['X-Object-Shared-By'] = perm_path
204
205 def update_public_meta(public, meta):
206     if not public:
207         return
208     meta['X-Object-Public'] = public
209
210 def validate_modification_preconditions(request, meta):
211     """Check that the modified timestamp conforms with the preconditions set."""
212     
213     if 'modified' not in meta:
214         return # TODO: Always return?
215     
216     if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
217     if if_modified_since is not None:
218         if_modified_since = parse_http_date_safe(if_modified_since)
219     if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
220         raise NotModified('Resource has not been modified')
221     
222     if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
223     if if_unmodified_since is not None:
224         if_unmodified_since = parse_http_date_safe(if_unmodified_since)
225     if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
226         raise PreconditionFailed('Resource has been modified')
227
228 def validate_matching_preconditions(request, meta):
229     """Check that the ETag conforms with the preconditions set."""
230     
231     if 'hash' not in meta:
232         return # TODO: Always return?
233     
234     if_match = request.META.get('HTTP_IF_MATCH')
235     if if_match is not None and if_match != '*':
236         if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
237             raise PreconditionFailed('Resource Etag does not match')
238     
239     if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
240     if if_none_match is not None:
241         if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
242             raise NotModified('Resource Etag matches')
243
244 def split_container_object_string(s):
245     if not len(s) > 0 or s[0] != '/':
246         raise ValueError
247     s = s[1:]
248     pos = s.find('/')
249     if pos == -1:
250         raise ValueError
251     return s[:pos], s[(pos + 1):]
252
253 def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
254     """Copy or move an object."""
255     
256     meta, permissions, public = get_object_headers(request)
257     src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
258     try:
259         if move:
260             backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
261         else:
262             backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
263     except NotAllowedError:
264         raise Unauthorized('Access denied')
265     except NameError, IndexError:
266         raise ItemNotFound('Container or object does not exist')
267     except ValueError:
268         raise BadRequest('Invalid sharing header')
269     except AttributeError, e:
270         raise Conflict(json.dumps(e.data))
271     if public is not None:
272         try:
273             backend.update_object_public(request.user, v_account, dest_container, dest_name, public)
274         except NotAllowedError:
275             raise Unauthorized('Access denied')
276         except NameError:
277             raise ItemNotFound('Object does not exist')
278
279 def get_int_parameter(p):
280     if p is not None:
281         try:
282             p = int(p)
283         except ValueError:
284             return None
285         if p < 0:
286             return None
287     return p
288
289 def get_content_length(request):
290     content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
291     if content_length is None:
292         raise LengthRequired('Missing or 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'] = list(set([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'] = list(set([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 server_software and server_software.startswith('mod_python'):
429         return request._req
430     if 'wsgi.input' in request.environ:
431         return request.environ['wsgi.input']
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