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.
37 from django.conf import settings
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
42 from django.utils.encoding import smart_unicode, smart_str
43 from xml.dom import minidom
45 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
46 LengthRequired, PreconditionFailed, RequestEntityTooLarge, RangeNotSatisfiable, UnprocessableEntity)
47 from pithos.api.util import (rename_meta_key, format_header_key, printable_header_dict, get_account_headers,
48 put_account_headers, get_container_headers, put_container_headers, get_object_headers, put_object_headers,
49 update_manifest_meta, update_sharing_meta, update_public_meta, validate_modification_preconditions,
50 validate_matching_preconditions, split_container_object_string, copy_or_move_object,
51 get_int_parameter, get_content_length, get_content_range, socket_read_iterator,
52 object_data_response, put_object_block, hashmap_hash, api_method, json_encode_decimal)
53 from pithos.backends import connect_backend
54 from pithos.backends.base import NotAllowedError, QuotaError
57 logger = logging.getLogger(__name__)
60 def top_demux(request):
61 if request.method == 'GET':
63 return account_list(request)
64 return authenticate(request)
66 return method_not_allowed(request)
68 def account_demux(request, v_account):
69 if request.method == 'HEAD':
70 return account_meta(request, v_account)
71 elif request.method == 'POST':
72 return account_update(request, v_account)
73 elif request.method == 'GET':
74 return container_list(request, v_account)
76 return method_not_allowed(request)
78 def container_demux(request, v_account, v_container):
79 if request.method == 'HEAD':
80 return container_meta(request, v_account, v_container)
81 elif request.method == 'PUT':
82 return container_create(request, v_account, v_container)
83 elif request.method == 'POST':
84 return container_update(request, v_account, v_container)
85 elif request.method == 'DELETE':
86 return container_delete(request, v_account, v_container)
87 elif request.method == 'GET':
88 return object_list(request, v_account, v_container)
90 return method_not_allowed(request)
92 def object_demux(request, v_account, v_container, v_object):
93 if request.method == 'HEAD':
94 return object_meta(request, v_account, v_container, v_object)
95 elif request.method == 'GET':
96 return object_read(request, v_account, v_container, v_object)
97 elif request.method == 'PUT':
98 return object_write(request, v_account, v_container, v_object)
99 elif request.method == 'COPY':
100 return object_copy(request, v_account, v_container, v_object)
101 elif request.method == 'MOVE':
102 return object_move(request, v_account, v_container, v_object)
103 elif request.method == 'POST':
104 if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
105 return object_write_form(request, v_account, v_container, v_object)
106 return object_update(request, v_account, v_container, v_object)
107 elif request.method == 'DELETE':
108 return object_delete(request, v_account, v_container, v_object)
110 return method_not_allowed(request)
113 def authenticate(request):
114 # Normal Response Codes: 204
115 # Error Response Codes: serviceUnavailable (503),
116 # unauthorized (401),
119 x_auth_user = request.META.get('HTTP_X_AUTH_USER')
120 x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
121 if not x_auth_user or not x_auth_key:
122 raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
123 response = HttpResponse(status=204)
125 uri = request.build_absolute_uri()
127 uri = uri[:uri.find('?')]
129 response['X-Auth-Token'] = x_auth_key
130 response['X-Storage-Url'] = uri + (uri.endswith('/') and '' or '/') + x_auth_user
133 @api_method('GET', format_allowed=True)
134 def account_list(request):
135 # Normal Response Codes: 200, 204
136 # Error Response Codes: serviceUnavailable (503),
139 response = HttpResponse()
141 marker = request.GET.get('marker')
142 limit = get_int_parameter(request.GET.get('limit'))
146 accounts = request.backend.list_accounts(request.user, marker, limit)
148 if request.serialization == 'text':
149 if len(accounts) == 0:
150 # The cloudfiles python bindings expect 200 if json/xml.
151 response.status_code = 204
153 response.status_code = 200
154 response.content = '\n'.join(accounts) + '\n'
160 meta = request.backend.get_account_meta(request.user, x)
161 groups = request.backend.get_account_groups(request.user, x)
162 except NotAllowedError:
163 raise Unauthorized('Access denied')
165 rename_meta_key(meta, 'modified', 'last_modified')
166 rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
167 for k, v in groups.iteritems():
168 meta['X-Container-Group-' + k] = ','.join(v)
169 account_meta.append(printable_header_dict(meta))
170 if request.serialization == 'xml':
171 data = render_to_string('accounts.xml', {'accounts': account_meta})
172 elif request.serialization == 'json':
173 data = json.dumps(account_meta)
174 response.status_code = 200
175 response.content = data
179 def account_meta(request, v_account):
180 # Normal Response Codes: 204
181 # Error Response Codes: serviceUnavailable (503),
182 # unauthorized (401),
185 until = get_int_parameter(request.GET.get('until'))
187 meta = request.backend.get_account_meta(request.user, v_account, until)
188 groups = request.backend.get_account_groups(request.user, v_account)
189 policy = request.backend.get_account_policy(request.user, v_account)
190 except NotAllowedError:
191 raise Unauthorized('Access denied')
193 validate_modification_preconditions(request, meta)
195 response = HttpResponse(status=204)
196 put_account_headers(response, meta, groups, policy)
200 def account_update(request, v_account):
201 # Normal Response Codes: 202
202 # Error Response Codes: serviceUnavailable (503),
203 # unauthorized (401),
206 meta, groups = get_account_headers(request)
208 if 'update' in request.GET:
212 request.backend.update_account_groups(request.user, v_account,
214 except NotAllowedError:
215 raise Unauthorized('Access denied')
217 raise BadRequest('Invalid groups header')
220 request.backend.update_account_meta(request.user, v_account, meta,
222 except NotAllowedError:
223 raise Unauthorized('Access denied')
224 return HttpResponse(status=202)
226 @api_method('GET', format_allowed=True)
227 def container_list(request, v_account):
228 # Normal Response Codes: 200, 204
229 # Error Response Codes: serviceUnavailable (503),
230 # itemNotFound (404),
231 # unauthorized (401),
234 until = get_int_parameter(request.GET.get('until'))
236 meta = request.backend.get_account_meta(request.user, v_account, until)
237 groups = request.backend.get_account_groups(request.user, v_account)
238 policy = request.backend.get_account_policy(request.user, v_account)
239 except NotAllowedError:
240 raise Unauthorized('Access denied')
242 validate_modification_preconditions(request, meta)
244 response = HttpResponse()
245 put_account_headers(response, meta, groups, policy)
247 marker = request.GET.get('marker')
248 limit = get_int_parameter(request.GET.get('limit'))
253 if 'shared' in request.GET:
257 containers = request.backend.list_containers(request.user, v_account,
258 marker, limit, shared, until)
259 except NotAllowedError:
260 raise Unauthorized('Access denied')
264 if request.serialization == 'text':
265 if len(containers) == 0:
266 # The cloudfiles python bindings expect 200 if json/xml.
267 response.status_code = 204
269 response.status_code = 200
270 response.content = '\n'.join(containers) + '\n'
276 meta = request.backend.get_container_meta(request.user, v_account,
278 policy = request.backend.get_container_policy(request.user,
280 except NotAllowedError:
281 raise Unauthorized('Access denied')
285 rename_meta_key(meta, 'modified', 'last_modified')
286 rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
287 for k, v in policy.iteritems():
288 meta['X-Container-Policy-' + k] = v
289 container_meta.append(printable_header_dict(meta))
290 if request.serialization == 'xml':
291 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
292 elif request.serialization == 'json':
293 data = json.dumps(container_meta)
294 response.status_code = 200
295 response.content = data
299 def container_meta(request, v_account, v_container):
300 # Normal Response Codes: 204
301 # Error Response Codes: serviceUnavailable (503),
302 # itemNotFound (404),
303 # unauthorized (401),
306 until = get_int_parameter(request.GET.get('until'))
308 meta = request.backend.get_container_meta(request.user, v_account,
310 meta['object_meta'] = request.backend.list_object_meta(request.user,
311 v_account, v_container, until)
312 policy = request.backend.get_container_policy(request.user, v_account,
314 except NotAllowedError:
315 raise Unauthorized('Access denied')
317 raise ItemNotFound('Container does not exist')
319 validate_modification_preconditions(request, meta)
321 response = HttpResponse(status=204)
322 put_container_headers(request, response, meta, policy)
326 def container_create(request, v_account, v_container):
327 # Normal Response Codes: 201, 202
328 # Error Response Codes: serviceUnavailable (503),
329 # itemNotFound (404),
330 # unauthorized (401),
333 meta, policy = get_container_headers(request)
336 request.backend.put_container(request.user, v_account, v_container, policy)
338 except NotAllowedError:
339 raise Unauthorized('Access denied')
341 raise BadRequest('Invalid policy header')
345 if ret == 202 and policy:
347 request.backend.update_container_policy(request.user, v_account,
348 v_container, policy, replace=False)
349 except NotAllowedError:
350 raise Unauthorized('Access denied')
352 raise ItemNotFound('Container does not exist')
354 raise BadRequest('Invalid policy header')
357 request.backend.update_container_meta(request.user, v_account,
358 v_container, meta, replace=False)
359 except NotAllowedError:
360 raise Unauthorized('Access denied')
362 raise ItemNotFound('Container does not exist')
364 return HttpResponse(status=ret)
367 def container_update(request, v_account, v_container):
368 # Normal Response Codes: 202
369 # Error Response Codes: serviceUnavailable (503),
370 # itemNotFound (404),
371 # unauthorized (401),
374 meta, policy = get_container_headers(request)
376 if 'update' in request.GET:
380 request.backend.update_container_policy(request.user, v_account,
381 v_container, policy, replace)
382 except NotAllowedError:
383 raise Unauthorized('Access denied')
385 raise ItemNotFound('Container does not exist')
387 raise BadRequest('Invalid policy header')
390 request.backend.update_container_meta(request.user, v_account,
391 v_container, meta, replace)
392 except NotAllowedError:
393 raise Unauthorized('Access denied')
395 raise ItemNotFound('Container does not exist')
398 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
399 content_length = get_int_parameter(request.META.get('CONTENT_LENGTH', 0))
400 content_type = request.META.get('CONTENT_TYPE')
402 if content_type and content_type == 'application/octet-stream' and content_length != 0:
403 for data in socket_read_iterator(request, content_length,
404 request.backend.block_size):
405 # TODO: Raise 408 (Request Timeout) if this takes too long.
406 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
407 hashmap.append(request.backend.put_block(data))
409 response = HttpResponse(status=202)
411 response.content = '\n'.join(hashmap) + '\n'
414 @api_method('DELETE')
415 def container_delete(request, v_account, v_container):
416 # Normal Response Codes: 204
417 # Error Response Codes: serviceUnavailable (503),
419 # itemNotFound (404),
420 # unauthorized (401),
423 until = get_int_parameter(request.GET.get('until'))
425 request.backend.delete_container(request.user, v_account, v_container,
427 except NotAllowedError:
428 raise Unauthorized('Access denied')
430 raise ItemNotFound('Container does not exist')
432 raise Conflict('Container is not empty')
433 return HttpResponse(status=204)
435 @api_method('GET', format_allowed=True)
436 def object_list(request, v_account, v_container):
437 # Normal Response Codes: 200, 204
438 # Error Response Codes: serviceUnavailable (503),
439 # itemNotFound (404),
440 # unauthorized (401),
443 until = get_int_parameter(request.GET.get('until'))
445 meta = request.backend.get_container_meta(request.user, v_account,
447 meta['object_meta'] = request.backend.list_object_meta(request.user,
448 v_account, v_container, until)
449 policy = request.backend.get_container_policy(request.user, v_account,
451 except NotAllowedError:
452 raise Unauthorized('Access denied')
454 raise ItemNotFound('Container does not exist')
456 validate_modification_preconditions(request, meta)
458 response = HttpResponse()
459 put_container_headers(request, response, meta, policy)
461 path = request.GET.get('path')
462 prefix = request.GET.get('prefix')
463 delimiter = request.GET.get('delimiter')
465 # Path overrides prefix and delimiter.
473 if prefix and delimiter:
474 prefix = prefix + delimiter
477 prefix = prefix.lstrip('/')
479 marker = request.GET.get('marker')
480 limit = get_int_parameter(request.GET.get('limit'))
484 keys = request.GET.get('meta')
486 keys = keys.split(',')
487 l = [smart_str(x) for x in keys if x.strip() != '']
488 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
493 if 'shared' in request.GET:
497 objects = request.backend.list_objects(request.user, v_account,
498 v_container, prefix, delimiter, marker,
499 limit, virtual, keys, shared, until)
500 except NotAllowedError:
501 raise Unauthorized('Access denied')
503 raise ItemNotFound('Container does not exist')
505 if request.serialization == 'text':
506 if len(objects) == 0:
507 # The cloudfiles python bindings expect 200 if json/xml.
508 response.status_code = 204
510 response.status_code = 200
511 response.content = '\n'.join([x[0] for x in objects]) + '\n'
517 # Virtual objects/directories.
518 object_meta.append({'subdir': x[0]})
521 meta = request.backend.get_object_meta(request.user, v_account,
522 v_container, x[0], x[1])
524 permissions = request.backend.get_object_permissions(
525 request.user, v_account, v_container, x[0])
526 public = request.backend.get_object_public(request.user,
527 v_account, v_container, x[0])
531 except NotAllowedError:
532 raise Unauthorized('Access denied')
536 rename_meta_key(meta, 'modified', 'last_modified')
537 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
538 rename_meta_key(meta, 'version', 'x_object_version')
539 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
540 update_sharing_meta(request, permissions, v_account, v_container, x[0], meta)
541 update_public_meta(public, meta)
542 object_meta.append(printable_header_dict(meta))
543 if request.serialization == 'xml':
544 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
545 elif request.serialization == 'json':
546 data = json.dumps(object_meta, default=json_encode_decimal)
547 response.status_code = 200
548 response.content = data
552 def object_meta(request, v_account, v_container, v_object):
553 # Normal Response Codes: 204
554 # Error Response Codes: serviceUnavailable (503),
555 # itemNotFound (404),
556 # unauthorized (401),
559 version = request.GET.get('version')
561 meta = request.backend.get_object_meta(request.user, v_account,
562 v_container, v_object, version)
564 permissions = request.backend.get_object_permissions(request.user,
565 v_account, v_container, v_object)
566 public = request.backend.get_object_public(request.user, v_account,
567 v_container, v_object)
571 except NotAllowedError:
572 raise Unauthorized('Access denied')
574 raise ItemNotFound('Object does not exist')
576 raise ItemNotFound('Version does not exist')
578 update_manifest_meta(request, v_account, meta)
579 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
580 update_public_meta(public, meta)
582 # Evaluate conditions.
583 validate_modification_preconditions(request, meta)
585 validate_matching_preconditions(request, meta)
587 response = HttpResponse(status=304)
588 response['ETag'] = meta['hash']
591 response = HttpResponse(status=200)
592 put_object_headers(response, meta)
595 @api_method('GET', format_allowed=True)
596 def object_read(request, v_account, v_container, v_object):
597 # Normal Response Codes: 200, 206
598 # Error Response Codes: serviceUnavailable (503),
599 # rangeNotSatisfiable (416),
600 # preconditionFailed (412),
601 # itemNotFound (404),
602 # unauthorized (401),
606 version = request.GET.get('version')
608 # Reply with the version list. Do this first, as the object may be deleted.
609 if version == 'list':
610 if request.serialization == 'text':
611 raise BadRequest('No format specified for version list.')
614 v = request.backend.list_versions(request.user, v_account,
615 v_container, v_object)
616 except NotAllowedError:
617 raise Unauthorized('Access denied')
619 if request.serialization == 'xml':
620 d['object'] = v_object
621 data = render_to_string('versions.xml', d)
622 elif request.serialization == 'json':
623 data = json.dumps(d, default=json_encode_decimal)
625 response = HttpResponse(data, status=200)
626 response['Content-Length'] = len(data)
630 meta = request.backend.get_object_meta(request.user, v_account,
631 v_container, v_object, version)
633 permissions = request.backend.get_object_permissions(request.user,
634 v_account, v_container, v_object)
635 public = request.backend.get_object_public(request.user, v_account,
636 v_container, v_object)
640 except NotAllowedError:
641 raise Unauthorized('Access denied')
643 raise ItemNotFound('Object does not exist')
645 raise ItemNotFound('Version does not exist')
647 update_manifest_meta(request, v_account, meta)
648 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
649 update_public_meta(public, meta)
651 # Evaluate conditions.
652 validate_modification_preconditions(request, meta)
654 validate_matching_preconditions(request, meta)
656 response = HttpResponse(status=304)
657 response['ETag'] = meta['hash']
662 if 'X-Object-Manifest' in meta:
664 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
665 objects = request.backend.list_objects(request.user, v_account,
666 src_container, prefix=src_name, virtual=False)
667 except NotAllowedError:
668 raise Unauthorized('Access denied')
670 raise BadRequest('Invalid X-Object-Manifest header')
672 raise ItemNotFound('Container does not exist')
676 s, h = request.backend.get_object_hashmap(request.user,
677 v_account, src_container, x[0], x[1])
680 except NotAllowedError:
681 raise Unauthorized('Access denied')
683 raise ItemNotFound('Object does not exist')
685 raise ItemNotFound('Version does not exist')
688 s, h = request.backend.get_object_hashmap(request.user, v_account,
689 v_container, v_object, version)
692 except NotAllowedError:
693 raise Unauthorized('Access denied')
695 raise ItemNotFound('Object does not exist')
697 raise ItemNotFound('Version does not exist')
699 # Reply with the hashmap.
700 if 'hashmap' in request.GET and request.serialization != 'text':
702 hashmap = sum(hashmaps, [])
704 'block_size': request.backend.block_size,
705 'block_hash': request.backend.hash_algorithm,
708 if request.serialization == 'xml':
709 d['object'] = v_object
710 data = render_to_string('hashes.xml', d)
711 elif request.serialization == 'json':
714 response = HttpResponse(data, status=200)
715 put_object_headers(response, meta)
716 response['Content-Length'] = len(data)
719 request.serialization = 'text' # Unset.
720 return object_data_response(request, sizes, hashmaps, meta)
722 @api_method('PUT', format_allowed=True)
723 def object_write(request, v_account, v_container, v_object):
724 # Normal Response Codes: 201
725 # Error Response Codes: serviceUnavailable (503),
726 # unprocessableEntity (422),
727 # lengthRequired (411),
729 # itemNotFound (404),
730 # unauthorized (401),
733 # Evaluate conditions.
734 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
736 meta = request.backend.get_object_meta(request.user, v_account,
737 v_container, v_object)
738 except NotAllowedError:
739 raise Unauthorized('Access denied')
742 validate_matching_preconditions(request, meta)
744 copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
745 move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
746 if copy_from or move_from:
747 content_length = get_content_length(request) # Required by the API.
749 src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
751 src_account = request.user
754 src_container, src_name = split_container_object_string(move_from)
756 raise BadRequest('Invalid X-Move-From header')
757 version_id = copy_or_move_object(request, src_account, src_container, src_name,
758 v_account, v_container, v_object, move=True)
761 src_container, src_name = split_container_object_string(copy_from)
763 raise BadRequest('Invalid X-Copy-From header')
764 version_id = copy_or_move_object(request, src_account, src_container, src_name,
765 v_account, v_container, v_object, move=False)
766 response = HttpResponse(status=201)
767 response['X-Object-Version'] = version_id
770 meta, permissions, public = get_object_headers(request)
772 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
773 content_length = get_content_length(request)
774 # Should be BadRequest, but API says otherwise.
775 if 'Content-Type' not in meta:
776 raise LengthRequired('Missing Content-Type header')
778 if 'hashmap' in request.GET:
779 if request.serialization not in ('json', 'xml'):
780 raise BadRequest('Invalid hashmap format')
783 for block in socket_read_iterator(request, content_length,
784 request.backend.block_size):
785 data = '%s%s' % (data, block)
787 if request.serialization == 'json':
789 if not hasattr(d, '__getitem__'):
790 raise BadRequest('Invalid data formating')
792 hashmap = d['hashes']
793 size = int(d['bytes'])
795 raise BadRequest('Invalid data formatting')
796 elif request.serialization == 'xml':
798 xml = minidom.parseString(data)
799 obj = xml.getElementsByTagName('object')[0]
800 size = int(obj.attributes['bytes'].value)
802 hashes = xml.getElementsByTagName('hash')
805 hashmap.append(hash.firstChild.data)
807 raise BadRequest('Invalid data formatting')
809 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
814 for data in socket_read_iterator(request, content_length,
815 request.backend.block_size):
816 # TODO: Raise 408 (Request Timeout) if this takes too long.
817 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
819 hashmap.append(request.backend.put_block(data))
822 meta['hash'] = md5.hexdigest().lower()
823 etag = request.META.get('HTTP_ETAG')
824 if etag and parse_etags(etag)[0].lower() != meta['hash']:
825 raise UnprocessableEntity('Object ETag does not match')
828 version_id = request.backend.update_object_hashmap(request.user,
829 v_account, v_container, v_object, size, hashmap, meta,
831 except NotAllowedError:
832 raise Unauthorized('Access denied')
833 except IndexError, e:
834 raise Conflict('\n'.join(e.data) + '\n')
836 raise ItemNotFound('Container does not exist')
838 raise BadRequest('Invalid sharing header')
839 except AttributeError, e:
840 raise Conflict('\n'.join(e.data) + '\n')
842 raise RequestEntityTooLarge('Quota exceeded')
843 if public is not None:
845 request.backend.update_object_public(request.user, v_account,
846 v_container, v_object, public)
847 except NotAllowedError:
848 raise Unauthorized('Access denied')
850 raise ItemNotFound('Object does not exist')
852 response = HttpResponse(status=201)
853 response['ETag'] = meta['hash']
854 response['X-Object-Version'] = version_id
858 def object_write_form(request, v_account, v_container, v_object):
859 # Normal Response Codes: 201
860 # Error Response Codes: serviceUnavailable (503),
861 # itemNotFound (404),
862 # unauthorized (401),
865 if not request.FILES.has_key('X-Object-Data'):
866 raise BadRequest('Missing X-Object-Data field')
867 file = request.FILES['X-Object-Data']
870 meta['Content-Type'] = file.content_type
875 for data in file.chunks(request.backend.block_size):
877 hashmap.append(request.backend.put_block(data))
880 meta['hash'] = md5.hexdigest().lower()
883 version_id = request.backend.update_object_hashmap(request.user,
884 v_account, v_container, v_object, size, hashmap, meta, True)
885 except NotAllowedError:
886 raise Unauthorized('Access denied')
888 raise ItemNotFound('Container does not exist')
890 raise RequestEntityTooLarge('Quota exceeded')
892 response = HttpResponse(status=201)
893 response['ETag'] = meta['hash']
894 response['X-Object-Version'] = version_id
898 def object_copy(request, v_account, v_container, v_object):
899 # Normal Response Codes: 201
900 # Error Response Codes: serviceUnavailable (503),
901 # itemNotFound (404),
902 # unauthorized (401),
905 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
907 dest_account = request.user
908 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
910 raise BadRequest('Missing Destination header')
912 dest_container, dest_name = split_container_object_string(dest_path)
914 raise BadRequest('Invalid Destination header')
916 # Evaluate conditions.
917 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
918 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
920 meta = request.backend.get_object_meta(request.user, v_account,
921 v_container, v_object, src_version)
922 except NotAllowedError:
923 raise Unauthorized('Access denied')
924 except (NameError, IndexError):
925 raise ItemNotFound('Container or object does not exist')
926 validate_matching_preconditions(request, meta)
928 version_id = copy_or_move_object(request, v_account, v_container, v_object,
929 dest_account, dest_container, dest_name, move=False)
930 response = HttpResponse(status=201)
931 response['X-Object-Version'] = version_id
935 def object_move(request, v_account, v_container, v_object):
936 # Normal Response Codes: 201
937 # Error Response Codes: serviceUnavailable (503),
938 # itemNotFound (404),
939 # unauthorized (401),
942 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
944 dest_account = request.user
945 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
947 raise BadRequest('Missing Destination header')
949 dest_container, dest_name = split_container_object_string(dest_path)
951 raise BadRequest('Invalid Destination header')
953 # Evaluate conditions.
954 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
956 meta = request.backend.get_object_meta(request.user, v_account,
957 v_container, v_object)
958 except NotAllowedError:
959 raise Unauthorized('Access denied')
961 raise ItemNotFound('Container or object does not exist')
962 validate_matching_preconditions(request, meta)
964 version_id = copy_or_move_object(request, v_account, v_container, v_object,
965 dest_account, dest_container, dest_name, move=True)
966 response = HttpResponse(status=201)
967 response['X-Object-Version'] = version_id
971 def object_update(request, v_account, v_container, v_object):
972 # Normal Response Codes: 202, 204
973 # Error Response Codes: serviceUnavailable (503),
975 # itemNotFound (404),
976 # unauthorized (401),
978 meta, permissions, public = get_object_headers(request)
979 content_type = meta.get('Content-Type')
981 del(meta['Content-Type']) # Do not allow changing the Content-Type.
984 prev_meta = request.backend.get_object_meta(request.user, v_account,
985 v_container, v_object)
986 except NotAllowedError:
987 raise Unauthorized('Access denied')
989 raise ItemNotFound('Object does not exist')
991 # Evaluate conditions.
992 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
993 validate_matching_preconditions(request, prev_meta)
995 # If replacing, keep previous values of 'Content-Type' and 'hash'.
997 if 'update' in request.GET:
1000 for k in ('Content-Type', 'hash'):
1002 meta[k] = prev_meta[k]
1004 # A Content-Type or X-Source-Object header indicates data updates.
1005 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1006 if (not content_type or content_type != 'application/octet-stream') and not src_object:
1007 response = HttpResponse(status=202)
1009 # Do permissions first, as it may fail easier.
1010 if permissions is not None:
1012 request.backend.update_object_permissions(request.user,
1013 v_account, v_container, v_object, permissions)
1014 except NotAllowedError:
1015 raise Unauthorized('Access denied')
1017 raise ItemNotFound('Object does not exist')
1019 raise BadRequest('Invalid sharing header')
1020 except AttributeError, e:
1021 raise Conflict('\n'.join(e.data) + '\n')
1022 if public is not None:
1024 request.backend.update_object_public(request.user, v_account,
1025 v_container, v_object, public)
1026 except NotAllowedError:
1027 raise Unauthorized('Access denied')
1029 raise ItemNotFound('Object does not exist')
1032 version_id = request.backend.update_object_meta(request.user,
1033 v_account, v_container, v_object, meta, replace)
1034 except NotAllowedError:
1035 raise Unauthorized('Access denied')
1037 raise ItemNotFound('Object does not exist')
1038 response['X-Object-Version'] = version_id
1042 # Single range update. Range must be in Content-Range.
1043 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1044 # (with the addition that '*' is allowed for the range - will append).
1045 content_range = request.META.get('HTTP_CONTENT_RANGE')
1046 if not content_range:
1047 raise BadRequest('Missing Content-Range header')
1048 ranges = get_content_range(request)
1050 raise RangeNotSatisfiable('Invalid Content-Range header')
1053 size, hashmap = request.backend.get_object_hashmap(request.user,
1054 v_account, v_container, v_object)
1055 except NotAllowedError:
1056 raise Unauthorized('Access denied')
1058 raise ItemNotFound('Object does not exist')
1060 offset, length, total = ranges
1064 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1066 src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
1068 src_account = request.user
1069 src_container, src_name = split_container_object_string(src_object)
1070 src_container = smart_unicode(src_container, strings_only=True)
1071 src_name = smart_unicode(src_name, strings_only=True)
1072 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1074 src_size, src_hashmap = request.backend.get_object_hashmap(request.user,
1075 src_account, src_container, src_name, src_version)
1076 except NotAllowedError:
1077 raise Unauthorized('Access denied')
1079 raise ItemNotFound('Source object does not exist')
1083 elif length > src_size:
1084 raise BadRequest('Object length is smaller than range length')
1086 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1088 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1089 content_length = get_content_length(request)
1092 length = content_length
1094 if content_length == -1:
1095 # TODO: Get up to length bytes in chunks.
1096 length = content_length
1097 elif length != content_length:
1098 raise BadRequest('Content length does not match range length')
1099 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1100 raise RangeNotSatisfiable('Supplied range will change provided object limits')
1102 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1103 if dest_bytes is not None:
1104 dest_bytes = get_int_parameter(dest_bytes)
1105 if dest_bytes is None:
1106 raise BadRequest('Invalid X-Object-Bytes header')
1109 if offset % request.backend.block_size == 0:
1110 # Update the hashes only.
1113 bi = int(offset / request.backend.block_size)
1114 bl = min(length, request.backend.block_size)
1115 if bi < len(hashmap):
1116 if bl == request.backend.block_size:
1117 hashmap[bi] = src_hashmap[sbi]
1119 data = request.backend.get_block(src_hashmap[sbi])
1120 hashmap[bi] = request.backend.update_block(hashmap[bi],
1123 hashmap.append(src_hashmap[sbi])
1131 data += request.backend.get_block(src_hashmap[sbi])
1132 if length < request.backend.block_size:
1133 data = data[:length]
1134 bytes = put_object_block(request, hashmap, data, offset)
1141 for d in socket_read_iterator(request, length,
1142 request.backend.block_size):
1143 # TODO: Raise 408 (Request Timeout) if this takes too long.
1144 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1146 bytes = put_object_block(request, hashmap, data, offset)
1150 put_object_block(request, hashmap, data, offset)
1154 if dest_bytes is not None and dest_bytes < size:
1156 hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1157 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
1159 version_id = request.backend.update_object_hashmap(request.user,
1160 v_account, v_container, v_object, size, hashmap, meta,
1161 replace, permissions)
1162 except NotAllowedError:
1163 raise Unauthorized('Access denied')
1165 raise ItemNotFound('Container does not exist')
1167 raise BadRequest('Invalid sharing header')
1168 except AttributeError, e:
1169 raise Conflict('\n'.join(e.data) + '\n')
1171 raise RequestEntityTooLarge('Quota exceeded')
1172 if public is not None:
1174 request.backend.update_object_public(request.user, v_account,
1175 v_container, v_object, public)
1176 except NotAllowedError:
1177 raise Unauthorized('Access denied')
1179 raise ItemNotFound('Object does not exist')
1181 response = HttpResponse(status=204)
1182 response['ETag'] = meta['hash']
1183 response['X-Object-Version'] = version_id
1186 @api_method('DELETE')
1187 def object_delete(request, v_account, v_container, v_object):
1188 # Normal Response Codes: 204
1189 # Error Response Codes: serviceUnavailable (503),
1190 # itemNotFound (404),
1191 # unauthorized (401),
1194 until = get_int_parameter(request.GET.get('until'))
1196 request.backend.delete_object(request.user, v_account, v_container,
1198 except NotAllowedError:
1199 raise Unauthorized('Access denied')
1201 raise ItemNotFound('Object does not exist')
1202 return HttpResponse(status=204)
1205 def method_not_allowed(request):
1206 raise BadRequest('Method not allowed')