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 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 = 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 = backend.get_account_meta(request.user, x)
161 groups = 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 = backend.get_account_meta(request.user, v_account, until)
188 groups = 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 backend.update_account_groups(request.user, v_account, groups, replace)
212 except NotAllowedError:
213 raise Unauthorized('Access denied')
215 raise BadRequest('Invalid groups header')
218 backend.update_account_meta(request.user, v_account, meta, replace)
219 except NotAllowedError:
220 raise Unauthorized('Access denied')
221 return HttpResponse(status=202)
223 @api_method('GET', format_allowed=True)
224 def container_list(request, v_account):
225 # Normal Response Codes: 200, 204
226 # Error Response Codes: serviceUnavailable (503),
227 # itemNotFound (404),
228 # unauthorized (401),
231 until = get_int_parameter(request.GET.get('until'))
233 meta = backend.get_account_meta(request.user, v_account, until)
234 groups = backend.get_account_groups(request.user, v_account)
235 except NotAllowedError:
236 raise Unauthorized('Access denied')
238 validate_modification_preconditions(request, meta)
240 response = HttpResponse()
241 put_account_headers(response, meta, groups)
243 marker = request.GET.get('marker')
244 limit = get_int_parameter(request.GET.get('limit'))
249 if 'shared' in request.GET:
253 containers = backend.list_containers(request.user, v_account, marker, limit, shared, until)
254 except NotAllowedError:
255 raise Unauthorized('Access denied')
259 if request.serialization == 'text':
260 if len(containers) == 0:
261 # The cloudfiles python bindings expect 200 if json/xml.
262 response.status_code = 204
264 response.status_code = 200
265 response.content = '\n'.join(containers) + '\n'
271 meta = backend.get_container_meta(request.user, v_account, x, until)
272 policy = backend.get_container_policy(request.user, v_account, x)
273 except NotAllowedError:
274 raise Unauthorized('Access denied')
278 rename_meta_key(meta, 'modified', 'last_modified')
279 rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
280 for k, v in policy.iteritems():
281 meta['X-Container-Policy-' + k] = v
282 container_meta.append(printable_header_dict(meta))
283 if request.serialization == 'xml':
284 data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
285 elif request.serialization == 'json':
286 data = json.dumps(container_meta)
287 response.status_code = 200
288 response.content = data
292 def container_meta(request, v_account, v_container):
293 # Normal Response Codes: 204
294 # Error Response Codes: serviceUnavailable (503),
295 # itemNotFound (404),
296 # unauthorized (401),
299 until = get_int_parameter(request.GET.get('until'))
301 meta = backend.get_container_meta(request.user, v_account, v_container, until)
302 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
303 policy = backend.get_container_policy(request.user, v_account, v_container)
304 except NotAllowedError:
305 raise Unauthorized('Access denied')
307 raise ItemNotFound('Container does not exist')
309 validate_modification_preconditions(request, meta)
311 response = HttpResponse(status=204)
312 put_container_headers(response, meta, policy)
316 def container_create(request, v_account, v_container):
317 # Normal Response Codes: 201, 202
318 # Error Response Codes: serviceUnavailable (503),
319 # itemNotFound (404),
320 # unauthorized (401),
323 meta, policy = get_container_headers(request)
326 backend.put_container(request.user, v_account, v_container, policy)
328 except NotAllowedError:
329 raise Unauthorized('Access denied')
331 raise BadRequest('Invalid policy header')
335 if ret == 202 and policy:
337 backend.update_container_policy(request.user, v_account, v_container, policy, replace=False)
338 except NotAllowedError:
339 raise Unauthorized('Access denied')
341 raise ItemNotFound('Container does not exist')
343 raise BadRequest('Invalid policy header')
346 backend.update_container_meta(request.user, v_account, v_container, meta, replace=False)
347 except NotAllowedError:
348 raise Unauthorized('Access denied')
350 raise ItemNotFound('Container does not exist')
352 return HttpResponse(status=ret)
355 def container_update(request, v_account, v_container):
356 # Normal Response Codes: 202
357 # Error Response Codes: serviceUnavailable (503),
358 # itemNotFound (404),
359 # unauthorized (401),
362 meta, policy = get_container_headers(request)
364 if 'update' in request.GET:
368 backend.update_container_policy(request.user, v_account, v_container, policy, replace)
369 except NotAllowedError:
370 raise Unauthorized('Access denied')
372 raise ItemNotFound('Container does not exist')
374 raise BadRequest('Invalid policy header')
377 backend.update_container_meta(request.user, v_account, v_container, meta, replace)
378 except NotAllowedError:
379 raise Unauthorized('Access denied')
381 raise ItemNotFound('Container does not exist')
382 return HttpResponse(status=202)
384 @api_method('DELETE')
385 def container_delete(request, v_account, v_container):
386 # Normal Response Codes: 204
387 # Error Response Codes: serviceUnavailable (503),
389 # itemNotFound (404),
390 # unauthorized (401),
393 until = get_int_parameter(request.GET.get('until'))
395 backend.delete_container(request.user, v_account, v_container, until)
396 except NotAllowedError:
397 raise Unauthorized('Access denied')
399 raise ItemNotFound('Container does not exist')
401 raise Conflict('Container is not empty')
402 return HttpResponse(status=204)
404 @api_method('GET', format_allowed=True)
405 def object_list(request, v_account, v_container):
406 # Normal Response Codes: 200, 204
407 # Error Response Codes: serviceUnavailable (503),
408 # itemNotFound (404),
409 # unauthorized (401),
412 until = get_int_parameter(request.GET.get('until'))
414 meta = backend.get_container_meta(request.user, v_account, v_container, until)
415 meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
416 policy = backend.get_container_policy(request.user, v_account, v_container)
417 except NotAllowedError:
418 raise Unauthorized('Access denied')
420 raise ItemNotFound('Container does not exist')
422 validate_modification_preconditions(request, meta)
424 response = HttpResponse()
425 put_container_headers(response, meta, policy)
427 path = request.GET.get('path')
428 prefix = request.GET.get('prefix')
429 delimiter = request.GET.get('delimiter')
431 # Path overrides prefix and delimiter.
439 if prefix and delimiter:
440 prefix = prefix + delimiter
443 prefix = prefix.lstrip('/')
445 marker = request.GET.get('marker')
446 limit = get_int_parameter(request.GET.get('limit'))
450 keys = request.GET.get('meta')
452 keys = keys.split(',')
453 l = [smart_str(x) for x in keys if x.strip() != '']
454 keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
459 if 'shared' in request.GET:
463 objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
464 except NotAllowedError:
465 raise Unauthorized('Access denied')
467 raise ItemNotFound('Container does not exist')
469 if request.serialization == 'text':
470 if len(objects) == 0:
471 # The cloudfiles python bindings expect 200 if json/xml.
472 response.status_code = 204
474 response.status_code = 200
475 response.content = '\n'.join([x[0] for x in objects]) + '\n'
481 # Virtual objects/directories.
482 object_meta.append({'subdir': x[0]})
485 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
487 permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
488 public = backend.get_object_public(request.user, v_account, v_container, x[0])
492 except NotAllowedError:
493 raise Unauthorized('Access denied')
497 rename_meta_key(meta, 'modified', 'last_modified')
498 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
499 rename_meta_key(meta, 'version', 'x_object_version')
500 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
501 update_sharing_meta(request, permissions, v_account, v_container, x[0], meta)
502 update_public_meta(public, meta)
503 object_meta.append(printable_header_dict(meta))
504 if request.serialization == 'xml':
505 data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
506 elif request.serialization == 'json':
507 data = json.dumps(object_meta)
508 response.status_code = 200
509 response.content = data
513 def object_meta(request, v_account, v_container, v_object):
514 # Normal Response Codes: 204
515 # Error Response Codes: serviceUnavailable (503),
516 # itemNotFound (404),
517 # unauthorized (401),
520 version = request.GET.get('version')
522 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
524 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
525 public = backend.get_object_public(request.user, v_account, v_container, v_object)
529 except NotAllowedError:
530 raise Unauthorized('Access denied')
532 raise ItemNotFound('Object does not exist')
534 raise ItemNotFound('Version does not exist')
536 update_manifest_meta(request, v_account, meta)
537 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
538 update_public_meta(public, meta)
540 # Evaluate conditions.
541 validate_modification_preconditions(request, meta)
543 validate_matching_preconditions(request, meta)
545 response = HttpResponse(status=304)
546 response['ETag'] = meta['hash']
549 response = HttpResponse(status=200)
550 put_object_headers(response, meta)
553 @api_method('GET', format_allowed=True)
554 def object_read(request, v_account, v_container, v_object):
555 # Normal Response Codes: 200, 206
556 # Error Response Codes: serviceUnavailable (503),
557 # rangeNotSatisfiable (416),
558 # preconditionFailed (412),
559 # itemNotFound (404),
560 # unauthorized (401),
564 version = request.GET.get('version')
566 # Reply with the version list. Do this first, as the object may be deleted.
567 if version == 'list':
568 if request.serialization == 'text':
569 raise BadRequest('No format specified for version list.')
572 v = backend.list_versions(request.user, v_account, v_container, v_object)
573 except NotAllowedError:
574 raise Unauthorized('Access denied')
576 if request.serialization == 'xml':
577 d['object'] = v_object
578 data = render_to_string('versions.xml', d)
579 elif request.serialization == 'json':
582 response = HttpResponse(data, status=200)
583 response['Content-Length'] = len(data)
587 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
589 permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
590 public = backend.get_object_public(request.user, v_account, v_container, v_object)
594 except NotAllowedError:
595 raise Unauthorized('Access denied')
597 raise ItemNotFound('Object does not exist')
599 raise ItemNotFound('Version does not exist')
601 update_manifest_meta(request, v_account, meta)
602 update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
603 update_public_meta(public, meta)
605 # Evaluate conditions.
606 validate_modification_preconditions(request, meta)
608 validate_matching_preconditions(request, meta)
610 response = HttpResponse(status=304)
611 response['ETag'] = meta['hash']
616 if 'X-Object-Manifest' in meta:
618 src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
619 objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
620 except NotAllowedError:
621 raise Unauthorized('Access denied')
623 raise BadRequest('Invalid X-Object-Manifest header')
625 raise ItemNotFound('Container does not exist')
629 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
632 except NotAllowedError:
633 raise Unauthorized('Access denied')
635 raise ItemNotFound('Object does not exist')
637 raise ItemNotFound('Version does not exist')
640 s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
643 except NotAllowedError:
644 raise Unauthorized('Access denied')
646 raise ItemNotFound('Object does not exist')
648 raise ItemNotFound('Version does not exist')
650 # Reply with the hashmap.
651 if request.serialization != 'text':
653 hashmap = sum(hashmaps, [])
654 d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
655 if request.serialization == 'xml':
656 d['object'] = v_object
657 data = render_to_string('hashes.xml', d)
658 elif request.serialization == 'json':
661 response = HttpResponse(data, status=200)
662 put_object_headers(response, meta)
663 response['Content-Length'] = len(data)
666 return object_data_response(request, sizes, hashmaps, meta)
668 @api_method('PUT', format_allowed=True)
669 def object_write(request, v_account, v_container, v_object):
670 # Normal Response Codes: 201
671 # Error Response Codes: serviceUnavailable (503),
672 # unprocessableEntity (422),
673 # lengthRequired (411),
675 # itemNotFound (404),
676 # unauthorized (401),
679 if not request.GET.get('format'):
680 request.serialization = 'text'
682 # Evaluate conditions.
683 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
685 meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
686 except NotAllowedError:
687 raise Unauthorized('Access denied')
690 validate_matching_preconditions(request, meta)
692 copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
693 move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
694 if copy_from or move_from:
695 content_length = get_content_length(request) # Required by the API.
699 src_container, src_name = split_container_object_string(move_from)
701 raise BadRequest('Invalid X-Move-From header')
702 version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
705 src_container, src_name = split_container_object_string(copy_from)
707 raise BadRequest('Invalid X-Copy-From header')
708 version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
709 response = HttpResponse(status=201)
710 response['X-Object-Version'] = version_id
713 meta, permissions, public = get_object_headers(request)
715 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
716 content_length = get_content_length(request)
717 # Should be BadRequest, but API says otherwise.
718 if 'Content-Type' not in meta:
719 raise LengthRequired('Missing Content-Type header')
721 if request.serialization != 'text':
723 for block in socket_read_iterator(request, content_length, backend.block_size):
724 data = '%s%s' % (data, block)
726 if request.serialization == 'json':
728 if not hasattr(d, '__getitem__'):
729 raise BadRequest('Invalid data formating')
731 hashmap = d['hashes']
732 size = int(d['bytes'])
734 raise BadRequest('Invalid data formatting')
735 elif request.serialization == 'xml':
737 xml = minidom.parseString(data)
738 obj = xml.getElementsByTagName('object')[0]
739 size = int(obj.attributes['bytes'].value)
741 hashes = xml.getElementsByTagName('hash')
744 hashmap.append(hash.firstChild.data)
746 raise BadRequest('Invalid data formatting')
748 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
753 for data in socket_read_iterator(request, content_length, backend.block_size):
754 # TODO: Raise 408 (Request Timeout) if this takes too long.
755 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
757 hashmap.append(backend.put_block(data))
760 meta['hash'] = md5.hexdigest().lower()
761 etag = request.META.get('HTTP_ETAG')
762 if etag and parse_etags(etag)[0].lower() != meta['hash']:
763 raise UnprocessableEntity('Object ETag does not match')
766 version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
767 except NotAllowedError:
768 raise Unauthorized('Access denied')
769 except IndexError, e:
770 raise Conflict('\n'.join(e.data) + '\n')
772 raise ItemNotFound('Container does not exist')
774 raise BadRequest('Invalid sharing header')
775 except AttributeError, e:
776 raise Conflict('\n'.join(e.data) + '\n')
777 if public is not None:
779 backend.update_object_public(request.user, v_account, v_container, v_object, public)
780 except NotAllowedError:
781 raise Unauthorized('Access denied')
783 raise ItemNotFound('Object does not exist')
785 response = HttpResponse(status=201)
786 response['ETag'] = meta['hash']
787 response['X-Object-Version'] = version_id
791 def object_write_form(request, v_account, v_container, v_object):
792 # Normal Response Codes: 201
793 # Error Response Codes: serviceUnavailable (503),
794 # itemNotFound (404),
795 # unauthorized (401),
798 if not request.FILES.has_key('X-Object-Data'):
799 raise BadRequest('Missing X-Object-Data field')
800 file = request.FILES['X-Object-Data']
803 meta['Content-Type'] = file.content_type
808 for data in file.chunks(backend.block_size):
810 hashmap.append(backend.put_block(data))
813 meta['hash'] = md5.hexdigest().lower()
816 version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
817 except NotAllowedError:
818 raise Unauthorized('Access denied')
820 raise ItemNotFound('Container does not exist')
822 response = HttpResponse(status=201)
823 response['ETag'] = meta['hash']
824 response['X-Object-Version'] = version_id
828 def object_copy(request, v_account, v_container, v_object):
829 # Normal Response Codes: 201
830 # Error Response Codes: serviceUnavailable (503),
831 # itemNotFound (404),
832 # unauthorized (401),
835 dest_path = request.META.get('HTTP_DESTINATION')
837 raise BadRequest('Missing Destination header')
839 dest_container, dest_name = split_container_object_string(dest_path)
841 raise BadRequest('Invalid Destination header')
843 # Evaluate conditions.
844 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
845 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
847 meta = backend.get_object_meta(request.user, v_account, v_container, v_object, src_version)
848 except NotAllowedError:
849 raise Unauthorized('Access denied')
850 except (NameError, IndexError):
851 raise ItemNotFound('Container or object does not exist')
852 validate_matching_preconditions(request, meta)
854 version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
855 response = HttpResponse(status=201)
856 response['X-Object-Version'] = version_id
860 def object_move(request, v_account, v_container, v_object):
861 # Normal Response Codes: 201
862 # Error Response Codes: serviceUnavailable (503),
863 # itemNotFound (404),
864 # unauthorized (401),
867 dest_path = request.META.get('HTTP_DESTINATION')
869 raise BadRequest('Missing Destination header')
871 dest_container, dest_name = split_container_object_string(dest_path)
873 raise BadRequest('Invalid Destination header')
875 # Evaluate conditions.
876 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
878 meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
879 except NotAllowedError:
880 raise Unauthorized('Access denied')
882 raise ItemNotFound('Container or object does not exist')
883 validate_matching_preconditions(request, meta)
885 version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
886 response = HttpResponse(status=201)
887 response['X-Object-Version'] = version_id
891 def object_update(request, v_account, v_container, v_object):
892 # Normal Response Codes: 202, 204
893 # Error Response Codes: serviceUnavailable (503),
895 # itemNotFound (404),
896 # unauthorized (401),
898 meta, permissions, public = get_object_headers(request)
899 content_type = meta.get('Content-Type')
901 del(meta['Content-Type']) # Do not allow changing the Content-Type.
904 prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
905 except NotAllowedError:
906 raise Unauthorized('Access denied')
908 raise ItemNotFound('Object does not exist')
910 # Evaluate conditions.
911 if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
912 validate_matching_preconditions(request, prev_meta)
914 # If replacing, keep previous values of 'Content-Type' and 'hash'.
916 if 'update' in request.GET:
919 for k in ('Content-Type', 'hash'):
921 meta[k] = prev_meta[k]
923 # A Content-Type or X-Source-Object header indicates data updates.
924 src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
925 if (not content_type or content_type != 'application/octet-stream') and not src_object:
926 response = HttpResponse(status=202)
928 # Do permissions first, as it may fail easier.
929 if permissions is not None:
931 backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
932 except NotAllowedError:
933 raise Unauthorized('Access denied')
935 raise ItemNotFound('Object does not exist')
937 raise BadRequest('Invalid sharing header')
938 except AttributeError, e:
939 raise Conflict('\n'.join(e.data) + '\n')
940 if public is not None:
942 backend.update_object_public(request.user, v_account, v_container, v_object, public)
943 except NotAllowedError:
944 raise Unauthorized('Access denied')
946 raise ItemNotFound('Object does not exist')
949 version_id = backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace)
950 except NotAllowedError:
951 raise Unauthorized('Access denied')
953 raise ItemNotFound('Object does not exist')
954 response['X-Object-Version'] = version_id
958 # Single range update. Range must be in Content-Range.
959 # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
960 # (with the addition that '*' is allowed for the range - will append).
961 content_range = request.META.get('HTTP_CONTENT_RANGE')
962 if not content_range:
963 raise BadRequest('Missing Content-Range header')
964 ranges = get_content_range(request)
966 raise RangeNotSatisfiable('Invalid Content-Range header')
969 size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
970 except NotAllowedError:
971 raise Unauthorized('Access denied')
973 raise ItemNotFound('Object does not exist')
975 offset, length, total = ranges
979 raise RangeNotSatisfiable('Supplied offset is beyond object limits')
981 src_container, src_name = split_container_object_string(src_object)
982 src_container = smart_unicode(src_container, strings_only=True)
983 src_name = smart_unicode(src_name, strings_only=True)
984 src_version = request.META.get('HTTP_X_SOURCE_VERSION')
986 src_size, src_hashmap = backend.get_object_hashmap(request.user, v_account, src_container, src_name, src_version)
987 except NotAllowedError:
988 raise Unauthorized('Access denied')
990 raise ItemNotFound('Source object does not exist')
994 elif length > src_size:
995 raise BadRequest('Object length is smaller than range length')
997 # Require either a Content-Length, or 'chunked' Transfer-Encoding.
999 if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1000 content_length = get_content_length(request)
1003 length = content_length
1005 if content_length == -1:
1006 # TODO: Get up to length bytes in chunks.
1007 length = content_length
1008 elif length != content_length:
1009 raise BadRequest('Content length does not match range length')
1010 if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1011 raise RangeNotSatisfiable('Supplied range will change provided object limits')
1013 dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1014 if dest_bytes is not None:
1015 dest_bytes = get_int_parameter(dest_bytes)
1016 if dest_bytes is None:
1017 raise BadRequest('Invalid X-Object-Bytes header')
1020 if offset % backend.block_size == 0:
1021 # Update the hashes only.
1024 bi = int(offset / backend.block_size)
1025 bl = min(length, backend.block_size)
1026 if bi < len(hashmap):
1027 if bl == backend.block_size:
1028 hashmap[bi] = src_hashmap[sbi]
1030 data = backend.get_block(src_hashmap[sbi])
1031 hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], 0)
1033 hashmap.append(src_hashmap[sbi])
1041 data += backend.get_block(src_hashmap[sbi])
1042 if length < backend.block_size:
1043 data = data[:length]
1044 bytes = put_object_block(hashmap, data, offset)
1051 for d in socket_read_iterator(request, length, backend.block_size):
1052 # TODO: Raise 408 (Request Timeout) if this takes too long.
1053 # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1055 bytes = put_object_block(hashmap, data, offset)
1059 put_object_block(hashmap, data, offset)
1063 if dest_bytes is not None and dest_bytes < size:
1065 hashmap = hashmap[:(int((size - 1) / backend.block_size) + 1)]
1066 meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
1068 version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, replace, permissions)
1069 except NotAllowedError:
1070 raise Unauthorized('Access denied')
1072 raise ItemNotFound('Container does not exist')
1074 raise BadRequest('Invalid sharing header')
1075 except AttributeError, e:
1076 raise Conflict('\n'.join(e.data) + '\n')
1077 if public is not None:
1079 backend.update_object_public(request.user, v_account, v_container, v_object, public)
1080 except NotAllowedError:
1081 raise Unauthorized('Access denied')
1083 raise ItemNotFound('Object does not exist')
1085 response = HttpResponse(status=204)
1086 response['ETag'] = meta['hash']
1087 response['X-Object-Version'] = version_id
1090 @api_method('DELETE')
1091 def object_delete(request, v_account, v_container, v_object):
1092 # Normal Response Codes: 204
1093 # Error Response Codes: serviceUnavailable (503),
1094 # itemNotFound (404),
1095 # unauthorized (401),
1098 until = get_int_parameter(request.GET.get('until'))
1100 backend.delete_object(request.user, v_account, v_container, v_object, until)
1101 except NotAllowedError:
1102 raise Unauthorized('Access denied')
1104 raise ItemNotFound('Object does not exist')
1105 return HttpResponse(status=204)
1108 def method_not_allowed(request):
1109 raise BadRequest('Method not allowed')