Container PUT can also be used for updating metadata/policy.
[pithos] / pithos / api / functions.py
1 # Copyright 2011 GRNET S.A. All rights reserved.
2
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10
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.
15
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.
28
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.
33
34 import logging
35 import hashlib
36
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
44
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
55
56
57 logger = logging.getLogger(__name__)
58
59
60 def top_demux(request):
61     if request.method == 'GET':
62         if request.user:
63             return account_list(request)
64         return authenticate(request)
65     else:
66         return method_not_allowed(request)
67
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)
75     else:
76         return method_not_allowed(request)
77
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)
89     else:
90         return method_not_allowed(request)
91
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)
109     else:
110         return method_not_allowed(request)
111
112 @api_method('GET')
113 def authenticate(request):
114     # Normal Response Codes: 204
115     # Error Response Codes: serviceUnavailable (503),
116     #                       unauthorized (401),
117     #                       badRequest (400)
118     
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)
124     
125     uri = request.build_absolute_uri()
126     if '?' in uri:
127         uri = uri[:uri.find('?')]
128     
129     response['X-Auth-Token'] = x_auth_key
130     response['X-Storage-Url'] = uri + (uri.endswith('/') and '' or '/') + x_auth_user
131     return response
132
133 @api_method('GET', format_allowed=True)
134 def account_list(request):
135     # Normal Response Codes: 200, 204
136     # Error Response Codes: serviceUnavailable (503),
137     #                       badRequest (400)
138     
139     response = HttpResponse()
140     
141     marker = request.GET.get('marker')
142     limit = get_int_parameter(request.GET.get('limit'))
143     if not limit:
144         limit = 10000
145     
146     accounts = backend.list_accounts(request.user, marker, limit)
147     
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
152             return response
153         response.status_code = 200
154         response.content = '\n'.join(accounts) + '\n'
155         return response
156     
157     account_meta = []
158     for x in accounts:
159         try:
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')
164         else:
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
176     return response
177
178 @api_method('HEAD')
179 def account_meta(request, v_account):
180     # Normal Response Codes: 204
181     # Error Response Codes: serviceUnavailable (503),
182     #                       unauthorized (401),
183     #                       badRequest (400)
184     
185     until = get_int_parameter(request.GET.get('until'))
186     try:
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')
191     
192     validate_modification_preconditions(request, meta)
193     
194     response = HttpResponse(status=204)
195     put_account_headers(response, meta, groups)
196     return response
197
198 @api_method('POST')
199 def account_update(request, v_account):
200     # Normal Response Codes: 202
201     # Error Response Codes: serviceUnavailable (503),
202     #                       unauthorized (401),
203     #                       badRequest (400)
204     
205     meta, groups = get_account_headers(request)
206     replace = True
207     if 'update' in request.GET:
208         replace = False
209     if groups:
210         try:
211             backend.update_account_groups(request.user, v_account, groups, replace)
212         except NotAllowedError:
213             raise Unauthorized('Access denied')
214         except ValueError:
215             raise BadRequest('Invalid groups header')
216     if meta or replace:
217         try:
218             backend.update_account_meta(request.user, v_account, meta, replace)
219         except NotAllowedError:
220             raise Unauthorized('Access denied')
221     return HttpResponse(status=202)
222
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),
229     #                       badRequest (400)
230     
231     until = get_int_parameter(request.GET.get('until'))
232     try:
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')
237     
238     validate_modification_preconditions(request, meta)
239     
240     response = HttpResponse()
241     put_account_headers(response, meta, groups)
242     
243     marker = request.GET.get('marker')
244     limit = get_int_parameter(request.GET.get('limit'))
245     if not limit:
246         limit = 10000
247     
248     shared = False
249     if 'shared' in request.GET:
250         shared = True
251     
252     try:
253         containers = backend.list_containers(request.user, v_account, marker, limit, shared, until)
254     except NotAllowedError:
255         raise Unauthorized('Access denied')
256     except NameError:
257         containers = []
258     
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
263             return response
264         response.status_code = 200
265         response.content = '\n'.join(containers) + '\n'
266         return response
267     
268     container_meta = []
269     for x in containers:
270         try:
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')
275         except NameError:
276             pass
277         else:
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
289     return response
290
291 @api_method('HEAD')
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),
297     #                       badRequest (400)
298     
299     until = get_int_parameter(request.GET.get('until'))
300     try:
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')
306     except NameError:
307         raise ItemNotFound('Container does not exist')
308     
309     validate_modification_preconditions(request, meta)
310     
311     response = HttpResponse(status=204)
312     put_container_headers(response, meta, policy)
313     return response
314
315 @api_method('PUT')
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),
321     #                       badRequest (400)
322     
323     meta, policy = get_container_headers(request)
324     
325     try:
326         backend.put_container(request.user, v_account, v_container, policy)
327         ret = 201
328     except NotAllowedError:
329         raise Unauthorized('Access denied')
330     except ValueError:
331         raise BadRequest('Invalid policy header')
332     except NameError:
333         ret = 202
334     
335     if ret == 202 and policy:
336         try:
337             backend.update_container_policy(request.user, v_account, v_container, policy, replace=False)
338         except NotAllowedError:
339             raise Unauthorized('Access denied')
340         except NameError:
341             raise ItemNotFound('Container does not exist')
342         except ValueError:
343             raise BadRequest('Invalid policy header')
344     if meta:
345         try:
346             backend.update_container_meta(request.user, v_account, v_container, meta, replace=False)
347         except NotAllowedError:
348             raise Unauthorized('Access denied')
349         except NameError:
350             raise ItemNotFound('Container does not exist')
351     
352     return HttpResponse(status=ret)
353
354 @api_method('POST')
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),
360     #                       badRequest (400)
361     
362     meta, policy = get_container_headers(request)
363     replace = True
364     if 'update' in request.GET:
365         replace = False
366     if policy:
367         try:
368             backend.update_container_policy(request.user, v_account, v_container, policy, replace)
369         except NotAllowedError:
370             raise Unauthorized('Access denied')
371         except NameError:
372             raise ItemNotFound('Container does not exist')
373         except ValueError:
374             raise BadRequest('Invalid policy header')
375     if meta or replace:
376         try:
377             backend.update_container_meta(request.user, v_account, v_container, meta, replace)
378         except NotAllowedError:
379             raise Unauthorized('Access denied')
380         except NameError:
381             raise ItemNotFound('Container does not exist')
382     return HttpResponse(status=202)
383
384 @api_method('DELETE')
385 def container_delete(request, v_account, v_container):
386     # Normal Response Codes: 204
387     # Error Response Codes: serviceUnavailable (503),
388     #                       conflict (409),
389     #                       itemNotFound (404),
390     #                       unauthorized (401),
391     #                       badRequest (400)
392     
393     until = get_int_parameter(request.GET.get('until'))
394     try:
395         backend.delete_container(request.user, v_account, v_container, until)
396     except NotAllowedError:
397         raise Unauthorized('Access denied')
398     except NameError:
399         raise ItemNotFound('Container does not exist')
400     except IndexError:
401         raise Conflict('Container is not empty')
402     return HttpResponse(status=204)
403
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),
410     #                       badRequest (400)
411     
412     until = get_int_parameter(request.GET.get('until'))
413     try:
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')
419     except NameError:
420         raise ItemNotFound('Container does not exist')
421     
422     validate_modification_preconditions(request, meta)
423     
424     response = HttpResponse()
425     put_container_headers(response, meta, policy)
426     
427     path = request.GET.get('path')
428     prefix = request.GET.get('prefix')
429     delimiter = request.GET.get('delimiter')
430     
431     # Path overrides prefix and delimiter.
432     virtual = True
433     if path:
434         prefix = path
435         delimiter = '/'
436         virtual = False
437     
438     # Naming policy.
439     if prefix and delimiter:
440         prefix = prefix + delimiter
441     if not prefix:
442         prefix = ''
443     prefix = prefix.lstrip('/')
444     
445     marker = request.GET.get('marker')
446     limit = get_int_parameter(request.GET.get('limit'))
447     if not limit:
448         limit = 10000
449     
450     keys = request.GET.get('meta')
451     if keys:
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]
455     else:
456         keys = []
457     
458     shared = False
459     if 'shared' in request.GET:
460         shared = True
461     
462     try:
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')
466     except NameError:
467         raise ItemNotFound('Container does not exist')
468     
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
473             return response
474         response.status_code = 200
475         response.content = '\n'.join([x[0] for x in objects]) + '\n'
476         return response
477     
478     object_meta = []
479     for x in objects:
480         if x[1] is None:
481             # Virtual objects/directories.
482             object_meta.append({'subdir': x[0]})
483         else:
484             try:
485                 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
486                 if until is None:
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])
489                 else:
490                     permissions = None
491                     public = None
492             except NotAllowedError:
493                 raise Unauthorized('Access denied')
494             except NameError:
495                 pass
496             else:
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(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
510     return response
511
512 @api_method('HEAD')
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),
518     #                       badRequest (400)
519     
520     version = request.GET.get('version')
521     try:
522         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
523         if version is None:
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)
526         else:
527             permissions = None
528             public = None
529     except NotAllowedError:
530         raise Unauthorized('Access denied')
531     except NameError:
532         raise ItemNotFound('Object does not exist')
533     except IndexError:
534         raise ItemNotFound('Version does not exist')
535     
536     update_manifest_meta(request, v_account, meta)
537     update_sharing_meta(permissions, v_account, v_container, v_object, meta)
538     update_public_meta(public, meta)
539     
540     # Evaluate conditions.
541     validate_modification_preconditions(request, meta)
542     try:
543         validate_matching_preconditions(request, meta)
544     except NotModified:
545         response = HttpResponse(status=304)
546         response['ETag'] = meta['hash']
547         return response
548     
549     response = HttpResponse(status=200)
550     put_object_headers(response, meta)
551     return response
552
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),
561     #                       badRequest (400),
562     #                       notModified (304)
563     
564     version = request.GET.get('version')
565     
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.')
570         
571         try:
572             v = backend.list_versions(request.user, v_account, v_container, v_object)
573         except NotAllowedError:
574             raise Unauthorized('Access denied')
575         d = {'versions': v}
576         if request.serialization == 'xml':
577             d['object'] = v_object
578             data = render_to_string('versions.xml', d)
579         elif request.serialization  == 'json':
580             data = json.dumps(d)
581         
582         response = HttpResponse(data, status=200)
583         response['Content-Length'] = len(data)
584         return response
585     
586     try:
587         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
588         if version is None:
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)
591         else:
592             permissions = None
593             public = None
594     except NotAllowedError:
595         raise Unauthorized('Access denied')
596     except NameError:
597         raise ItemNotFound('Object does not exist')
598     except IndexError:
599         raise ItemNotFound('Version does not exist')
600     
601     update_manifest_meta(request, v_account, meta)
602     update_sharing_meta(permissions, v_account, v_container, v_object, meta)
603     update_public_meta(public, meta)
604     
605     # Evaluate conditions.
606     validate_modification_preconditions(request, meta)
607     try:
608         validate_matching_preconditions(request, meta)
609     except NotModified:
610         response = HttpResponse(status=304)
611         response['ETag'] = meta['hash']
612         return response
613     
614     sizes = []
615     hashmaps = []
616     if 'X-Object-Manifest' in meta:
617         try:
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')
622         except ValueError:
623             raise BadRequest('Invalid X-Object-Manifest header')
624         except NameError:
625             raise ItemNotFound('Container does not exist')
626         
627         try:
628             for x in objects:
629                 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
630                 sizes.append(s)
631                 hashmaps.append(h)
632         except NotAllowedError:
633             raise Unauthorized('Access denied')
634         except NameError:
635             raise ItemNotFound('Object does not exist')
636         except IndexError:
637             raise ItemNotFound('Version does not exist')
638     else:
639         try:
640             s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
641             sizes.append(s)
642             hashmaps.append(h)
643         except NotAllowedError:
644             raise Unauthorized('Access denied')
645         except NameError:
646             raise ItemNotFound('Object does not exist')
647         except IndexError:
648             raise ItemNotFound('Version does not exist')
649     
650     # Reply with the hashmap.
651     if request.serialization != 'text':
652         size = sum(sizes)
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':
659             data = json.dumps(d)
660         
661         response = HttpResponse(data, status=200)
662         put_object_headers(response, meta)
663         response['Content-Length'] = len(data)
664         return response
665     
666     return object_data_response(request, sizes, hashmaps, meta)
667
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),
674     #                       conflict (409),
675     #                       itemNotFound (404),
676     #                       unauthorized (401),
677     #                       badRequest (400)
678     
679     if not request.GET.get('format'):
680         request.serialization = 'text'
681     
682     # Evaluate conditions.
683     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
684         try:
685             meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
686         except NotAllowedError:
687             raise Unauthorized('Access denied')
688         except NameError:
689             meta = {}
690         validate_matching_preconditions(request, meta)
691     
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.
696         
697         if move_from:
698             try:
699                 src_container, src_name = split_container_object_string(move_from)
700             except ValueError:
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)
703         else:
704             try:
705                 src_container, src_name = split_container_object_string(copy_from)
706             except ValueError:
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
711         return response
712     
713     meta, permissions, public = get_object_headers(request)
714     content_length = -1
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')
720     
721     if request.serialization != 'text':
722         data = ''
723         for block in socket_read_iterator(request, content_length, backend.block_size):
724             data = '%s%s' % (data, block)
725         
726         if request.serialization == 'json':
727             d = json.loads(data)
728             if not hasattr(d, '__getitem__'):
729                 raise BadRequest('Invalid data formating')
730             try:
731                 hashmap = d['hashes']
732                 size = int(d['bytes'])
733             except:
734                 raise BadRequest('Invalid data formatting')
735         elif request.serialization == 'xml':
736             try:
737                 xml = minidom.parseString(data)
738                 obj = xml.getElementsByTagName('object')[0]
739                 size = int(obj.attributes['bytes'].value)
740                 
741                 hashes = xml.getElementsByTagName('hash')
742                 hashmap = []
743                 for hash in hashes:
744                     hashmap.append(hash.firstChild.data)
745             except:
746                 raise BadRequest('Invalid data formatting')
747         
748         meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
749     else:
750         md5 = hashlib.md5()
751         size = 0
752         hashmap = []
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.
756             size += len(data)
757             hashmap.append(backend.put_block(data))
758             md5.update(data)
759         
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')
764     
765     try:
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')
771     except NameError:
772         raise ItemNotFound('Container does not exist')
773     except ValueError:
774         raise BadRequest('Invalid sharing header')
775     except AttributeError, e:
776         raise Conflict('\n'.join(e.data) + '\n')
777     if public is not None:
778         try:
779             backend.update_object_public(request.user, v_account, v_container, v_object, public)
780         except NotAllowedError:
781             raise Unauthorized('Access denied')
782         except NameError:
783             raise ItemNotFound('Object does not exist')
784     
785     response = HttpResponse(status=201)
786     response['ETag'] = meta['hash']
787     response['X-Object-Version'] = version_id
788     return response
789
790 @api_method('POST')
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),
796     #                       badRequest (400)
797     
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']
801     
802     meta = {}
803     meta['Content-Type'] = file.content_type
804     
805     md5 = hashlib.md5()
806     size = 0
807     hashmap = []
808     for data in file.chunks(backend.block_size):
809         size += len(data)
810         hashmap.append(backend.put_block(data))
811         md5.update(data)
812     
813     meta['hash'] = md5.hexdigest().lower()
814     
815     try:
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')
819     except NameError:
820         raise ItemNotFound('Container does not exist')
821     
822     response = HttpResponse(status=201)
823     response['ETag'] = meta['hash']
824     response['X-Object-Version'] = version_id
825     return response
826
827 @api_method('COPY')
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),
833     #                       badRequest (400)
834     
835     dest_path = request.META.get('HTTP_DESTINATION')
836     if not dest_path:
837         raise BadRequest('Missing Destination header')
838     try:
839         dest_container, dest_name = split_container_object_string(dest_path)
840     except ValueError:
841         raise BadRequest('Invalid Destination header')
842     
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')
846         try:
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)
853     
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
857     return response
858
859 @api_method('MOVE')
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),
865     #                       badRequest (400)
866     
867     dest_path = request.META.get('HTTP_DESTINATION')
868     if not dest_path:
869         raise BadRequest('Missing Destination header')
870     try:
871         dest_container, dest_name = split_container_object_string(dest_path)
872     except ValueError:
873         raise BadRequest('Invalid Destination header')
874     
875     # Evaluate conditions.
876     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
877         try:
878             meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
879         except NotAllowedError:
880             raise Unauthorized('Access denied')
881         except NameError:
882             raise ItemNotFound('Container or object does not exist')
883         validate_matching_preconditions(request, meta)
884     
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
888     return response
889
890 @api_method('POST')
891 def object_update(request, v_account, v_container, v_object):
892     # Normal Response Codes: 202, 204
893     # Error Response Codes: serviceUnavailable (503),
894     #                       conflict (409),
895     #                       itemNotFound (404),
896     #                       unauthorized (401),
897     #                       badRequest (400)
898     meta, permissions, public = get_object_headers(request)
899     content_type = meta.get('Content-Type')
900     if content_type:
901         del(meta['Content-Type']) # Do not allow changing the Content-Type.
902     
903     try:
904         prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
905     except NotAllowedError:
906         raise Unauthorized('Access denied')
907     except NameError:
908         raise ItemNotFound('Object does not exist')
909     
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)
913     
914     # If replacing, keep previous values of 'Content-Type' and 'hash'.
915     replace = True
916     if 'update' in request.GET:
917         replace = False
918     if replace:
919         for k in ('Content-Type', 'hash'):
920             if k in prev_meta:
921                 meta[k] = prev_meta[k]
922     
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)
927         
928         # Do permissions first, as it may fail easier.
929         if permissions is not None:
930             try:
931                 backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
932             except NotAllowedError:
933                 raise Unauthorized('Access denied')
934             except NameError:
935                 raise ItemNotFound('Object does not exist')
936             except ValueError:
937                 raise BadRequest('Invalid sharing header')
938             except AttributeError, e:
939                 raise Conflict('\n'.join(e.data) + '\n')
940         if public is not None:
941             try:
942                 backend.update_object_public(request.user, v_account, v_container, v_object, public)
943             except NotAllowedError:
944                 raise Unauthorized('Access denied')
945             except NameError:
946                 raise ItemNotFound('Object does not exist')
947         if meta or replace:
948             try:
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')
952             except NameError:
953                 raise ItemNotFound('Object does not exist')        
954             response['X-Object-Version'] = version_id
955         
956         return response
957     
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)
965     if not ranges:
966         raise RangeNotSatisfiable('Invalid Content-Range header')
967     
968     try:
969         size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
970     except NotAllowedError:
971         raise Unauthorized('Access denied')
972     except NameError:
973         raise ItemNotFound('Object does not exist')
974     
975     offset, length, total = ranges
976     if offset is None:
977         offset = size
978     elif offset > size:
979         raise RangeNotSatisfiable('Supplied offset is beyond object limits')
980     if src_object:
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')
985         try:
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')
989         except NameError:
990             raise ItemNotFound('Source object does not exist')
991         
992         if length is None:
993             length = src_size
994         elif length > src_size:
995             raise BadRequest('Object length is smaller than range length')
996     else:
997         # Require either a Content-Length, or 'chunked' Transfer-Encoding.
998         content_length = -1
999         if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1000             content_length = get_content_length(request)
1001         
1002         if length is None:
1003             length = content_length
1004         else:
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')
1012     
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')
1018     
1019     if src_object:
1020         if offset % backend.block_size == 0:
1021             # Update the hashes only.
1022             sbi = 0
1023             while length > 0:
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]
1029                     else:
1030                         data = backend.get_block(src_hashmap[sbi])
1031                         hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], 0)
1032                 else:
1033                     hashmap.append(src_hashmap[sbi])
1034                 offset += bl
1035                 length -= bl
1036                 sbi += 1
1037         else:
1038             data = ''
1039             sbi = 0
1040             while length > 0:
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)
1045                 offset += bytes
1046                 data = data[bytes:]
1047                 length -= bytes
1048                 sbi += 1
1049     else:
1050         data = ''
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.
1054             data += d
1055             bytes = put_object_block(hashmap, data, offset)
1056             offset += bytes
1057             data = data[bytes:]
1058         if len(data) > 0:
1059             put_object_block(hashmap, data, offset)
1060     
1061     if offset > size:
1062         size = offset
1063     if dest_bytes is not None and dest_bytes < size:
1064         size = dest_bytes
1065         hashmap = hashmap[:(int((size - 1) / backend.block_size) + 1)]
1066     meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
1067     try:
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')
1071     except NameError:
1072         raise ItemNotFound('Container does not exist')
1073     except ValueError:
1074         raise BadRequest('Invalid sharing header')
1075     except AttributeError, e:
1076         raise Conflict('\n'.join(e.data) + '\n')
1077     if public is not None:
1078         try:
1079             backend.update_object_public(request.user, v_account, v_container, v_object, public)
1080         except NotAllowedError:
1081             raise Unauthorized('Access denied')
1082         except NameError:
1083             raise ItemNotFound('Object does not exist')
1084     
1085     response = HttpResponse(status=204)
1086     response['ETag'] = meta['hash']
1087     response['X-Object-Version'] = version_id
1088     return response
1089
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),
1096     #                       badRequest (400)
1097     
1098     until = get_int_parameter(request.GET.get('until'))
1099     try:
1100         backend.delete_object(request.user, v_account, v_container, v_object, until)
1101     except NotAllowedError:
1102         raise Unauthorized('Access denied')
1103     except NameError:
1104         raise ItemNotFound('Object does not exist')
1105     return HttpResponse(status=204)
1106
1107 @api_method()
1108 def method_not_allowed(request):
1109     raise BadRequest('Method not allowed')