1 # Copyright 2011-2012 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
34 from functools import wraps
36 from traceback import format_exc
37 from wsgiref.handlers import format_date_time
38 from binascii import hexlify, unhexlify
39 from datetime import datetime, tzinfo, timedelta
40 from urllib import quote, unquote
42 from django.conf import settings
43 from django.http import HttpResponse
44 from django.template.loader import render_to_string
45 from django.utils import simplejson as json
46 from django.utils.http import http_date, parse_etags
47 from django.utils.encoding import smart_unicode, smart_str
48 from django.core.files.uploadhandler import FileUploadHandler
49 from django.core.files.uploadedfile import UploadedFile
51 from synnefo.lib.parsedate import parse_http_date_safe, parse_http_date
53 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
54 Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
55 RangeNotSatisfiable, InternalServerError, NotImplemented)
56 from pithos.api.short_url import encode_url
57 from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
58 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
60 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_CONNECTION,
61 BACKEND_QUOTA, BACKEND_VERSIONING)
62 from pithos.backends import connect_backend
63 from pithos.backends.base import NotAllowedError, QuotaError
72 logger = logging.getLogger(__name__)
76 def utcoffset(self, dt):
85 def json_encode_decimal(obj):
86 if isinstance(obj, decimal.Decimal):
88 raise TypeError(repr(obj) + " is not JSON serializable")
91 """Return an ISO8601 date string that includes a timezone."""
93 return d.replace(tzinfo=UTC()).isoformat()
95 def rename_meta_key(d, old, new):
101 def printable_header_dict(d):
102 """Format a meta dictionary for printing out json/xml.
104 Convert all keys to lower case and replace dashes with underscores.
105 Format 'last_modified' timestamp.
108 if 'last_modified' in d and d['last_modified']:
109 d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
110 return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
112 def format_header_key(k):
113 """Convert underscores to dashes and capitalize intra-dash strings."""
114 return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
116 def get_header_prefix(request, prefix):
117 """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
119 prefix = 'HTTP_' + prefix.upper().replace('-', '_')
120 # TODO: Document or remove '~' replacing.
121 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)])
123 def check_meta_headers(meta):
125 raise BadRequest('Too many headers.')
126 for k, v in meta.iteritems():
128 raise BadRequest('Header name too large.')
130 raise BadRequest('Header value too large.')
132 def get_account_headers(request):
133 meta = get_header_prefix(request, 'X-Account-Meta-')
134 check_meta_headers(meta)
136 for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
138 if '-' in n or '_' in n:
139 raise BadRequest('Bad characters in group name')
140 groups[n] = v.replace(' ', '').split(',')
141 while '' in groups[n]:
145 def put_account_headers(response, meta, groups, policy):
147 response['X-Account-Container-Count'] = meta['count']
149 response['X-Account-Bytes-Used'] = meta['bytes']
150 response['Last-Modified'] = http_date(int(meta['modified']))
151 for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
152 response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
153 if 'until_timestamp' in meta:
154 response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
155 for k, v in groups.iteritems():
156 k = smart_str(k, strings_only=True)
157 k = format_header_key('X-Account-Group-' + k)
158 v = smart_str(','.join(v), strings_only=True)
160 for k, v in policy.iteritems():
161 response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
163 def get_container_headers(request):
164 meta = get_header_prefix(request, 'X-Container-Meta-')
165 check_meta_headers(meta)
166 policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
169 def put_container_headers(request, response, meta, policy):
171 response['X-Container-Object-Count'] = meta['count']
173 response['X-Container-Bytes-Used'] = meta['bytes']
174 response['Last-Modified'] = http_date(int(meta['modified']))
175 for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
176 response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
177 l = [smart_str(x, strings_only=True) for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
178 response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
179 response['X-Container-Block-Size'] = request.backend.block_size
180 response['X-Container-Block-Hash'] = request.backend.hash_algorithm
181 if 'until_timestamp' in meta:
182 response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
183 for k, v in policy.iteritems():
184 response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
186 def get_object_headers(request):
187 content_type = request.META.get('CONTENT_TYPE', None)
188 meta = get_header_prefix(request, 'X-Object-Meta-')
189 check_meta_headers(meta)
190 if request.META.get('HTTP_CONTENT_ENCODING'):
191 meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
192 if request.META.get('HTTP_CONTENT_DISPOSITION'):
193 meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
194 if request.META.get('HTTP_X_OBJECT_MANIFEST'):
195 meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
196 return content_type, meta, get_sharing(request), get_public(request)
198 def put_object_headers(response, meta, restricted=False):
199 response['ETag'] = meta['checksum']
200 response['Content-Length'] = meta['bytes']
201 response['Content-Type'] = meta.get('type', 'application/octet-stream')
202 response['Last-Modified'] = http_date(int(meta['modified']))
204 response['X-Object-Hash'] = meta['hash']
205 response['X-Object-UUID'] = meta['uuid']
206 response['X-Object-Modified-By'] = smart_str(meta['modified_by'], strings_only=True)
207 response['X-Object-Version'] = meta['version']
208 response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
209 for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
210 response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
211 for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
212 'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
215 response[k] = smart_str(meta[k], strings_only=True)
217 for k in ('Content-Encoding', 'Content-Disposition'):
219 response[k] = smart_str(meta[k], strings_only=True)
221 def update_manifest_meta(request, v_account, meta):
222 """Update metadata if the object has an X-Object-Manifest."""
224 if 'X-Object-Manifest' in meta:
228 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
229 objects = request.backend.list_objects(request.user_uniq, v_account,
230 src_container, prefix=src_name, virtual=False)
232 src_meta = request.backend.get_object_meta(request.user_uniq,
233 v_account, src_container, x[0], 'pithos', x[1])
234 etag += src_meta['checksum']
235 bytes += src_meta['bytes']
239 meta['bytes'] = bytes
242 meta['checksum'] = md5.hexdigest().lower()
244 def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
245 if permissions is None:
247 allowed, perm_path, perms = permissions
251 r = ','.join(perms.get('read', []))
253 ret.append('read=' + r)
254 w = ','.join(perms.get('write', []))
256 ret.append('write=' + w)
257 meta['X-Object-Sharing'] = '; '.join(ret)
258 if '/'.join((v_account, v_container, v_object)) != perm_path:
259 meta['X-Object-Shared-By'] = perm_path
260 if request.user_uniq != v_account:
261 meta['X-Object-Allowed-To'] = allowed
263 def update_public_meta(public, meta):
266 meta['X-Object-Public'] = '/public/' + encode_url(public)
268 def validate_modification_preconditions(request, meta):
269 """Check that the modified timestamp conforms with the preconditions set."""
271 if 'modified' not in meta:
272 return # TODO: Always return?
274 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
275 if if_modified_since is not None:
276 if_modified_since = parse_http_date_safe(if_modified_since)
277 if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
278 raise NotModified('Resource has not been modified')
280 if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
281 if if_unmodified_since is not None:
282 if_unmodified_since = parse_http_date_safe(if_unmodified_since)
283 if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
284 raise PreconditionFailed('Resource has been modified')
286 def validate_matching_preconditions(request, meta):
287 """Check that the ETag conforms with the preconditions set."""
289 etag = meta['checksum']
293 if_match = request.META.get('HTTP_IF_MATCH')
294 if if_match is not None:
296 raise PreconditionFailed('Resource does not exist')
297 if if_match != '*' and etag not in [x.lower() for x in parse_etags(if_match)]:
298 raise PreconditionFailed('Resource ETag does not match')
300 if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
301 if if_none_match is not None:
302 # TODO: If this passes, must ignore If-Modified-Since header.
304 if if_none_match == '*' or etag in [x.lower() for x in parse_etags(if_none_match)]:
305 # TODO: Continue if an If-Modified-Since header is present.
306 if request.method in ('HEAD', 'GET'):
307 raise NotModified('Resource ETag matches')
308 raise PreconditionFailed('Resource exists or ETag matches')
310 def split_container_object_string(s):
311 if not len(s) > 0 or s[0] != '/':
315 if pos == -1 or pos == len(s) - 1:
317 return s[:pos], s[(pos + 1):]
319 def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False):
320 """Copy or move an object."""
322 if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
323 del(request.META['CONTENT_TYPE'])
324 content_type, meta, permissions, public = get_object_headers(request)
325 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
328 version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
329 dest_account, dest_container, dest_name,
330 content_type, 'pithos', meta, False, permissions)
332 version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
333 dest_account, dest_container, dest_name,
334 content_type, 'pithos', meta, False, permissions, src_version)
335 except NotAllowedError:
336 raise Forbidden('Not allowed')
337 except (NameError, IndexError):
338 raise ItemNotFound('Container or object does not exist')
340 raise BadRequest('Invalid sharing header')
342 raise RequestEntityTooLarge('Quota exceeded')
343 if public is not None:
345 request.backend.update_object_public(request.user_uniq, dest_account, dest_container, dest_name, public)
346 except NotAllowedError:
347 raise Forbidden('Not allowed')
349 raise ItemNotFound('Object does not exist')
352 def get_int_parameter(p):
362 def get_content_length(request):
363 content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
364 if content_length is None:
365 raise LengthRequired('Missing or invalid Content-Length header')
366 return content_length
368 def get_range(request, size):
369 """Parse a Range header from the request.
371 Either returns None, when the header is not existent or should be ignored,
372 or a list of (offset, length) tuples - should be further checked.
375 ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
376 if not ranges.startswith('bytes='):
380 for r in (x.strip() for x in ranges[6:].split(',')):
381 p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
385 offset = m.group('offset')
386 upto = m.group('upto')
387 if offset == '' and upto == '':
396 ret.append((offset, upto - offset + 1))
398 ret.append((offset, size - offset))
401 ret.append((size - length, length))
405 def get_content_range(request):
406 """Parse a Content-Range header from the request.
408 Either returns None, when the header is not existent or should be ignored,
409 or an (offset, length, total) tuple - check as length, total may be None.
410 Returns (None, None, None) if the provided range is '*/*'.
413 ranges = request.META.get('HTTP_CONTENT_RANGE', '')
417 p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
420 if ranges == 'bytes */*':
421 return (None, None, None)
423 offset = int(m.group('offset'))
424 upto = m.group('upto')
425 total = m.group('total')
434 if (upto is not None and offset > upto) or \
435 (total is not None and offset >= total) or \
436 (total is not None and upto is not None and upto >= total):
442 length = upto - offset + 1
443 return (offset, length, total)
445 def get_sharing(request):
446 """Parse an X-Object-Sharing header from the request.
448 Raises BadRequest on error.
451 permissions = request.META.get('HTTP_X_OBJECT_SHARING')
452 if permissions is None:
455 # TODO: Document or remove '~' replacing.
456 permissions = permissions.replace('~', '')
459 permissions = permissions.replace(' ', '')
460 if permissions == '':
462 for perm in (x for x in permissions.split(';')):
463 if perm.startswith('read='):
464 ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
465 if '' in ret['read']:
466 ret['read'].remove('')
467 if '*' in ret['read']:
469 if len(ret['read']) == 0:
470 raise BadRequest('Bad X-Object-Sharing header value')
471 elif perm.startswith('write='):
472 ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
473 if '' in ret['write']:
474 ret['write'].remove('')
475 if '*' in ret['write']:
477 if len(ret['write']) == 0:
478 raise BadRequest('Bad X-Object-Sharing header value')
480 raise BadRequest('Bad X-Object-Sharing header value')
482 # Keep duplicates only in write list.
483 dups = [x for x in ret.get('read', []) if x in ret.get('write', []) and x != '*']
486 ret['read'].remove(x)
487 if len(ret['read']) == 0:
492 def get_public(request):
493 """Parse an X-Object-Public header from the request.
495 Raises BadRequest on error.
498 public = request.META.get('HTTP_X_OBJECT_PUBLIC')
502 public = public.replace(' ', '').lower()
505 elif public == 'false' or public == '':
507 raise BadRequest('Bad X-Object-Public header value')
509 def raw_input_socket(request):
510 """Return the socket for reading the rest of the request."""
512 server_software = request.META.get('SERVER_SOFTWARE')
513 if server_software and server_software.startswith('mod_python'):
515 if 'wsgi.input' in request.environ:
516 return request.environ['wsgi.input']
517 raise NotImplemented('Unknown server software')
519 MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024) # 5GB
521 def socket_read_iterator(request, length=0, blocksize=4096):
522 """Return a maximum of blocksize data read from the socket in each iteration.
524 Read up to 'length'. If 'length' is negative, will attempt a chunked read.
525 The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
528 sock = raw_input_socket(request)
529 if length < 0: # Chunked transfers
530 # Small version (server does the dechunking).
531 if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
532 while length < MAX_UPLOAD_SIZE:
533 data = sock.read(blocksize)
537 raise BadRequest('Maximum size is reached')
539 # Long version (do the dechunking).
541 while length < MAX_UPLOAD_SIZE:
543 if hasattr(sock, 'readline'):
544 chunk_length = sock.readline()
547 while chunk_length[-1:] != '\n':
548 chunk_length += sock.read(1)
550 pos = chunk_length.find(';')
552 chunk_length = chunk_length[:pos]
554 chunk_length = int(chunk_length, 16)
556 raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
558 if chunk_length == 0:
562 # Get the actual data.
563 while chunk_length > 0:
564 chunk = sock.read(min(chunk_length, blocksize))
565 chunk_length -= len(chunk)
569 if len(data) >= blocksize:
570 ret = data[:blocksize]
571 data = data[blocksize:]
574 raise BadRequest('Maximum size is reached')
576 if length > MAX_UPLOAD_SIZE:
577 raise BadRequest('Maximum size is reached')
579 data = sock.read(min(length, blocksize))
585 class SaveToBackendHandler(FileUploadHandler):
586 """Handle a file from an HTML form the django way."""
588 def __init__(self, request=None):
589 super(SaveToBackendHandler, self).__init__(request)
590 self.backend = request.backend
592 def put_data(self, length):
593 if len(self.data) >= length:
594 block = self.data[:length]
595 self.file.hashmap.append(self.backend.put_block(block))
596 self.md5.update(block)
597 self.data = self.data[length:]
599 def new_file(self, field_name, file_name, content_type, content_length, charset=None):
600 self.md5 = hashlib.md5()
602 self.file = UploadedFile(name=file_name, content_type=content_type, charset=charset)
604 self.file.hashmap = []
606 def receive_data_chunk(self, raw_data, start):
607 self.data += raw_data
608 self.file.size += len(raw_data)
609 self.put_data(self.request.backend.block_size)
612 def file_complete(self, file_size):
616 self.file.etag = self.md5.hexdigest().lower()
619 class ObjectWrapper(object):
620 """Return the object's data block-per-block in each iteration.
622 Read from the object using the offset and length provided in each entry of the range list.
625 def __init__(self, backend, ranges, sizes, hashmaps, boundary):
626 self.backend = backend
629 self.hashmaps = hashmaps
630 self.boundary = boundary
631 self.size = sum(self.sizes)
638 self.range_index = -1
639 self.offset, self.length = self.ranges[0]
644 def part_iterator(self):
646 # Get the file for the current offset.
647 file_size = self.sizes[self.file_index]
648 while self.offset >= file_size:
649 self.offset -= file_size
651 file_size = self.sizes[self.file_index]
653 # Get the block for the current position.
654 self.block_index = int(self.offset / self.backend.block_size)
655 if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
656 self.block_hash = self.hashmaps[self.file_index][self.block_index]
658 self.block = self.backend.get_block(self.block_hash)
660 raise ItemNotFound('Block does not exist')
662 # Get the data from the block.
663 bo = self.offset % self.backend.block_size
664 bs = self.backend.block_size
665 if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
666 self.sizes[self.file_index] % self.backend.block_size):
667 bs = self.sizes[self.file_index] % self.backend.block_size
668 bl = min(self.length, bs - bo)
669 data = self.block[bo:bo + bl]
677 if len(self.ranges) == 1:
678 return self.part_iterator()
679 if self.range_index == len(self.ranges):
682 if self.range_index == -1:
684 return self.part_iterator()
685 except StopIteration:
686 self.range_index += 1
688 if self.range_index < len(self.ranges):
690 self.offset, self.length = self.ranges[self.range_index]
692 if self.range_index > 0:
694 out.append('--' + self.boundary)
695 out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
696 out.append('Content-Transfer-Encoding: binary')
699 return '\r\n'.join(out)
703 out.append('--' + self.boundary + '--')
705 return '\r\n'.join(out)
707 def object_data_response(request, sizes, hashmaps, meta, public=False):
708 """Get the HttpResponse object for replying with the object's data."""
712 ranges = get_range(request, size)
717 check = [True for offset, length in ranges if
718 length <= 0 or length > size or
719 offset < 0 or offset >= size or
720 offset + length > size]
722 raise RangeNotSatisfiable('Requested range exceeds object limits')
724 if_range = request.META.get('HTTP_IF_RANGE')
727 # Modification time has passed instead.
728 last_modified = parse_http_date(if_range)
729 if last_modified != meta['modified']:
733 if if_range != meta['checksum']:
737 if ret == 206 and len(ranges) > 1:
738 boundary = uuid.uuid4().hex
741 wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
742 response = HttpResponse(wrapper, status=ret)
743 put_object_headers(response, meta, public)
746 offset, length = ranges[0]
747 response['Content-Length'] = length # Update with the correct length.
748 response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
750 del(response['Content-Length'])
751 response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
754 def put_object_block(request, hashmap, data, offset):
755 """Put one block of data at the given offset."""
757 bi = int(offset / request.backend.block_size)
758 bo = offset % request.backend.block_size
759 bl = min(len(data), request.backend.block_size - bo)
760 if bi < len(hashmap):
761 hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
763 hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
764 return bl # Return ammount of data written.
766 def hashmap_md5(backend, hashmap, size):
767 """Produce the MD5 sum from the data in the hashmap."""
769 # TODO: Search backend for the MD5 of another object with the same hashmap and size...
771 bs = backend.block_size
772 for bi, hash in enumerate(hashmap):
773 data = backend.get_block(hash) # Blocks come in padded.
774 if bi == len(hashmap) - 1:
775 data = data[:size % bs]
777 return md5.hexdigest().lower()
779 def simple_list_response(request, l):
780 if request.serialization == 'text':
781 return '\n'.join(l) + '\n'
782 if request.serialization == 'xml':
783 return render_to_string('items.xml', {'items': l})
784 if request.serialization == 'json':
788 backend = connect_backend(db_module=BACKEND_DB_MODULE,
789 db_connection=BACKEND_DB_CONNECTION,
790 block_module=BACKEND_BLOCK_MODULE,
791 block_path=BACKEND_BLOCK_PATH,
792 block_umask=BACKEND_BLOCK_UMASK,
793 queue_module=BACKEND_QUEUE_MODULE,
794 queue_connection=BACKEND_QUEUE_CONNECTION)
795 backend.default_policy['quota'] = BACKEND_QUOTA
796 backend.default_policy['versioning'] = BACKEND_VERSIONING
799 def update_request_headers(request):
800 # Handle URL-encoded keys and values.
801 meta = dict([(k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
802 for k, v in meta.iteritems():
806 except UnicodeDecodeError:
807 raise BadRequest('Bad character in headers.')
808 if '%' in k or '%' in v:
810 request.META[unquote(k)] = smart_unicode(unquote(v), strings_only=True)
812 def update_response_headers(request, response):
813 if request.serialization == 'xml':
814 response['Content-Type'] = 'application/xml; charset=UTF-8'
815 elif request.serialization == 'json':
816 response['Content-Type'] = 'application/json; charset=UTF-8'
817 elif not response['Content-Type']:
818 response['Content-Type'] = 'text/plain; charset=UTF-8'
820 if (not response.has_header('Content-Length') and
821 not (response.has_header('Content-Type') and
822 response['Content-Type'].startswith('multipart/byteranges'))):
823 response['Content-Length'] = len(response.content)
825 # URL-encode unicode in headers.
826 meta = response.items()
828 if (k.startswith('X-Account-') or k.startswith('X-Container-') or
829 k.startswith('X-Object-') or k.startswith('Content-')):
831 response[quote(k)] = quote(v, safe='/=,:@; ')
833 def render_fault(request, fault):
834 if isinstance(fault, InternalServerError) and settings.DEBUG:
835 fault.details = format_exc(fault)
837 request.serialization = 'text'
838 data = fault.message + '\n'
840 data += '\n' + fault.details
841 response = HttpResponse(data, status=fault.code)
842 update_response_headers(request, response)
845 def request_serialization(request, format_allowed=False):
846 """Return the serialization format requested.
848 Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
851 if not format_allowed:
854 format = request.GET.get('format')
857 elif format == 'xml':
860 for item in request.META.get('HTTP_ACCEPT', '').split(','):
861 accept, sep, rest = item.strip().partition(';')
862 if accept == 'application/json':
864 elif accept == 'application/xml' or accept == 'text/xml':
869 def api_method(http_method=None, format_allowed=False, user_required=True):
870 """Decorator function for views that implement an API method."""
874 def wrapper(request, *args, **kwargs):
876 if http_method and request.method != http_method:
877 raise BadRequest('Method not allowed.')
878 if user_required and getattr(request, 'user', None) is None:
879 raise Unauthorized('Access denied')
881 # The args variable may contain up to (account, container, object).
882 if len(args) > 1 and len(args[1]) > 256:
883 raise BadRequest('Container name too large.')
884 if len(args) > 2 and len(args[2]) > 1024:
885 raise BadRequest('Object name too large.')
887 # Format and check headers.
888 update_request_headers(request)
890 # Fill in custom request variables.
891 request.serialization = request_serialization(request, format_allowed)
892 request.backend = get_backend()
894 response = func(request, *args, **kwargs)
895 update_response_headers(request, response)
898 return render_fault(request, fault)
899 except BaseException, e:
900 logger.exception('Unexpected error: %s' % e)
901 fault = InternalServerError('Unexpected error')
902 return render_fault(request, fault)
904 if getattr(request, 'backend', None) is not None:
905 request.backend.close()