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