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 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 inv_auth_tokens = dict((v, k) for k, v in settings.AUTH_TOKENS.items())
126 uri = request.build_absolute_uri()
128 uri = uri[:uri.find('?')]
130 response['X-Auth-Token'] = inv_auth_tokens.get(x_auth_user, '0000')
131 response['X-Storage-Url'] = uri + (uri.endswith('/') and '' or '/') + x_auth_user
134 @api_method('GET', format_allowed=True)
135 def account_list(request):
136 # Normal Response Codes: 200, 204
137 # Error Response Codes: serviceUnavailable (503),
140 response = HttpResponse()
142 marker = request.GET.get('marker')
143 limit = get_int_parameter(request.GET.get('limit'))
147 accounts = backend.list_accounts(request.user, marker, limit)
149 if request.serialization == 'text':
150 if len(accounts) == 0:
151 # The cloudfiles python bindings expect 200 if json/xml.
152 response.status_code = 204
154 response.status_code = 200
155 response.content = '\n'.join(accounts) + '\n'
161 meta = backend.get_account_meta(request.user, x)
162 groups = backend.get_account_groups(request.user, x)
163 except NotAllowedError:
164 raise Unauthorized('Access denied')
166 rename_meta_key(meta, 'modified', 'last_modified')
167 rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
168 for k, v in groups.iteritems():
169 meta['X-Container-Group-' + k] = ','.join(v)
170 account_meta.append(printable_header_dict(meta))
171 if request.serialization == 'xml':
172 data = render_to_string('accounts.xml', {'accounts': account_meta})
173 elif request.serialization == 'json':
174 data = json.dumps(account_meta)
175 response.status_code = 200
176 response.content = data
180 def account_meta(request, v_account):
181 # Normal Response Codes: 204
182 # Error Response Codes: serviceUnavailable (503),
183 # unauthorized (401),
186 until = get_int_parameter(request.GET.get('until'))
188 meta = backend.get_account_meta(request.user, v_account, until)
189 groups = backend.get_account_groups(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)
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 backend.update_account_groups(request.user, v_account, groups, replace)
213 except NotAllowedError:
214 raise Unauthorized('Access denied')
216 raise BadRequest('Invalid groups header')
219 backend.update_account_meta(request.user, v_account, meta, replace)
220 except NotAllowedError:
221 raise Unauthorized('Access denied')
222 return HttpResponse(status=202)
224 @api_method('GET', format_allowed=True)
225 def container_list(request, v_account):
226 # Normal Response Codes: 200, 204
227 # Error Response Codes: serviceUnavailable (503),
228 # itemNotFound (404),
229 # unauthorized (401),
232 until = get_int_parameter(request.GET.get('until'))
234 meta = backend.get_account_meta(request.user, v_account, until)
235 groups = backend.get_account_groups(request.user, v_account)
236 except NotAllowedError:
237 raise Unauthorized('Access denied')
239 validate_modification_preconditions(request, meta)
241 response = HttpResponse()
242 put_account_headers(response, meta, groups)
244 marker = request.GET.get('marker')
245 limit = get_int_parameter(request.GET.get('limit'))
250 if 'shared' in request.GET:
254 containers = backend.list_containers(request.user, v_account, marker, limit, shared, until)
255 except NotAllowedError:
256 raise Unauthorized('Access denied')
260 if request.serialization == 'text':
261 if len(containers) == 0:
262 # The cloudfiles python bindings expect 200 if json/xml.
263 response.status_code = 204
265 response.status_code = 200
266 response.content = '\n'.join(containers) + '\n'
272 meta = backend.get_container_meta(request.user, v_account, x, until)
273 policy = backend.get_container_policy(request.user, v_account, x)
274 except NotAllowedError:
275 raise Unauthorized('Access denied')
279 rename_meta_key(meta, 'modified', 'last_modified')
280 rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
281 for k, v in policy.iteritems():
282 meta['X-Container-Policy-' + k] = v
283 container_meta.append(printable_header_dict(meta))
284 if request.serialization == 'xml':
285 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
286 elif request.serialization == 'json':
287 data = json.dumps(container_meta)
288 response.status_code = 200
289 response.content = data
293 def container_meta(request, v_account, v_container):
294 # Normal Response Codes: 204
295 # Error Response Codes: serviceUnavailable (503),
296 # itemNotFound (404),
297 # unauthorized (401),
300 until = get_int_parameter(request.GET.get('until'))
302 meta = backend.get_container_meta(request.user, v_account, v_container, until)
303 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
304 policy = backend.get_container_policy(request.user, v_account, v_container)
305 except NotAllowedError:
306 raise Unauthorized('Access denied')
308 raise ItemNotFound('Container does not exist')
310 validate_modification_preconditions(request, meta)
312 response = HttpResponse(status=204)
313 put_container_headers(response, meta, policy)
317 def container_create(request, v_account, v_container):
318 # Normal Response Codes: 201, 202
319 # Error Response Codes: serviceUnavailable (503),
320 # itemNotFound (404),
321 # unauthorized (401),
324 meta, policy = get_container_headers(request)
327 backend.put_container(request.user, v_account, v_container, policy)
329 except NotAllowedError:
330 raise Unauthorized('Access denied')
332 raise BadRequest('Invalid policy header')
338 backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
339 except NotAllowedError:
340 raise Unauthorized('Access denied')
342 raise ItemNotFound('Container does not exist')
344 return HttpResponse(status=ret)
347 def container_update(request, v_account, v_container):
348 # Normal Response Codes: 202
349 # Error Response Codes: serviceUnavailable (503),
350 # itemNotFound (404),
351 # unauthorized (401),
354 meta, policy = get_container_headers(request)
356 if 'update' in request.GET:
360 backend.update_container_policy(request.user, v_account, v_container, policy, replace)
361 except NotAllowedError:
362 raise Unauthorized('Access denied')
364 raise ItemNotFound('Container does not exist')
366 raise BadRequest('Invalid policy header')
369 backend.update_container_meta(request.user, v_account, v_container, meta, replace)
370 except NotAllowedError:
371 raise Unauthorized('Access denied')
373 raise ItemNotFound('Container does not exist')
374 return HttpResponse(status=202)
376 @api_method('DELETE')
377 def container_delete(request, v_account, v_container):
378 # Normal Response Codes: 204
379 # Error Response Codes: serviceUnavailable (503),
381 # itemNotFound (404),
382 # unauthorized (401),
385 until = get_int_parameter(request.GET.get('until'))
387 backend.delete_container(request.user, v_account, v_container, until)
388 except NotAllowedError:
389 raise Unauthorized('Access denied')
391 raise ItemNotFound('Container does not exist')
393 raise Conflict('Container is not empty')
394 return HttpResponse(status=204)
396 @api_method('GET', format_allowed=True)
397 def object_list(request, v_account, v_container):
398 # Normal Response Codes: 200, 204
399 # Error Response Codes: serviceUnavailable (503),
400 # itemNotFound (404),
401 # unauthorized (401),
404 until = get_int_parameter(request.GET.get('until'))
406 meta = backend.get_container_meta(request.user, v_account, v_container, until)
407 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
408 policy = backend.get_container_policy(request.user, v_account, v_container)
409 except NotAllowedError:
410 raise Unauthorized('Access denied')
412 raise ItemNotFound('Container does not exist')
414 validate_modification_preconditions(request, meta)
416 response = HttpResponse()
417 put_container_headers(response, meta, policy)
419 path = request.GET.get('path')
420 prefix = request.GET.get('prefix')
421 delimiter = request.GET.get('delimiter')
423 # Path overrides prefix and delimiter.
431 if prefix and delimiter:
432 prefix = prefix + delimiter
435 prefix = prefix.lstrip('/')
437 marker = request.GET.get('marker')
438 limit = get_int_parameter(request.GET.get('limit'))
442 keys = request.GET.get('meta')
444 keys = keys.split(',')
445 l = [smart_str(x) for x in keys if x.strip() != '']
446 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
451 if 'shared' in request.GET:
455 objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
456 except NotAllowedError:
457 raise Unauthorized('Access denied')
459 raise ItemNotFound('Container does not exist')
461 if request.serialization == 'text':
462 if len(objects) == 0:
463 # The cloudfiles python bindings expect 200 if json/xml.
464 response.status_code = 204
466 response.status_code = 200
467 response.content = '\n'.join([x[0] for x in objects]) + '\n'
473 # Virtual objects/directories.
474 object_meta.append({'subdir': x[0]})
477 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
479 permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
480 public = backend.get_object_public(request.user, v_account, v_container, x[0])
484 except NotAllowedError:
485 raise Unauthorized('Access denied')
489 rename_meta_key(meta, 'modified', 'last_modified')
490 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
491 rename_meta_key(meta, 'version', 'x_object_version')
492 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
493 update_sharing_meta(permissions, v_account, v_container, x[0], meta)
494 update_public_meta(public, meta)
495 object_meta.append(printable_header_dict(meta))
496 if request.serialization == 'xml':
497 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
498 elif request.serialization == 'json':
499 data = json.dumps(object_meta)
500 response.status_code = 200
501 response.content = data
505 def object_meta(request, v_account, v_container, v_object):
506 # Normal Response Codes: 204
507 # Error Response Codes: serviceUnavailable (503),
508 # itemNotFound (404),
509 # unauthorized (401),
512 version = request.GET.get('version')
514 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
516 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
517 public = backend.get_object_public(request.user, v_account, v_container, v_object)
521 except NotAllowedError:
522 raise Unauthorized('Access denied')
524 raise ItemNotFound('Object does not exist')
526 raise ItemNotFound('Version does not exist')
528 update_manifest_meta(request, v_account, meta)
529 update_sharing_meta(permissions, v_account, v_container, v_object, meta)
530 update_public_meta(public, meta)
532 # Evaluate conditions.
533 validate_modification_preconditions(request, meta)
535 validate_matching_preconditions(request, meta)
537 response = HttpResponse(status=304)
538 response['ETag'] = meta['hash']
541 response = HttpResponse(status=200)
542 put_object_headers(response, meta)
545 @api_method('GET', format_allowed=True)
546 def object_read(request, v_account, v_container, v_object):
547 # Normal Response Codes: 200, 206
548 # Error Response Codes: serviceUnavailable (503),
549 # rangeNotSatisfiable (416),
550 # preconditionFailed (412),
551 # itemNotFound (404),
552 # unauthorized (401),
556 version = request.GET.get('version')
558 # Reply with the version list. Do this first, as the object may be deleted.
559 if version == 'list':
560 if request.serialization == 'text':
561 raise BadRequest('No format specified for version list.')
564 v = backend.list_versions(request.user, v_account, v_container, v_object)
565 except NotAllowedError:
566 raise Unauthorized('Access denied')
568 if request.serialization == 'xml':
569 d['object'] = v_object
570 data = render_to_string('versions.xml', d)
571 elif request.serialization == 'json':
574 response = HttpResponse(data, status=200)
575 response['Content-Length'] = len(data)
579 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
581 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
582 public = backend.get_object_public(request.user, v_account, v_container, v_object)
586 except NotAllowedError:
587 raise Unauthorized('Access denied')
589 raise ItemNotFound('Object does not exist')
591 raise ItemNotFound('Version does not exist')
593 update_manifest_meta(request, v_account, meta)
594 update_sharing_meta(permissions, v_account, v_container, v_object, meta)
595 update_public_meta(public, meta)
597 # Evaluate conditions.
598 validate_modification_preconditions(request, meta)
600 validate_matching_preconditions(request, meta)
602 response = HttpResponse(status=304)
603 response['ETag'] = meta['hash']
608 if 'X-Object-Manifest' in meta:
610 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
611 objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
612 except NotAllowedError:
613 raise Unauthorized('Access denied')
615 raise BadRequest('Invalid X-Object-Manifest header')
617 raise ItemNotFound('Container does not exist')
621 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
624 except NotAllowedError:
625 raise Unauthorized('Access denied')
627 raise ItemNotFound('Object does not exist')
629 raise ItemNotFound('Version does not exist')
632 s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
635 except NotAllowedError:
636 raise Unauthorized('Access denied')
638 raise ItemNotFound('Object does not exist')
640 raise ItemNotFound('Version does not exist')
642 # Reply with the hashmap.
643 if request.serialization != 'text':
645 hashmap = sum(hashmaps, [])
646 d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
647 if request.serialization == 'xml':
648 d['object'] = v_object
649 data = render_to_string('hashes.xml', d)
650 elif request.serialization == 'json':
653 response = HttpResponse(data, status=200)
654 put_object_headers(response, meta)
655 response['Content-Length'] = len(data)
658 return object_data_response(request, sizes, hashmaps, meta)
660 @api_method('PUT', format_allowed=True)
661 def object_write(request, v_account, v_container, v_object):
662 # Normal Response Codes: 201
663 # Error Response Codes: serviceUnavailable (503),
664 # unprocessableEntity (422),
665 # lengthRequired (411),
667 # itemNotFound (404),
668 # unauthorized (401),
671 if not request.GET.get('format'):
672 request.serialization = 'text'
674 # Evaluate conditions.
675 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
677 meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
678 except NotAllowedError:
679 raise Unauthorized('Access denied')
682 validate_matching_preconditions(request, meta)
684 copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
685 move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
686 if copy_from or move_from:
687 content_length = get_content_length(request) # Required by the API.
691 src_container, src_name = split_container_object_string(move_from)
693 raise BadRequest('Invalid X-Move-From header')
694 version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
697 src_container, src_name = split_container_object_string(copy_from)
699 raise BadRequest('Invalid X-Copy-From header')
700 version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
701 response = HttpResponse(status=201)
702 response['X-Object-Version'] = version_id
705 meta, permissions, public = get_object_headers(request)
707 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
708 content_length = get_content_length(request)
709 # Should be BadRequest, but API says otherwise.
710 if 'Content-Type' not in meta:
711 raise LengthRequired('Missing Content-Type header')
713 if request.serialization != 'text':
715 for block in socket_read_iterator(request, content_length, backend.block_size):
716 data = '%s%s' % (data, block)
718 if request.serialization == 'json':
720 if not hasattr(d, '__getitem__'):
721 raise BadRequest('Invalid data formating')
723 hashmap = d['hashes']
724 size = int(d['bytes'])
726 raise BadRequest('Invalid data formatting')
727 elif request.serialization == 'xml':
729 xml = minidom.parseString(data)
730 obj = xml.getElementsByTagName('object')[0]
731 size = int(obj.attributes['bytes'].value)
733 hashes = xml.getElementsByTagName('hash')
736 hashmap.append(hash.firstChild.data)
738 raise BadRequest('Invalid data formatting')
740 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
745 for data in socket_read_iterator(request, content_length, backend.block_size):
746 # TODO: Raise 408 (Request Timeout) if this takes too long.
747 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
749 hashmap.append(backend.put_block(data))
752 meta['hash'] = md5.hexdigest().lower()
753 etag = request.META.get('HTTP_ETAG')
754 if etag and parse_etags(etag)[0].lower() != meta['hash']:
755 raise UnprocessableEntity('Object ETag does not match')
758 version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
759 except NotAllowedError:
760 raise Unauthorized('Access denied')
761 except IndexError, e:
762 raise Conflict('\n'.join(e.data) + '\n')
764 raise ItemNotFound('Container does not exist')
766 raise BadRequest('Invalid sharing header')
767 except AttributeError, e:
768 raise Conflict('\n'.join(e.data) + '\n')
769 if public is not None:
771 backend.update_object_public(request.user, v_account, v_container, v_object, public)
772 except NotAllowedError:
773 raise Unauthorized('Access denied')
775 raise ItemNotFound('Object does not exist')
777 response = HttpResponse(status=201)
778 response['ETag'] = meta['hash']
779 response['X-Object-Version'] = version_id
783 def object_write_form(request, v_account, v_container, v_object):
784 # Normal Response Codes: 201
785 # Error Response Codes: serviceUnavailable (503),
786 # itemNotFound (404),
787 # unauthorized (401),
790 if not request.FILES.has_key('X-Object-Data'):
791 raise BadRequest('Missing X-Object-Data field')
792 file = request.FILES['X-Object-Data']
795 meta['Content-Type'] = file.content_type
800 for data in file.chunks(backend.block_size):
802 hashmap.append(backend.put_block(data))
805 meta['hash'] = md5.hexdigest().lower()
808 version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
809 except NotAllowedError:
810 raise Unauthorized('Access denied')
812 raise ItemNotFound('Container does not exist')
814 response = HttpResponse(status=201)
815 response['ETag'] = meta['hash']
816 response['X-Object-Version'] = version_id
820 def object_copy(request, v_account, v_container, v_object):
821 # Normal Response Codes: 201
822 # Error Response Codes: serviceUnavailable (503),
823 # itemNotFound (404),
824 # unauthorized (401),
827 dest_path = request.META.get('HTTP_DESTINATION')
829 raise BadRequest('Missing Destination header')
831 dest_container, dest_name = split_container_object_string(dest_path)
833 raise BadRequest('Invalid Destination header')
835 # Evaluate conditions.
836 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
837 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
839 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, src_version)
840 except NotAllowedError:
841 raise Unauthorized('Access denied')
842 except (NameError, IndexError):
843 raise ItemNotFound('Container or object does not exist')
844 validate_matching_preconditions(request, meta)
846 version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
847 response = HttpResponse(status=201)
848 response['X-Object-Version'] = version_id
852 def object_move(request, v_account, v_container, v_object):
853 # Normal Response Codes: 201
854 # Error Response Codes: serviceUnavailable (503),
855 # itemNotFound (404),
856 # unauthorized (401),
859 dest_path = request.META.get('HTTP_DESTINATION')
861 raise BadRequest('Missing Destination header')
863 dest_container, dest_name = split_container_object_string(dest_path)
865 raise BadRequest('Invalid Destination header')
867 # Evaluate conditions.
868 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
870 meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
871 except NotAllowedError:
872 raise Unauthorized('Access denied')
874 raise ItemNotFound('Container or object does not exist')
875 validate_matching_preconditions(request, meta)
877 version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
878 response = HttpResponse(status=201)
879 response['X-Object-Version'] = version_id
883 def object_update(request, v_account, v_container, v_object):
884 # Normal Response Codes: 202, 204
885 # Error Response Codes: serviceUnavailable (503),
887 # itemNotFound (404),
888 # unauthorized (401),
890 meta, permissions, public = get_object_headers(request)
891 content_type = meta.get('Content-Type')
893 del(meta['Content-Type']) # Do not allow changing the Content-Type.
896 prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
897 except NotAllowedError:
898 raise Unauthorized('Access denied')
900 raise ItemNotFound('Object does not exist')
902 # Evaluate conditions.
903 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
904 validate_matching_preconditions(request, prev_meta)
906 # If replacing, keep previous values of 'Content-Type' and 'hash'.
908 if 'update' in request.GET:
911 for k in ('Content-Type', 'hash'):
913 meta[k] = prev_meta[k]
915 # A Content-Type or X-Source-Object header indicates data updates.
916 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
917 if (not content_type or content_type != 'application/octet-stream') and not src_object:
918 response = HttpResponse(status=202)
920 # Do permissions first, as it may fail easier.
921 if permissions is not None:
923 backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
924 except NotAllowedError:
925 raise Unauthorized('Access denied')
927 raise ItemNotFound('Object does not exist')
929 raise BadRequest('Invalid sharing header')
930 except AttributeError, e:
931 raise Conflict('\n'.join(e.data) + '\n')
932 if public is not None:
934 backend.update_object_public(request.user, v_account, v_container, v_object, public)
935 except NotAllowedError:
936 raise Unauthorized('Access denied')
938 raise ItemNotFound('Object does not exist')
941 version_id = backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace)
942 except NotAllowedError:
943 raise Unauthorized('Access denied')
945 raise ItemNotFound('Object does not exist')
946 response['X-Object-Version'] = version_id
950 # Single range update. Range must be in Content-Range.
951 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
952 # (with the addition that '*' is allowed for the range - will append).
953 content_range = request.META.get('HTTP_CONTENT_RANGE')
954 if not content_range:
955 raise BadRequest('Missing Content-Range header')
956 ranges = get_content_range(request)
958 raise RangeNotSatisfiable('Invalid Content-Range header')
961 size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
962 except NotAllowedError:
963 raise Unauthorized('Access denied')
965 raise ItemNotFound('Object does not exist')
967 offset, length, total = ranges
971 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
973 src_container, src_name = split_container_object_string(src_object)
974 src_container = smart_unicode(src_container, strings_only=True)
975 src_name = smart_unicode(src_name, strings_only=True)
976 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
978 src_size, src_hashmap = backend.get_object_hashmap(request.user, v_account, src_container, src_name, src_version)
979 except NotAllowedError:
980 raise Unauthorized('Access denied')
982 raise ItemNotFound('Source object does not exist')
986 elif length > src_size:
987 raise BadRequest('Object length is smaller than range length')
989 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
991 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
992 content_length = get_content_length(request)
995 length = content_length
997 if content_length == -1:
998 # TODO: Get up to length bytes in chunks.
999 length = content_length
1000 elif length != content_length:
1001 raise BadRequest('Content length does not match range length')
1002 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1003 raise RangeNotSatisfiable('Supplied range will change provided object limits')
1005 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1006 if dest_bytes is not None:
1007 dest_bytes = get_int_parameter(dest_bytes)
1008 if dest_bytes is None:
1009 raise BadRequest('Invalid X-Object-Bytes header')
1012 if offset % backend.block_size == 0:
1013 # Update the hashes only.
1016 bi = int(offset / backend.block_size)
1017 bl = min(length, backend.block_size)
1018 if bi < len(hashmap):
1019 if bl == backend.block_size:
1020 hashmap[bi] = src_hashmap[sbi]
1022 data = backend.get_block(src_hashmap[sbi])
1023 hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], 0)
1025 hashmap.append(src_hashmap[sbi])
1033 data += backend.get_block(src_hashmap[sbi])
1034 if length < backend.block_size:
1035 data = data[:length]
1036 bytes = put_object_block(hashmap, data, offset)
1043 for d in socket_read_iterator(request, length, backend.block_size):
1044 # TODO: Raise 408 (Request Timeout) if this takes too long.
1045 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1047 bytes = put_object_block(hashmap, data, offset)
1051 put_object_block(hashmap, data, offset)
1055 if dest_bytes is not None and dest_bytes < size:
1057 hashmap = hashmap[:(int((size - 1) / backend.block_size) + 1)]
1058 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
1060 version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, replace, permissions)
1061 except NotAllowedError:
1062 raise Unauthorized('Access denied')
1064 raise ItemNotFound('Container does not exist')
1066 raise BadRequest('Invalid sharing header')
1067 except AttributeError, e:
1068 raise Conflict('\n'.join(e.data) + '\n')
1069 if public is not None:
1071 backend.update_object_public(request.user, v_account, v_container, v_object, public)
1072 except NotAllowedError:
1073 raise Unauthorized('Access denied')
1075 raise ItemNotFound('Object does not exist')
1077 response = HttpResponse(status=204)
1078 response['ETag'] = meta['hash']
1079 response['X-Object-Version'] = version_id
1082 @api_method('DELETE')
1083 def object_delete(request, v_account, v_container, v_object):
1084 # Normal Response Codes: 204
1085 # Error Response Codes: serviceUnavailable (503),
1086 # itemNotFound (404),
1087 # unauthorized (401),
1090 until = get_int_parameter(request.GET.get('until'))
1092 backend.delete_object(request.user, v_account, v_container, v_object, until)
1093 except NotAllowedError:
1094 raise Unauthorized('Access denied')
1096 raise ItemNotFound('Object does not exist')
1097 return HttpResponse(status=204)
1100 def method_not_allowed(request):
1101 raise BadRequest('Method not allowed')