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 pithos.lib.compat 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.backends import connect_backend
58 from pithos.backends.base import NotAllowedError, QuotaError
67 logger = logging.getLogger(__name__)
71 def utcoffset(self, dt):
80 def json_encode_decimal(obj):
81 if isinstance(obj, decimal.Decimal):
83 raise TypeError(repr(obj) + " is not JSON serializable")
86 """Return an ISO8601 date string that includes a timezone."""
88 return d.replace(tzinfo=UTC()).isoformat()
90 def rename_meta_key(d, old, new):
96 def printable_header_dict(d):
97 """Format a meta dictionary for printing out json/xml.
99 Convert all keys to lower case and replace dashes with underscores.
100 Format 'last_modified' timestamp.
103 if 'last_modified' in d:
104 d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
105 return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
107 def format_header_key(k):
108 """Convert underscores to dashes and capitalize intra-dash strings."""
109 return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
111 def get_header_prefix(request, prefix):
112 """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
114 prefix = 'HTTP_' + prefix.upper().replace('-', '_')
115 # TODO: Document or remove '~' replacing.
116 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)])
118 def get_account_headers(request):
119 meta = get_header_prefix(request, 'X-Account-Meta-')
121 for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
123 if '-' in n or '_' in n:
124 raise BadRequest('Bad characters in group name')
125 groups[n] = v.replace(' ', '').split(',')
126 while '' in groups[n]:
130 def put_account_headers(response, meta, groups, policy):
132 response['X-Account-Container-Count'] = meta['count']
134 response['X-Account-Bytes-Used'] = meta['bytes']
135 response['Last-Modified'] = http_date(int(meta['modified']))
136 for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
137 response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
138 if 'until_timestamp' in meta:
139 response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
140 for k, v in groups.iteritems():
141 k = smart_str(k, strings_only=True)
142 k = format_header_key('X-Account-Group-' + k)
143 v = smart_str(','.join(v), strings_only=True)
145 for k, v in policy.iteritems():
146 response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
148 def get_container_headers(request):
149 meta = get_header_prefix(request, 'X-Container-Meta-')
150 policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
153 def put_container_headers(request, response, meta, policy):
155 response['X-Container-Object-Count'] = meta['count']
157 response['X-Container-Bytes-Used'] = meta['bytes']
158 response['Last-Modified'] = http_date(int(meta['modified']))
159 for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
160 response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
161 l = [smart_str(x, strings_only=True) for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
162 response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
163 response['X-Container-Block-Size'] = request.backend.block_size
164 response['X-Container-Block-Hash'] = request.backend.hash_algorithm
165 if 'until_timestamp' in meta:
166 response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
167 for k, v in policy.iteritems():
168 response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
170 def get_object_headers(request):
171 meta = get_header_prefix(request, 'X-Object-Meta-')
172 if request.META.get('CONTENT_TYPE'):
173 meta['Content-Type'] = request.META['CONTENT_TYPE']
174 if request.META.get('HTTP_CONTENT_ENCODING'):
175 meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
176 if request.META.get('HTTP_CONTENT_DISPOSITION'):
177 meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
178 if request.META.get('HTTP_X_OBJECT_MANIFEST'):
179 meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
180 return meta, get_sharing(request), get_public(request)
182 def put_object_headers(response, meta, restricted=False):
184 response['ETag'] = meta['ETag']
185 response['Content-Length'] = meta['bytes']
186 response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
187 response['Last-Modified'] = http_date(int(meta['modified']))
189 response['X-Object-Hash'] = meta['hash']
190 response['X-Object-UUID'] = meta['uuid']
191 response['X-Object-Modified-By'] = smart_str(meta['modified_by'], strings_only=True)
192 response['X-Object-Version'] = meta['version']
193 response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
194 for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
195 response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
196 for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
197 'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
200 response[k] = smart_str(meta[k], strings_only=True)
202 for k in ('Content-Encoding', 'Content-Disposition'):
204 response[k] = smart_str(meta[k], strings_only=True)
206 def update_manifest_meta(request, v_account, meta):
207 """Update metadata if the object has an X-Object-Manifest."""
209 if 'X-Object-Manifest' in meta:
213 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
214 objects = request.backend.list_objects(request.user_uniq, v_account,
215 src_container, prefix=src_name, virtual=False)
217 src_meta = request.backend.get_object_meta(request.user_uniq,
218 v_account, src_container, x[0], 'pithos', x[1])
219 if 'ETag' in src_meta:
220 etag += src_meta['ETag']
221 bytes += src_meta['bytes']
225 meta['bytes'] = bytes
228 meta['ETag'] = md5.hexdigest().lower()
230 def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
231 if permissions is None:
233 allowed, perm_path, perms = permissions
237 r = ','.join(perms.get('read', []))
239 ret.append('read=' + r)
240 w = ','.join(perms.get('write', []))
242 ret.append('write=' + w)
243 meta['X-Object-Sharing'] = '; '.join(ret)
244 if '/'.join((v_account, v_container, v_object)) != perm_path:
245 meta['X-Object-Shared-By'] = perm_path
246 if request.user_uniq != v_account:
247 meta['X-Object-Allowed-To'] = allowed
249 def update_public_meta(public, meta):
252 meta['X-Object-Public'] = '/public/' + encode_url(public)
254 def validate_modification_preconditions(request, meta):
255 """Check that the modified timestamp conforms with the preconditions set."""
257 if 'modified' not in meta:
258 return # TODO: Always return?
260 if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
261 if if_modified_since is not None:
262 if_modified_since = parse_http_date_safe(if_modified_since)
263 if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
264 raise NotModified('Resource has not been modified')
266 if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
267 if if_unmodified_since is not None:
268 if_unmodified_since = parse_http_date_safe(if_unmodified_since)
269 if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
270 raise PreconditionFailed('Resource has been modified')
272 def validate_matching_preconditions(request, meta):
273 """Check that the ETag conforms with the preconditions set."""
275 etag = meta.get('ETag', None)
277 if_match = request.META.get('HTTP_IF_MATCH')
278 if if_match is not None:
280 raise PreconditionFailed('Resource does not exist')
281 if if_match != '*' and etag not in [x.lower() for x in parse_etags(if_match)]:
282 raise PreconditionFailed('Resource ETag does not match')
284 if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
285 if if_none_match is not None:
286 # TODO: If this passes, must ignore If-Modified-Since header.
288 if if_none_match == '*' or etag in [x.lower() for x in parse_etags(if_none_match)]:
289 # TODO: Continue if an If-Modified-Since header is present.
290 if request.method in ('HEAD', 'GET'):
291 raise NotModified('Resource ETag matches')
292 raise PreconditionFailed('Resource exists or ETag matches')
294 def split_container_object_string(s):
295 if not len(s) > 0 or s[0] != '/':
299 if pos == -1 or pos == len(s) - 1:
301 return s[:pos], s[(pos + 1):]
303 def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False):
304 """Copy or move an object."""
306 if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
307 del(request.META['CONTENT_TYPE'])
308 meta, permissions, public = get_object_headers(request)
309 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
312 version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
313 dest_account, dest_container, dest_name,
314 'pithos', meta, False, permissions)
316 version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
317 dest_account, dest_container, dest_name,
318 'pithos', meta, False, permissions, src_version)
319 except NotAllowedError:
320 raise Forbidden('Not allowed')
321 except (NameError, IndexError):
322 raise ItemNotFound('Container or object does not exist')
324 raise BadRequest('Invalid sharing header')
325 except AttributeError, e:
326 raise Conflict(simple_list_response(request, e.data))
328 raise RequestEntityTooLarge('Quota exceeded')
329 if public is not None:
331 request.backend.update_object_public(request.user_uniq, dest_account, dest_container, dest_name, public)
332 except NotAllowedError:
333 raise Forbidden('Not allowed')
335 raise ItemNotFound('Object does not exist')
338 def get_int_parameter(p):
348 def get_content_length(request):
349 content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
350 if content_length is None:
351 raise LengthRequired('Missing or invalid Content-Length header')
352 return content_length
354 def get_range(request, size):
355 """Parse a Range header from the request.
357 Either returns None, when the header is not existent or should be ignored,
358 or a list of (offset, length) tuples - should be further checked.
361 ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
362 if not ranges.startswith('bytes='):
366 for r in (x.strip() for x in ranges[6:].split(',')):
367 p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
371 offset = m.group('offset')
372 upto = m.group('upto')
373 if offset == '' and upto == '':
382 ret.append((offset, upto - offset + 1))
384 ret.append((offset, size - offset))
387 ret.append((size - length, length))
391 def get_content_range(request):
392 """Parse a Content-Range header from the request.
394 Either returns None, when the header is not existent or should be ignored,
395 or an (offset, length, total) tuple - check as length, total may be None.
396 Returns (None, None, None) if the provided range is '*/*'.
399 ranges = request.META.get('HTTP_CONTENT_RANGE', '')
403 p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
406 if ranges == 'bytes */*':
407 return (None, None, None)
409 offset = int(m.group('offset'))
410 upto = m.group('upto')
411 total = m.group('total')
420 if (upto is not None and offset > upto) or \
421 (total is not None and offset >= total) or \
422 (total is not None and upto is not None and upto >= total):
428 length = upto - offset + 1
429 return (offset, length, total)
431 def get_sharing(request):
432 """Parse an X-Object-Sharing header from the request.
434 Raises BadRequest on error.
437 permissions = request.META.get('HTTP_X_OBJECT_SHARING')
438 if permissions is None:
441 # TODO: Document or remove '~' replacing.
442 permissions = permissions.replace('~', '')
445 permissions = permissions.replace(' ', '')
446 if permissions == '':
448 for perm in (x for x in permissions.split(';')):
449 if perm.startswith('read='):
450 ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
451 if '' in ret['read']:
452 ret['read'].remove('')
453 if '*' in ret['read']:
455 if len(ret['read']) == 0:
456 raise BadRequest('Bad X-Object-Sharing header value')
457 elif perm.startswith('write='):
458 ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
459 if '' in ret['write']:
460 ret['write'].remove('')
461 if '*' in ret['write']:
463 if len(ret['write']) == 0:
464 raise BadRequest('Bad X-Object-Sharing header value')
466 raise BadRequest('Bad X-Object-Sharing header value')
468 # Keep duplicates only in write list.
469 dups = [x for x in ret.get('read', []) if x in ret.get('write', []) and x != '*']
472 ret['read'].remove(x)
473 if len(ret['read']) == 0:
478 def get_public(request):
479 """Parse an X-Object-Public header from the request.
481 Raises BadRequest on error.
484 public = request.META.get('HTTP_X_OBJECT_PUBLIC')
488 public = public.replace(' ', '').lower()
491 elif public == 'false' or public == '':
493 raise BadRequest('Bad X-Object-Public header value')
495 def raw_input_socket(request):
496 """Return the socket for reading the rest of the request."""
498 server_software = request.META.get('SERVER_SOFTWARE')
499 if server_software and server_software.startswith('mod_python'):
501 if 'wsgi.input' in request.environ:
502 return request.environ['wsgi.input']
503 raise NotImplemented('Unknown server software')
505 MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024) # 5GB
507 def socket_read_iterator(request, length=0, blocksize=4096):
508 """Return a maximum of blocksize data read from the socket in each iteration.
510 Read up to 'length'. If 'length' is negative, will attempt a chunked read.
511 The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
514 sock = raw_input_socket(request)
515 if length < 0: # Chunked transfers
516 # Small version (server does the dechunking).
517 if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
518 while length < MAX_UPLOAD_SIZE:
519 data = sock.read(blocksize)
523 raise BadRequest('Maximum size is reached')
525 # Long version (do the dechunking).
527 while length < MAX_UPLOAD_SIZE:
529 if hasattr(sock, 'readline'):
530 chunk_length = sock.readline()
533 while chunk_length[-1:] != '\n':
534 chunk_length += sock.read(1)
536 pos = chunk_length.find(';')
538 chunk_length = chunk_length[:pos]
540 chunk_length = int(chunk_length, 16)
542 raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
544 if chunk_length == 0:
548 # Get the actual data.
549 while chunk_length > 0:
550 chunk = sock.read(min(chunk_length, blocksize))
551 chunk_length -= len(chunk)
555 if len(data) >= blocksize:
556 ret = data[:blocksize]
557 data = data[blocksize:]
560 raise BadRequest('Maximum size is reached')
562 if length > MAX_UPLOAD_SIZE:
563 raise BadRequest('Maximum size is reached')
565 data = sock.read(min(length, blocksize))
571 class SaveToBackendHandler(FileUploadHandler):
572 """Handle a file from an HTML form the django way."""
574 def __init__(self, request=None):
575 super(SaveToBackendHandler, self).__init__(request)
576 self.backend = request.backend
578 def put_data(self, length):
579 if len(self.data) >= length:
580 block = self.data[:length]
581 self.file.hashmap.append(self.backend.put_block(block))
582 self.md5.update(block)
583 self.data = self.data[length:]
585 def new_file(self, field_name, file_name, content_type, content_length, charset=None):
586 self.md5 = hashlib.md5()
588 self.file = UploadedFile(name=file_name, content_type=content_type, charset=charset)
590 self.file.hashmap = []
592 def receive_data_chunk(self, raw_data, start):
593 self.data += raw_data
594 self.file.size += len(raw_data)
595 self.put_data(self.request.backend.block_size)
598 def file_complete(self, file_size):
602 self.file.etag = self.md5.hexdigest().lower()
605 class ObjectWrapper(object):
606 """Return the object's data block-per-block in each iteration.
608 Read from the object using the offset and length provided in each entry of the range list.
611 def __init__(self, backend, ranges, sizes, hashmaps, boundary):
612 self.backend = backend
615 self.hashmaps = hashmaps
616 self.boundary = boundary
617 self.size = sum(self.sizes)
624 self.range_index = -1
625 self.offset, self.length = self.ranges[0]
630 def part_iterator(self):
632 # Get the file for the current offset.
633 file_size = self.sizes[self.file_index]
634 while self.offset >= file_size:
635 self.offset -= file_size
637 file_size = self.sizes[self.file_index]
639 # Get the block for the current position.
640 self.block_index = int(self.offset / self.backend.block_size)
641 if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
642 self.block_hash = self.hashmaps[self.file_index][self.block_index]
644 self.block = self.backend.get_block(self.block_hash)
646 raise ItemNotFound('Block does not exist')
648 # Get the data from the block.
649 bo = self.offset % self.backend.block_size
650 bl = min(self.length, len(self.block) - bo)
651 data = self.block[bo:bo + bl]
659 if len(self.ranges) == 1:
660 return self.part_iterator()
661 if self.range_index == len(self.ranges):
664 if self.range_index == -1:
666 return self.part_iterator()
667 except StopIteration:
668 self.range_index += 1
670 if self.range_index < len(self.ranges):
672 self.offset, self.length = self.ranges[self.range_index]
674 if self.range_index > 0:
676 out.append('--' + self.boundary)
677 out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
678 out.append('Content-Transfer-Encoding: binary')
681 return '\r\n'.join(out)
685 out.append('--' + self.boundary + '--')
687 return '\r\n'.join(out)
689 def object_data_response(request, sizes, hashmaps, meta, public=False):
690 """Get the HttpResponse object for replying with the object's data."""
694 ranges = get_range(request, size)
699 check = [True for offset, length in ranges if
700 length <= 0 or length > size or
701 offset < 0 or offset >= size or
702 offset + length > size]
704 raise RangeNotSatisfiable('Requested range exceeds object limits')
706 if_range = request.META.get('HTTP_IF_RANGE')
709 # Modification time has passed instead.
710 last_modified = parse_http_date(if_range)
711 if last_modified != meta['modified']:
715 if if_range != meta['ETag']:
719 if ret == 206 and len(ranges) > 1:
720 boundary = uuid.uuid4().hex
723 wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
724 response = HttpResponse(wrapper, status=ret)
725 put_object_headers(response, meta, public)
728 offset, length = ranges[0]
729 response['Content-Length'] = length # Update with the correct length.
730 response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
732 del(response['Content-Length'])
733 response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
736 def put_object_block(request, hashmap, data, offset):
737 """Put one block of data at the given offset."""
739 bi = int(offset / request.backend.block_size)
740 bo = offset % request.backend.block_size
741 bl = min(len(data), request.backend.block_size - bo)
742 if bi < len(hashmap):
743 hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
745 hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
746 return bl # Return ammount of data written.
748 def hashmap_md5(request, hashmap, size):
749 """Produce the MD5 sum from the data in the hashmap."""
751 # TODO: Search backend for the MD5 of another object with the same hashmap and size...
753 bs = request.backend.block_size
754 for bi, hash in enumerate(hashmap):
755 data = request.backend.get_block(hash)
756 if bi == len(hashmap) - 1:
758 pad = bs - min(len(data), bs)
759 md5.update(data + ('\x00' * pad))
760 return md5.hexdigest().lower()
762 def simple_list_response(request, l):
763 if request.serialization == 'text':
764 return '\n'.join(l) + '\n'
765 if request.serialization == 'xml':
766 return render_to_string('items.xml', {'items': l})
767 if request.serialization == 'json':
771 backend = connect_backend(db_module=settings.BACKEND_DB_MODULE,
772 db_connection=settings.BACKEND_DB_CONNECTION,
773 block_module=settings.BACKEND_BLOCK_MODULE,
774 block_path=settings.BACKEND_BLOCK_PATH,
775 queue_module=settings.BACKEND_QUEUE_MODULE,
776 queue_connection=settings.BACKEND_QUEUE_CONNECTION)
777 backend.default_policy['quota'] = settings.BACKEND_QUOTA
778 backend.default_policy['versioning'] = settings.BACKEND_VERSIONING
781 def update_request_headers(request):
782 # Handle URL-encoded keys and values.
783 # Handle URL-encoded keys and values.
784 meta = dict([(k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
786 raise BadRequest('Too many headers.')
787 for k, v in meta.iteritems():
789 raise BadRequest('Header name too large.')
791 raise BadRequest('Header value too large.')
795 except UnicodeDecodeError:
796 raise BadRequest('Bad character in headers.')
797 if '%' in k or '%' in v:
799 request.META[unquote(k)] = smart_unicode(unquote(v), strings_only=True)
801 def update_response_headers(request, response):
802 if request.serialization == 'xml':
803 response['Content-Type'] = 'application/xml; charset=UTF-8'
804 elif request.serialization == 'json':
805 response['Content-Type'] = 'application/json; charset=UTF-8'
806 elif not response['Content-Type']:
807 response['Content-Type'] = 'text/plain; charset=UTF-8'
809 if (not response.has_header('Content-Length') and
810 not (response.has_header('Content-Type') and
811 response['Content-Type'].startswith('multipart/byteranges'))):
812 response['Content-Length'] = len(response.content)
814 # URL-encode unicode in headers.
815 meta = response.items()
817 if (k.startswith('X-Account-') or k.startswith('X-Container-') or
818 k.startswith('X-Object-') or k.startswith('Content-')):
820 response[quote(k)] = quote(v, safe='/=,:@; ')
823 response['Date'] = format_date_time(time())
825 def render_fault(request, fault):
826 if isinstance(fault, InternalServerError) and (settings.DEBUG or settings.TEST):
827 fault.details = format_exc(fault)
829 request.serialization = 'text'
830 data = fault.message + '\n'
832 data += '\n' + fault.details
833 response = HttpResponse(data, status=fault.code)
834 update_response_headers(request, response)
837 def request_serialization(request, format_allowed=False):
838 """Return the serialization format requested.
840 Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
843 if not format_allowed:
846 format = request.GET.get('format')
849 elif format == 'xml':
852 for item in request.META.get('HTTP_ACCEPT', '').split(','):
853 accept, sep, rest = item.strip().partition(';')
854 if accept == 'application/json':
856 elif accept == 'application/xml' or accept == 'text/xml':
861 def api_method(http_method=None, format_allowed=False, user_required=True):
862 """Decorator function for views that implement an API method."""
866 def wrapper(request, *args, **kwargs):
868 if http_method and request.method != http_method:
869 raise BadRequest('Method not allowed.')
870 if user_required and getattr(request, 'user', None) is None:
871 raise Unauthorized('Access denied')
873 # The args variable may contain up to (account, container, object).
874 if len(args) > 1 and len(args[1]) > 256:
875 raise BadRequest('Container name too large.')
876 if len(args) > 2 and len(args[2]) > 1024:
877 raise BadRequest('Object name too large.')
879 # Format and check headers.
880 update_request_headers(request)
882 # Fill in custom request variables.
883 request.serialization = request_serialization(request, format_allowed)
884 request.backend = get_backend()
886 response = func(request, *args, **kwargs)
887 update_response_headers(request, response)
890 return render_fault(request, fault)
891 except BaseException, e:
892 logger.exception('Unexpected error: %s' % e)
893 fault = InternalServerError('Unexpected error')
894 return render_fault(request, fault)
896 if getattr(request, 'backend', None) is not None:
897 request.backend.close()