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