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, 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)
53 from pithos.backends import connect_backend
54 from pithos.backends.base import NotAllowedError
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 except NotAllowedError:
190 raise Unauthorized('Access denied')
192 validate_modification_preconditions(request, meta)
194 response = HttpResponse(status=204)
195 put_account_headers(response, meta, groups)
199 def account_update(request, v_account):
200 # Normal Response Codes: 202
201 # Error Response Codes: serviceUnavailable (503),
202 # unauthorized (401),
205 meta, groups = get_account_headers(request)
207 if 'update' in request.GET:
211 request.backend.update_account_groups(request.user, v_account,
213 except NotAllowedError:
214 raise Unauthorized('Access denied')
216 raise BadRequest('Invalid groups header')
219 request.backend.update_account_meta(request.user, v_account, meta,
221 except NotAllowedError:
222 raise Unauthorized('Access denied')
223 return HttpResponse(status=202)
225 @api_method('GET', format_allowed=True)
226 def container_list(request, v_account):
227 # Normal Response Codes: 200, 204
228 # Error Response Codes: serviceUnavailable (503),
229 # itemNotFound (404),
230 # unauthorized (401),
233 until = get_int_parameter(request.GET.get('until'))
235 meta = request.backend.get_account_meta(request.user, v_account, until)
236 groups = request.backend.get_account_groups(request.user, v_account)
237 except NotAllowedError:
238 raise Unauthorized('Access denied')
240 validate_modification_preconditions(request, meta)
242 response = HttpResponse()
243 put_account_headers(response, meta, groups)
245 marker = request.GET.get('marker')
246 limit = get_int_parameter(request.GET.get('limit'))
251 if 'shared' in request.GET:
255 containers = request.backend.list_containers(request.user, v_account,
256 marker, limit, shared, until)
257 except NotAllowedError:
258 raise Unauthorized('Access denied')
262 if request.serialization == 'text':
263 if len(containers) == 0:
264 # The cloudfiles python bindings expect 200 if json/xml.
265 response.status_code = 204
267 response.status_code = 200
268 response.content = '\n'.join(containers) + '\n'
274 meta = request.backend.get_container_meta(request.user, v_account,
276 policy = request.backend.get_container_policy(request.user,
278 except NotAllowedError:
279 raise Unauthorized('Access denied')
283 rename_meta_key(meta, 'modified', 'last_modified')
284 rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
285 for k, v in policy.iteritems():
286 meta['X-Container-Policy-' + k] = v
287 container_meta.append(printable_header_dict(meta))
288 if request.serialization == 'xml':
289 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
290 elif request.serialization == 'json':
291 data = json.dumps(container_meta)
292 response.status_code = 200
293 response.content = data
297 def container_meta(request, v_account, v_container):
298 # Normal Response Codes: 204
299 # Error Response Codes: serviceUnavailable (503),
300 # itemNotFound (404),
301 # unauthorized (401),
304 until = get_int_parameter(request.GET.get('until'))
306 meta = request.backend.get_container_meta(request.user, v_account,
308 meta['object_meta'] = request.backend.list_object_meta(request.user,
309 v_account, v_container, until)
310 policy = request.backend.get_container_policy(request.user, v_account,
312 except NotAllowedError:
313 raise Unauthorized('Access denied')
315 raise ItemNotFound('Container does not exist')
317 validate_modification_preconditions(request, meta)
319 response = HttpResponse(status=204)
320 put_container_headers(request, response, meta, policy)
324 def container_create(request, v_account, v_container):
325 # Normal Response Codes: 201, 202
326 # Error Response Codes: serviceUnavailable (503),
327 # itemNotFound (404),
328 # unauthorized (401),
331 meta, policy = get_container_headers(request)
334 request.backend.put_container(request.user, v_account, v_container,
337 except NotAllowedError:
338 raise Unauthorized('Access denied')
340 raise BadRequest('Invalid policy header')
344 if ret == 202 and policy:
346 request.backend.update_container_policy(request.user, v_account,
347 v_container, policy, replace=False)
348 except NotAllowedError:
349 raise Unauthorized('Access denied')
351 raise ItemNotFound('Container does not exist')
353 raise BadRequest('Invalid policy header')
356 request.backend.update_container_meta(request.user, v_account,
357 v_container, meta, replace=False)
358 except NotAllowedError:
359 raise Unauthorized('Access denied')
361 raise ItemNotFound('Container does not exist')
363 return HttpResponse(status=ret)
366 def container_update(request, v_account, v_container):
367 # Normal Response Codes: 202
368 # Error Response Codes: serviceUnavailable (503),
369 # itemNotFound (404),
370 # unauthorized (401),
373 meta, policy = get_container_headers(request)
375 if 'update' in request.GET:
379 request.backend.update_container_policy(request.user, v_account,
380 v_container, policy, replace)
381 except NotAllowedError:
382 raise Unauthorized('Access denied')
384 raise ItemNotFound('Container does not exist')
386 raise BadRequest('Invalid policy header')
389 request.backend.update_container_meta(request.user, v_account,
390 v_container, meta, replace)
391 except NotAllowedError:
392 raise Unauthorized('Access denied')
394 raise ItemNotFound('Container does not exist')
395 return HttpResponse(status=202)
397 @api_method('DELETE')
398 def container_delete(request, v_account, v_container):
399 # Normal Response Codes: 204
400 # Error Response Codes: serviceUnavailable (503),
402 # itemNotFound (404),
403 # unauthorized (401),
406 until = get_int_parameter(request.GET.get('until'))
408 request.backend.delete_container(request.user, v_account, v_container,
410 except NotAllowedError:
411 raise Unauthorized('Access denied')
413 raise ItemNotFound('Container does not exist')
415 raise Conflict('Container is not empty')
416 return HttpResponse(status=204)
418 @api_method('GET', format_allowed=True)
419 def object_list(request, v_account, v_container):
420 # Normal Response Codes: 200, 204
421 # Error Response Codes: serviceUnavailable (503),
422 # itemNotFound (404),
423 # unauthorized (401),
426 until = get_int_parameter(request.GET.get('until'))
428 meta = request.backend.get_container_meta(request.user, v_account,
430 meta['object_meta'] = request.backend.list_object_meta(request.user,
431 v_account, v_container, until)
432 policy = request.backend.get_container_policy(request.user, v_account,
434 except NotAllowedError:
435 raise Unauthorized('Access denied')
437 raise ItemNotFound('Container does not exist')
439 validate_modification_preconditions(request, meta)
441 response = HttpResponse()
442 put_container_headers(request, response, meta, policy)
444 path = request.GET.get('path')
445 prefix = request.GET.get('prefix')
446 delimiter = request.GET.get('delimiter')
448 # Path overrides prefix and delimiter.
456 if prefix and delimiter:
457 prefix = prefix + delimiter
460 prefix = prefix.lstrip('/')
462 marker = request.GET.get('marker')
463 limit = get_int_parameter(request.GET.get('limit'))
467 keys = request.GET.get('meta')
469 keys = keys.split(',')
470 l = [smart_str(x) for x in keys if x.strip() != '']
471 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
476 if 'shared' in request.GET:
480 objects = request.backend.list_objects(request.user, v_account,
481 v_container, prefix, delimiter, marker,
482 limit, virtual, keys, shared, until)
483 except NotAllowedError:
484 raise Unauthorized('Access denied')
486 raise ItemNotFound('Container does not exist')
488 if request.serialization == 'text':
489 if len(objects) == 0:
490 # The cloudfiles python bindings expect 200 if json/xml.
491 response.status_code = 204
493 response.status_code = 200
494 response.content = '\n'.join([x[0] for x in objects]) + '\n'
500 # Virtual objects/directories.
501 object_meta.append({'subdir': x[0]})
504 meta = request.backend.get_object_meta(request.user, v_account,
505 v_container, x[0], x[1])
507 permissions = request.backend.get_object_permissions(
508 request.user, v_account, v_container, x[0])
509 public = request.backend.get_object_public(request.user,
510 v_account, v_container, x[0])
514 except NotAllowedError:
515 raise Unauthorized('Access denied')
519 rename_meta_key(meta, 'modified', 'last_modified')
520 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
521 rename_meta_key(meta, 'version', 'x_object_version')
522 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
523 update_sharing_meta(request, permissions, v_account, v_container, x[0], meta)
524 update_public_meta(public, meta)
525 object_meta.append(printable_header_dict(meta))
526 if request.serialization == 'xml':
527 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
528 elif request.serialization == 'json':
529 data = json.dumps(object_meta)
530 response.status_code = 200
531 response.content = data
535 def object_meta(request, v_account, v_container, v_object):
536 # Normal Response Codes: 204
537 # Error Response Codes: serviceUnavailable (503),
538 # itemNotFound (404),
539 # unauthorized (401),
542 version = request.GET.get('version')
544 meta = request.backend.get_object_meta(request.user, v_account,
545 v_container, v_object, version)
547 permissions = request.backend.get_object_permissions(request.user,
548 v_account, v_container, v_object)
549 public = request.backend.get_object_public(request.user, v_account,
550 v_container, v_object)
554 except NotAllowedError:
555 raise Unauthorized('Access denied')
557 raise ItemNotFound('Object does not exist')
559 raise ItemNotFound('Version does not exist')
561 update_manifest_meta(request, v_account, meta)
562 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
563 update_public_meta(public, meta)
565 # Evaluate conditions.
566 validate_modification_preconditions(request, meta)
568 validate_matching_preconditions(request, meta)
570 response = HttpResponse(status=304)
571 response['ETag'] = meta['hash']
574 response = HttpResponse(status=200)
575 put_object_headers(response, meta)
578 @api_method('GET', format_allowed=True)
579 def object_read(request, v_account, v_container, v_object):
580 # Normal Response Codes: 200, 206
581 # Error Response Codes: serviceUnavailable (503),
582 # rangeNotSatisfiable (416),
583 # preconditionFailed (412),
584 # itemNotFound (404),
585 # unauthorized (401),
589 version = request.GET.get('version')
591 # Reply with the version list. Do this first, as the object may be deleted.
592 if version == 'list':
593 if request.serialization == 'text':
594 raise BadRequest('No format specified for version list.')
597 v = request.backend.list_versions(request.user, v_account,
598 v_container, v_object)
599 except NotAllowedError:
600 raise Unauthorized('Access denied')
602 if request.serialization == 'xml':
603 d['object'] = v_object
604 data = render_to_string('versions.xml', d)
605 elif request.serialization == 'json':
608 response = HttpResponse(data, status=200)
609 response['Content-Length'] = len(data)
613 meta = request.backend.get_object_meta(request.user, v_account,
614 v_container, v_object, version)
616 permissions = request.backend.get_object_permissions(request.user,
617 v_account, v_container, v_object)
618 public = request.backend.get_object_public(request.user, v_account,
619 v_container, v_object)
623 except NotAllowedError:
624 raise Unauthorized('Access denied')
626 raise ItemNotFound('Object does not exist')
628 raise ItemNotFound('Version does not exist')
630 update_manifest_meta(request, v_account, meta)
631 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
632 update_public_meta(public, meta)
634 # Evaluate conditions.
635 validate_modification_preconditions(request, meta)
637 validate_matching_preconditions(request, meta)
639 response = HttpResponse(status=304)
640 response['ETag'] = meta['hash']
645 if 'X-Object-Manifest' in meta:
647 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
648 objects = request.backend.list_objects(request.user, v_account,
649 src_container, prefix=src_name, virtual=False)
650 except NotAllowedError:
651 raise Unauthorized('Access denied')
653 raise BadRequest('Invalid X-Object-Manifest header')
655 raise ItemNotFound('Container does not exist')
659 s, h = request.backend.get_object_hashmap(request.user,
660 v_account, src_container, x[0], x[1])
663 except NotAllowedError:
664 raise Unauthorized('Access denied')
666 raise ItemNotFound('Object does not exist')
668 raise ItemNotFound('Version does not exist')
671 s, h = request.backend.get_object_hashmap(request.user, v_account,
672 v_container, v_object, version)
675 except NotAllowedError:
676 raise Unauthorized('Access denied')
678 raise ItemNotFound('Object does not exist')
680 raise ItemNotFound('Version does not exist')
682 # Reply with the hashmap.
683 if 'hashmap' in request.GET and request.serialization != 'text':
685 hashmap = sum(hashmaps, [])
687 'block_size': request.backend.block_size,
688 'block_hash': request.backend.hash_algorithm,
691 if request.serialization == 'xml':
692 d['object'] = v_object
693 data = render_to_string('hashes.xml', d)
694 elif request.serialization == 'json':
697 response = HttpResponse(data, status=200)
698 put_object_headers(response, meta)
699 response['Content-Length'] = len(data)
702 request.serialization = 'text' # Unset.
703 return object_data_response(request, sizes, hashmaps, meta)
705 @api_method('PUT', format_allowed=True)
706 def object_write(request, v_account, v_container, v_object):
707 # Normal Response Codes: 201
708 # Error Response Codes: serviceUnavailable (503),
709 # unprocessableEntity (422),
710 # lengthRequired (411),
712 # itemNotFound (404),
713 # unauthorized (401),
716 # Evaluate conditions.
717 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
719 meta = request.backend.get_object_meta(request.user, v_account,
720 v_container, v_object)
721 except NotAllowedError:
722 raise Unauthorized('Access denied')
725 validate_matching_preconditions(request, meta)
727 copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
728 move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
729 if copy_from or move_from:
730 content_length = get_content_length(request) # Required by the API.
732 src_account = smart_unicode(request.META.get('HTTP_X_SOURCE_ACCOUNT'), strings_only=True)
734 src_account = request.user
737 src_container, src_name = split_container_object_string(move_from)
739 raise BadRequest('Invalid X-Move-From header')
740 version_id = copy_or_move_object(request, src_account, src_container, src_name,
741 v_account, v_container, v_object, move=True)
744 src_container, src_name = split_container_object_string(copy_from)
746 raise BadRequest('Invalid X-Copy-From header')
747 version_id = copy_or_move_object(request, src_account, src_container, src_name,
748 v_account, v_container, v_object, move=False)
749 response = HttpResponse(status=201)
750 response['X-Object-Version'] = version_id
753 meta, permissions, public = get_object_headers(request)
755 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
756 content_length = get_content_length(request)
757 # Should be BadRequest, but API says otherwise.
758 if 'Content-Type' not in meta:
759 raise LengthRequired('Missing Content-Type header')
761 if 'hashmap' in request.GET:
762 if request.serialization not in ('json', 'xml'):
763 raise BadRequest('Invalid hashmap format')
766 for block in socket_read_iterator(request, content_length,
767 request.backend.block_size):
768 data = '%s%s' % (data, block)
770 if request.serialization == 'json':
772 if not hasattr(d, '__getitem__'):
773 raise BadRequest('Invalid data formating')
775 hashmap = d['hashes']
776 size = int(d['bytes'])
778 raise BadRequest('Invalid data formatting')
779 elif request.serialization == 'xml':
781 xml = minidom.parseString(data)
782 obj = xml.getElementsByTagName('object')[0]
783 size = int(obj.attributes['bytes'].value)
785 hashes = xml.getElementsByTagName('hash')
788 hashmap.append(hash.firstChild.data)
790 raise BadRequest('Invalid data formatting')
792 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
797 for data in socket_read_iterator(request, content_length,
798 request.backend.block_size):
799 # TODO: Raise 408 (Request Timeout) if this takes too long.
800 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
802 hashmap.append(request.backend.put_block(data))
805 meta['hash'] = md5.hexdigest().lower()
806 etag = request.META.get('HTTP_ETAG')
807 if etag and parse_etags(etag)[0].lower() != meta['hash']:
808 raise UnprocessableEntity('Object ETag does not match')
811 version_id = request.backend.update_object_hashmap(request.user,
812 v_account, v_container, v_object, size, hashmap, meta,
814 except NotAllowedError:
815 raise Unauthorized('Access denied')
816 except IndexError, e:
817 raise Conflict('\n'.join(e.data) + '\n')
819 raise ItemNotFound('Container does not exist')
821 raise BadRequest('Invalid sharing header')
822 except AttributeError, e:
823 raise Conflict('\n'.join(e.data) + '\n')
824 if public is not None:
826 request.backend.update_object_public(request.user, v_account,
827 v_container, v_object, public)
828 except NotAllowedError:
829 raise Unauthorized('Access denied')
831 raise ItemNotFound('Object does not exist')
833 response = HttpResponse(status=201)
834 response['ETag'] = meta['hash']
835 response['X-Object-Version'] = version_id
839 def object_write_form(request, v_account, v_container, v_object):
840 # Normal Response Codes: 201
841 # Error Response Codes: serviceUnavailable (503),
842 # itemNotFound (404),
843 # unauthorized (401),
846 if not request.FILES.has_key('X-Object-Data'):
847 raise BadRequest('Missing X-Object-Data field')
848 file = request.FILES['X-Object-Data']
851 meta['Content-Type'] = file.content_type
856 for data in file.chunks(request.backend.block_size):
858 hashmap.append(request.backend.put_block(data))
861 meta['hash'] = md5.hexdigest().lower()
864 version_id = request.backend.update_object_hashmap(request.user,
865 v_account, v_container, v_object, size, hashmap, meta, True)
866 except NotAllowedError:
867 raise Unauthorized('Access denied')
869 raise ItemNotFound('Container does not exist')
871 response = HttpResponse(status=201)
872 response['ETag'] = meta['hash']
873 response['X-Object-Version'] = version_id
877 def object_copy(request, v_account, v_container, v_object):
878 # Normal Response Codes: 201
879 # Error Response Codes: serviceUnavailable (503),
880 # itemNotFound (404),
881 # unauthorized (401),
884 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
886 dest_account = request.user
887 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
889 raise BadRequest('Missing Destination header')
891 dest_container, dest_name = split_container_object_string(dest_path)
893 raise BadRequest('Invalid Destination header')
895 # Evaluate conditions.
896 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
897 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
899 meta = request.backend.get_object_meta(request.user, v_account,
900 v_container, v_object, src_version)
901 except NotAllowedError:
902 raise Unauthorized('Access denied')
903 except (NameError, IndexError):
904 raise ItemNotFound('Container or object does not exist')
905 validate_matching_preconditions(request, meta)
907 version_id = copy_or_move_object(request, v_account, v_container, v_object,
908 dest_account, dest_container, dest_name, move=False)
909 response = HttpResponse(status=201)
910 response['X-Object-Version'] = version_id
914 def object_move(request, v_account, v_container, v_object):
915 # Normal Response Codes: 201
916 # Error Response Codes: serviceUnavailable (503),
917 # itemNotFound (404),
918 # unauthorized (401),
921 dest_account = smart_unicode(request.META.get('HTTP_DESTINATION_ACCOUNT'), strings_only=True)
923 dest_account = request.user
924 dest_path = smart_unicode(request.META.get('HTTP_DESTINATION'), strings_only=True)
926 raise BadRequest('Missing Destination header')
928 dest_container, dest_name = split_container_object_string(dest_path)
930 raise BadRequest('Invalid Destination header')
932 # Evaluate conditions.
933 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
935 meta = request.backend.get_object_meta(request.user, v_account,
936 v_container, v_object)
937 except NotAllowedError:
938 raise Unauthorized('Access denied')
940 raise ItemNotFound('Container or object does not exist')
941 validate_matching_preconditions(request, meta)
943 version_id = copy_or_move_object(request, v_account, v_container, v_object,
944 dest_account, dest_container, dest_name, move=True)
945 response = HttpResponse(status=201)
946 response['X-Object-Version'] = version_id
950 def object_update(request, v_account, v_container, v_object):
951 # Normal Response Codes: 202, 204
952 # Error Response Codes: serviceUnavailable (503),
954 # itemNotFound (404),
955 # unauthorized (401),
957 meta, permissions, public = get_object_headers(request)
958 content_type = meta.get('Content-Type')
960 del(meta['Content-Type']) # Do not allow changing the Content-Type.
963 prev_meta = request.backend.get_object_meta(request.user, v_account,
964 v_container, v_object)
965 except NotAllowedError:
966 raise Unauthorized('Access denied')
968 raise ItemNotFound('Object does not exist')
970 # Evaluate conditions.
971 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
972 validate_matching_preconditions(request, prev_meta)
974 # If replacing, keep previous values of 'Content-Type' and 'hash'.
976 if 'update' in request.GET:
979 for k in ('Content-Type', 'hash'):
981 meta[k] = prev_meta[k]
983 # A Content-Type or X-Source-Object header indicates data updates.
984 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
985 if (not content_type or content_type != 'application/octet-stream') and not src_object:
986 response = HttpResponse(status=202)
988 # Do permissions first, as it may fail easier.
989 if permissions is not None:
991 request.backend.update_object_permissions(request.user,
992 v_account, v_container, v_object, permissions)
993 except NotAllowedError:
994 raise Unauthorized('Access denied')
996 raise ItemNotFound('Object does not exist')
998 raise BadRequest('Invalid sharing header')
999 except AttributeError, e:
1000 raise Conflict('\n'.join(e.data) + '\n')
1001 if public is not None:
1003 request.backend.update_object_public(request.user, v_account,
1004 v_container, v_object, public)
1005 except NotAllowedError:
1006 raise Unauthorized('Access denied')
1008 raise ItemNotFound('Object does not exist')
1011 version_id = request.backend.update_object_meta(request.user,
1012 v_account, v_container, v_object, meta, replace)
1013 except NotAllowedError:
1014 raise Unauthorized('Access denied')
1016 raise ItemNotFound('Object does not exist')
1017 response['X-Object-Version'] = version_id
1021 # Single range update. Range must be in Content-Range.
1022 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1023 # (with the addition that '*' is allowed for the range - will append).
1024 content_range = request.META.get('HTTP_CONTENT_RANGE')
1025 if not content_range:
1026 raise BadRequest('Missing Content-Range header')
1027 ranges = get_content_range(request)
1029 raise RangeNotSatisfiable('Invalid Content-Range header')
1032 size, hashmap = request.backend.get_object_hashmap(request.user,
1033 v_account, v_container, v_object)
1034 except NotAllowedError:
1035 raise Unauthorized('Access denied')
1037 raise ItemNotFound('Object does not exist')
1039 offset, length, total = ranges
1043 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1045 src_container, src_name = split_container_object_string(src_object)
1046 src_container = smart_unicode(src_container, strings_only=True)
1047 src_name = smart_unicode(src_name, strings_only=True)
1048 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1050 src_size, src_hashmap = request.backend.get_object_hashmap(
1051 request.user, v_account, src_container, src_name, src_version)
1052 except NotAllowedError:
1053 raise Unauthorized('Access denied')
1055 raise ItemNotFound('Source object does not exist')
1059 elif length > src_size:
1060 raise BadRequest('Object length is smaller than range length')
1062 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1064 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1065 content_length = get_content_length(request)
1068 length = content_length
1070 if content_length == -1:
1071 # TODO: Get up to length bytes in chunks.
1072 length = content_length
1073 elif length != content_length:
1074 raise BadRequest('Content length does not match range length')
1075 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1076 raise RangeNotSatisfiable('Supplied range will change provided object limits')
1078 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1079 if dest_bytes is not None:
1080 dest_bytes = get_int_parameter(dest_bytes)
1081 if dest_bytes is None:
1082 raise BadRequest('Invalid X-Object-Bytes header')
1085 if offset % request.backend.block_size == 0:
1086 # Update the hashes only.
1089 bi = int(offset / request.backend.block_size)
1090 bl = min(length, request.backend.block_size)
1091 if bi < len(hashmap):
1092 if bl == request.backend.block_size:
1093 hashmap[bi] = src_hashmap[sbi]
1095 data = request.backend.get_block(src_hashmap[sbi])
1096 hashmap[bi] = request.backend.update_block(hashmap[bi],
1099 hashmap.append(src_hashmap[sbi])
1107 data += request.backend.get_block(src_hashmap[sbi])
1108 if length < request.backend.block_size:
1109 data = data[:length]
1110 bytes = put_object_block(request, hashmap, data, offset)
1117 for d in socket_read_iterator(request, length,
1118 request.backend.block_size):
1119 # TODO: Raise 408 (Request Timeout) if this takes too long.
1120 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1122 bytes = put_object_block(request, hashmap, data, offset)
1126 put_object_block(request, hashmap, data, offset)
1130 if dest_bytes is not None and dest_bytes < size:
1132 hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1133 meta.update({'hash': hashmap_hash(request, hashmap)}) # Update ETag.
1135 version_id = request.backend.update_object_hashmap(request.user,
1136 v_account, v_container, v_object, size, hashmap, meta,
1137 replace, permissions)
1138 except NotAllowedError:
1139 raise Unauthorized('Access denied')
1141 raise ItemNotFound('Container does not exist')
1143 raise BadRequest('Invalid sharing header')
1144 except AttributeError, e:
1145 raise Conflict('\n'.join(e.data) + '\n')
1146 if public is not None:
1148 request.backend.update_object_public(request.user, v_account,
1149 v_container, v_object, public)
1150 except NotAllowedError:
1151 raise Unauthorized('Access denied')
1153 raise ItemNotFound('Object does not exist')
1155 response = HttpResponse(status=204)
1156 response['ETag'] = meta['hash']
1157 response['X-Object-Version'] = version_id
1160 @api_method('DELETE')
1161 def object_delete(request, v_account, v_container, v_object):
1162 # Normal Response Codes: 204
1163 # Error Response Codes: serviceUnavailable (503),
1164 # itemNotFound (404),
1165 # unauthorized (401),
1168 until = get_int_parameter(request.GET.get('until'))
1170 request.backend.delete_object(request.user, v_account, v_container,
1172 except NotAllowedError:
1173 raise Unauthorized('Access denied')
1175 raise ItemNotFound('Object does not exist')
1176 return HttpResponse(status=204)
1179 def method_not_allowed(request):
1180 raise BadRequest('Method not allowed')