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