1 # Copyright 2011 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.
38 from django.http import HttpResponse
39 from django.template.loader import render_to_string
40 from django.utils import simplejson as json
41 from django.utils.http import parse_etags
43 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
44 LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
45 from pithos.api.util import (format_header_key, printable_header_dict, get_account_headers,
46 put_account_headers, get_container_headers, put_container_headers, get_object_headers, put_object_headers,
47 update_manifest_meta, update_sharing_meta, validate_modification_preconditions,
48 validate_matching_preconditions, split_container_object_string, copy_or_move_object,
49 get_int_parameter, get_content_length, get_content_range, get_sharing, raw_input_socket,
50 socket_read_iterator, object_data_response, put_object_block, hashmap_hash, api_method)
51 from pithos.backends import backend
52 from pithos.backends.base import NotAllowedError
55 logger = logging.getLogger(__name__)
58 def top_demux(request):
59 if request.method == 'GET':
60 return authenticate(request)
62 return method_not_allowed(request)
64 def account_demux(request, v_account):
65 if request.method == 'HEAD':
66 return account_meta(request, v_account)
67 elif request.method == 'POST':
68 return account_update(request, v_account)
69 elif request.method == 'GET':
70 return container_list(request, v_account)
72 return method_not_allowed(request)
74 def container_demux(request, v_account, v_container):
75 if request.method == 'HEAD':
76 return container_meta(request, v_account, v_container)
77 elif request.method == 'PUT':
78 return container_create(request, v_account, v_container)
79 elif request.method == 'POST':
80 return container_update(request, v_account, v_container)
81 elif request.method == 'DELETE':
82 return container_delete(request, v_account, v_container)
83 elif request.method == 'GET':
84 return object_list(request, v_account, v_container)
86 return method_not_allowed(request)
88 def object_demux(request, v_account, v_container, v_object):
89 if request.method == 'HEAD':
90 return object_meta(request, v_account, v_container, v_object)
91 elif request.method == 'GET':
92 return object_read(request, v_account, v_container, v_object)
93 elif request.method == 'PUT':
94 return object_write(request, v_account, v_container, v_object)
95 elif request.method == 'COPY':
96 return object_copy(request, v_account, v_container, v_object)
97 elif request.method == 'MOVE':
98 return object_move(request, v_account, v_container, v_object)
99 elif request.method == 'POST':
100 return object_update(request, v_account, v_container, v_object)
101 elif request.method == 'DELETE':
102 return object_delete(request, v_account, v_container, v_object)
104 return method_not_allowed(request)
107 def authenticate(request):
108 # Normal Response Codes: 204
109 # Error Response Codes: serviceUnavailable (503),
110 # unauthorized (401),
113 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
114 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
115 if not x_auth_user or not x_auth_key:
116 raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
117 response = HttpResponse(status=204)
118 response['X-Auth-Token'] = '0000'
119 response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
123 def account_meta(request, v_account):
124 # Normal Response Codes: 204
125 # Error Response Codes: serviceUnavailable (503),
126 # unauthorized (401),
129 until = get_int_parameter(request, 'until')
131 meta = backend.get_account_meta(request.user, v_account, until)
132 groups = backend.get_account_groups(request.user, v_account)
133 except NotAllowedError:
134 raise Unauthorized('Access denied')
136 response = HttpResponse(status=204)
137 put_account_headers(response, meta, groups)
141 def account_update(request, v_account):
142 # Normal Response Codes: 202
143 # Error Response Codes: serviceUnavailable (503),
144 # unauthorized (401),
147 meta, groups = get_account_headers(request)
149 if 'update' in request.GET:
153 backend.update_account_groups(request.user, v_account, groups, replace)
154 except NotAllowedError:
155 raise Unauthorized('Access denied')
157 raise BadRequest('Invalid groups header')
159 backend.update_account_meta(request.user, v_account, meta, replace)
160 except NotAllowedError:
161 raise Unauthorized('Access denied')
162 return HttpResponse(status=202)
164 @api_method('GET', format_allowed=True)
165 def container_list(request, v_account):
166 # Normal Response Codes: 200, 204
167 # Error Response Codes: serviceUnavailable (503),
168 # itemNotFound (404),
169 # unauthorized (401),
172 until = get_int_parameter(request, 'until')
174 meta = backend.get_account_meta(request.user, v_account, until)
175 groups = backend.get_account_groups(request.user, v_account)
176 except NotAllowedError:
177 raise Unauthorized('Access denied')
179 validate_modification_preconditions(request, meta)
181 response = HttpResponse()
182 put_account_headers(response, meta, groups)
184 marker = request.GET.get('marker')
185 limit = request.GET.get('limit')
195 containers = backend.list_containers(request.user, v_account, marker, limit, until)
196 except NotAllowedError:
197 raise Unauthorized('Access denied')
201 if request.serialization == 'text':
202 if len(containers) == 0:
203 # The cloudfiles python bindings expect 200 if json/xml.
204 response.status_code = 204
206 response.status_code = 200
207 response.content = '\n'.join([x[0] for x in containers]) + '\n'
214 meta = backend.get_container_meta(request.user, v_account, x[0], until)
215 container_meta.append(printable_header_dict(meta))
216 except NotAllowedError:
217 raise Unauthorized('Access denied')
220 if request.serialization == 'xml':
221 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
222 elif request.serialization == 'json':
223 data = json.dumps(container_meta)
224 response.status_code = 200
225 response.content = data
229 def container_meta(request, v_account, v_container):
230 # Normal Response Codes: 204
231 # Error Response Codes: serviceUnavailable (503),
232 # itemNotFound (404),
233 # unauthorized (401),
236 until = get_int_parameter(request, 'until')
238 meta = backend.get_container_meta(request.user, v_account, v_container, until)
239 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
240 except NotAllowedError:
241 raise Unauthorized('Access denied')
243 raise ItemNotFound('Container does not exist')
245 response = HttpResponse(status=204)
246 put_container_headers(response, meta)
250 def container_create(request, v_account, v_container):
251 # Normal Response Codes: 201, 202
252 # Error Response Codes: serviceUnavailable (503),
253 # itemNotFound (404),
254 # unauthorized (401),
257 meta = get_container_headers(request)
260 backend.put_container(request.user, v_account, v_container)
262 except NotAllowedError:
263 raise Unauthorized('Access denied')
269 backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
270 except NotAllowedError:
271 raise Unauthorized('Access denied')
273 raise ItemNotFound('Container does not exist')
275 return HttpResponse(status=ret)
278 def container_update(request, v_account, v_container):
279 # Normal Response Codes: 202
280 # Error Response Codes: serviceUnavailable (503),
281 # itemNotFound (404),
282 # unauthorized (401),
285 meta = get_container_headers(request)
287 if 'update' in request.GET:
290 backend.update_container_meta(request.user, v_account, v_container, meta, replace)
291 except NotAllowedError:
292 raise Unauthorized('Access denied')
294 raise ItemNotFound('Container does not exist')
295 return HttpResponse(status=202)
297 @api_method('DELETE')
298 def container_delete(request, v_account, v_container):
299 # Normal Response Codes: 204
300 # Error Response Codes: serviceUnavailable (503),
302 # itemNotFound (404),
303 # unauthorized (401),
307 backend.delete_container(request.user, v_account, v_container)
308 except NotAllowedError:
309 raise Unauthorized('Access denied')
311 raise ItemNotFound('Container does not exist')
313 raise Conflict('Container is not empty')
314 return HttpResponse(status=204)
316 @api_method('GET', format_allowed=True)
317 def object_list(request, v_account, v_container):
318 # Normal Response Codes: 200, 204
319 # Error Response Codes: serviceUnavailable (503),
320 # itemNotFound (404),
321 # unauthorized (401),
324 until = get_int_parameter(request, 'until')
326 meta = backend.get_container_meta(request.user, v_account, v_container, until)
327 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
328 except NotAllowedError:
329 raise Unauthorized('Access denied')
331 raise ItemNotFound('Container does not exist')
333 validate_modification_preconditions(request, meta)
335 response = HttpResponse()
336 put_container_headers(response, meta)
338 path = request.GET.get('path')
339 prefix = request.GET.get('prefix')
340 delimiter = request.GET.get('delimiter')
342 # Path overrides prefix and delimiter.
350 if prefix and delimiter:
351 prefix = prefix + delimiter
354 prefix = prefix.lstrip('/')
356 marker = request.GET.get('marker')
357 limit = request.GET.get('limit')
366 keys = request.GET.get('meta')
368 keys = keys.split(',')
369 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in keys if x.strip() != '']
374 objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, until)
375 except NotAllowedError:
376 raise Unauthorized('Access denied')
378 raise ItemNotFound('Container does not exist')
380 if request.serialization == 'text':
381 if len(objects) == 0:
382 # The cloudfiles python bindings expect 200 if json/xml.
383 response.status_code = 204
385 response.status_code = 200
386 response.content = '\n'.join([x[0] for x in objects]) + '\n'
392 # Virtual objects/directories.
393 object_meta.append({'subdir': x[0]})
396 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
398 permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
401 except NotAllowedError:
402 raise Unauthorized('Access denied')
405 update_sharing_meta(permissions, v_account, v_container, x[0], meta)
406 object_meta.append(printable_header_dict(meta))
407 if request.serialization == 'xml':
408 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
409 elif request.serialization == 'json':
410 data = json.dumps(object_meta)
411 response.status_code = 200
412 response.content = data
416 def object_meta(request, v_account, v_container, v_object):
417 # Normal Response Codes: 204
418 # Error Response Codes: serviceUnavailable (503),
419 # itemNotFound (404),
420 # unauthorized (401),
423 version = request.GET.get('version')
425 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
427 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
430 except NotAllowedError:
431 raise Unauthorized('Access denied')
433 raise ItemNotFound('Object does not exist')
435 raise ItemNotFound('Version does not exist')
437 update_manifest_meta(request, v_account, meta)
438 update_sharing_meta(permissions, v_account, v_container, v_object, meta)
440 response = HttpResponse(status=200)
441 put_object_headers(response, meta)
444 @api_method('GET', format_allowed=True)
445 def object_read(request, v_account, v_container, v_object):
446 # Normal Response Codes: 200, 206
447 # Error Response Codes: serviceUnavailable (503),
448 # rangeNotSatisfiable (416),
449 # preconditionFailed (412),
450 # itemNotFound (404),
451 # unauthorized (401),
455 version = request.GET.get('version')
457 # Reply with the version list. Do this first, as the object may be deleted.
458 if version == 'list':
459 if request.serialization == 'text':
460 raise BadRequest('No format specified for version list.')
463 v = backend.list_versions(request.user, v_account, v_container, v_object)
464 except NotAllowedError:
465 raise Unauthorized('Access denied')
467 if request.serialization == 'xml':
468 d['object'] = v_object
469 data = render_to_string('versions.xml', d)
470 elif request.serialization == 'json':
473 response = HttpResponse(data, status=200)
474 response['Content-Length'] = len(data)
478 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
480 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
483 except NotAllowedError:
484 raise Unauthorized('Access denied')
486 raise ItemNotFound('Object does not exist')
488 raise ItemNotFound('Version does not exist')
490 update_manifest_meta(request, v_account, meta)
491 update_sharing_meta(permissions, v_account, v_container, v_object, meta)
493 # Evaluate conditions.
494 validate_modification_preconditions(request, meta)
496 validate_matching_preconditions(request, meta)
498 response = HttpResponse(status=304)
499 response['ETag'] = meta['hash']
504 if 'X-Object-Manifest' in meta:
506 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
507 objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
508 except NotAllowedError:
509 raise Unauthorized('Access denied')
511 raise BadRequest('Invalid X-Object-Manifest header')
513 raise ItemNotFound('Container does not exist')
517 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
520 except NotAllowedError:
521 raise Unauthorized('Access denied')
523 raise ItemNotFound('Object does not exist')
525 raise ItemNotFound('Version does not exist')
528 s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
531 except NotAllowedError:
532 raise Unauthorized('Access denied')
534 raise ItemNotFound('Object does not exist')
536 raise ItemNotFound('Version does not exist')
538 # Reply with the hashmap.
539 if request.serialization != 'text':
541 hashmap = sum(hashmaps, [])
542 d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
543 if request.serialization == 'xml':
544 d['object'] = v_object
545 data = render_to_string('hashes.xml', d)
546 elif request.serialization == 'json':
549 response = HttpResponse(data, status=200)
550 put_object_headers(response, meta)
551 response['Content-Length'] = len(data)
554 return object_data_response(request, sizes, hashmaps, meta)
556 @api_method('PUT', format_allowed=True)
557 def object_write(request, v_account, v_container, v_object):
558 # Normal Response Codes: 201
559 # Error Response Codes: serviceUnavailable (503),
560 # unprocessableEntity (422),
561 # lengthRequired (411),
563 # itemNotFound (404),
564 # unauthorized (401),
566 copy_from = request.META.get('HTTP_X_COPY_FROM')
567 move_from = request.META.get('HTTP_X_MOVE_FROM')
568 if copy_from or move_from:
569 # TODO: Why is this required? Copy this ammount?
570 content_length = get_content_length(request)
574 src_container, src_name = split_container_object_string(move_from)
576 raise BadRequest('Invalid X-Move-From header')
577 copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
580 src_container, src_name = split_container_object_string(copy_from)
582 raise BadRequest('Invalid X-Copy-From header')
583 copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
584 return HttpResponse(status=201)
586 meta = get_object_headers(request)
587 permissions = get_sharing(request)
589 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
590 content_length = get_content_length(request)
591 # Should be BadRequest, but API says otherwise.
592 if 'Content-Type' not in meta:
593 raise LengthRequired('Missing Content-Type header')
595 if request.serialization == 'json':
597 sock = raw_input_socket(request)
598 for block in socket_read_iterator(sock, content_length, backend.block_size):
599 data = '%s%s' % (data, block)
601 if not hasattr(d, '__getitem__'):
602 raise BadRequest('Invalid data formating')
604 hashmap = d['hashes']
607 raise BadRequest('Invalid data formatting')
608 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
609 elif request.serialization == 'xml':
610 #TODO support for xml
611 raise BadRequest('Format xml is not supported')
616 sock = raw_input_socket(request)
617 for data in socket_read_iterator(sock, content_length, backend.block_size):
618 # TODO: Raise 408 (Request Timeout) if this takes too long.
619 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
621 hashmap.append(backend.put_block(data))
624 meta['hash'] = md5.hexdigest().lower()
625 etag = request.META.get('HTTP_ETAG')
626 if etag and parse_etags(etag)[0].lower() != meta['hash']:
627 raise UnprocessableEntity('Object ETag does not match')
632 backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
633 except NotAllowedError:
634 raise Unauthorized('Access denied')
635 except IndexError, e:
636 payload = json.dumps(e.data)
639 raise ItemNotFound('Container does not exist')
641 raise BadRequest('Invalid sharing header')
642 except AttributeError:
643 raise Conflict('Sharing already set above or below this path in the hierarchy')
645 response = HttpResponse(content=payload, status=code)
646 response['ETag'] = meta['hash']
650 def object_copy(request, v_account, v_container, v_object):
651 # Normal Response Codes: 201
652 # Error Response Codes: serviceUnavailable (503),
653 # itemNotFound (404),
654 # unauthorized (401),
657 dest_path = request.META.get('HTTP_DESTINATION')
659 raise BadRequest('Missing Destination header')
661 dest_container, dest_name = split_container_object_string(dest_path)
663 raise BadRequest('Invalid Destination header')
664 copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
665 return HttpResponse(status=201)
668 def object_move(request, v_account, v_container, v_object):
669 # Normal Response Codes: 201
670 # Error Response Codes: serviceUnavailable (503),
671 # itemNotFound (404),
672 # unauthorized (401),
675 dest_path = request.META.get('HTTP_DESTINATION')
677 raise BadRequest('Missing Destination header')
679 dest_container, dest_name = split_container_object_string(dest_path)
681 raise BadRequest('Invalid Destination header')
682 copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
683 return HttpResponse(status=201)
686 def object_update(request, v_account, v_container, v_object):
687 # Normal Response Codes: 202, 204
688 # Error Response Codes: serviceUnavailable (503),
690 # itemNotFound (404),
691 # unauthorized (401),
694 meta = get_object_headers(request)
695 permissions = get_sharing(request)
696 content_type = meta.get('Content-Type')
698 del(meta['Content-Type']) # Do not allow changing the Content-Type.
701 prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
702 except NotAllowedError:
703 raise Unauthorized('Access denied')
705 raise ItemNotFound('Object does not exist')
706 # If replacing, keep previous values of 'Content-Type' and 'hash'.
708 if 'update' in request.GET:
711 for k in ('Content-Type', 'hash'):
713 meta[k] = prev_meta[k]
715 # A Content-Type header indicates data updates.
716 if not content_type or content_type != 'application/octet-stream':
717 # Do permissions first, as it may fail easier.
718 if permissions is not None:
720 backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
721 except NotAllowedError:
722 raise Unauthorized('Access denied')
724 raise ItemNotFound('Object does not exist')
726 raise BadRequest('Invalid sharing header')
727 except AttributeError:
728 raise Conflict('Sharing already set above or below this path in the hierarchy')
730 backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace)
731 except NotAllowedError:
732 raise Unauthorized('Access denied')
734 raise ItemNotFound('Object does not exist')
735 return HttpResponse(status=202)
737 # Single range update. Range must be in Content-Range.
738 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
739 # (with the addition that '*' is allowed for the range - will append).
740 content_range = request.META.get('HTTP_CONTENT_RANGE')
741 if not content_range:
742 raise BadRequest('Missing Content-Range header')
743 ranges = get_content_range(request)
745 raise RangeNotSatisfiable('Invalid Content-Range header')
746 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
748 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
749 content_length = get_content_length(request)
752 size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
753 except NotAllowedError:
754 raise Unauthorized('Access denied')
756 raise ItemNotFound('Object does not exist')
758 offset, length, total = ranges
762 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
763 if length is None or content_length == -1:
764 length = content_length # Nevermind the error.
765 elif length != content_length:
766 raise BadRequest('Content length does not match range length')
767 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
768 raise RangeNotSatisfiable('Supplied range will change provided object limits')
770 sock = raw_input_socket(request)
772 for d in socket_read_iterator(sock, length, backend.block_size):
773 # TODO: Raise 408 (Request Timeout) if this takes too long.
774 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
776 bytes = put_object_block(hashmap, data, offset)
780 put_object_block(hashmap, data, offset)
784 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
786 backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, replace, permissions)
787 except NotAllowedError:
788 raise Unauthorized('Access denied')
790 raise ItemNotFound('Container does not exist')
792 raise BadRequest('Invalid sharing header')
793 except AttributeError:
794 raise Conflict('Sharing already set above or below this path in the hierarchy')
796 response = HttpResponse(status=204)
797 response['ETag'] = meta['hash']
800 @api_method('DELETE')
801 def object_delete(request, v_account, v_container, v_object):
802 # Normal Response Codes: 204
803 # Error Response Codes: serviceUnavailable (503),
804 # itemNotFound (404),
805 # unauthorized (401),
809 backend.delete_object(request.user, v_account, v_container, v_object)
810 except NotAllowedError:
811 raise Unauthorized('Access denied')
813 raise ItemNotFound('Object does not exist')
814 return HttpResponse(status=204)
817 def method_not_allowed(request):
818 raise BadRequest('Method not allowed')