Fix reading when apache does the dechunking.
[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 xml.dom import minidom
43
44 from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
45     LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
46 from pithos.api.util import (rename_meta_key, format_header_key, printable_header_dict, get_account_headers,
47     put_account_headers, get_container_headers, put_container_headers, get_object_headers, put_object_headers,
48     update_manifest_meta, update_sharing_meta, update_public_meta, validate_modification_preconditions,
49     validate_matching_preconditions, split_container_object_string, copy_or_move_object,
50     get_int_parameter, get_content_length, get_content_range, socket_read_iterator,
51     object_data_response, put_object_block, hashmap_hash, api_method)
52 from pithos.backends import backend
53 from pithos.backends.base import NotAllowedError
54
55
56 logger = logging.getLogger(__name__)
57
58
59 def top_demux(request):
60     if request.method == 'GET':
61         if request.user:
62             return account_list(request)
63         return authenticate(request)
64     else:
65         return method_not_allowed(request)
66
67 def account_demux(request, v_account):
68     if request.method == 'HEAD':
69         return account_meta(request, v_account)
70     elif request.method == 'POST':
71         return account_update(request, v_account)
72     elif request.method == 'GET':
73         return container_list(request, v_account)
74     else:
75         return method_not_allowed(request)
76
77 def container_demux(request, v_account, v_container):
78     if request.method == 'HEAD':
79         return container_meta(request, v_account, v_container)
80     elif request.method == 'PUT':
81         return container_create(request, v_account, v_container)
82     elif request.method == 'POST':
83         return container_update(request, v_account, v_container)
84     elif request.method == 'DELETE':
85         return container_delete(request, v_account, v_container)
86     elif request.method == 'GET':
87         return object_list(request, v_account, v_container)
88     else:
89         return method_not_allowed(request)
90
91 def object_demux(request, v_account, v_container, v_object):
92     if request.method == 'HEAD':
93         return object_meta(request, v_account, v_container, v_object)
94     elif request.method == 'GET':
95         return object_read(request, v_account, v_container, v_object)
96     elif request.method == 'PUT':
97         return object_write(request, v_account, v_container, v_object)
98     elif request.method == 'COPY':
99         return object_copy(request, v_account, v_container, v_object)
100     elif request.method == 'MOVE':
101         return object_move(request, v_account, v_container, v_object)
102     elif request.method == 'POST':
103         if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
104             return object_write_form(request, v_account, v_container, v_object)
105         return object_update(request, v_account, v_container, v_object)
106     elif request.method == 'DELETE':
107         return object_delete(request, v_account, v_container, v_object)
108     else:
109         return method_not_allowed(request)
110
111 @api_method('GET')
112 def authenticate(request):
113     # Normal Response Codes: 204
114     # Error Response Codes: serviceUnavailable (503),
115     #                       unauthorized (401),
116     #                       badRequest (400)
117     
118     x_auth_user = request.META.get('HTTP_X_AUTH_USER')
119     x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
120     if not x_auth_user or not x_auth_key:
121         raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
122     response = HttpResponse(status=204)
123     
124     inv_auth_tokens = dict((v, k) for k, v in settings.AUTH_TOKENS.items())
125     uri = request.build_absolute_uri()
126     if '?' in uri:
127         uri = uri[:uri.find('?')]
128     
129     response['X-Auth-Token'] = inv_auth_tokens.get(x_auth_user, '0000')
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     try:
217         backend.update_account_meta(request.user, v_account, meta, replace)
218     except NotAllowedError:
219         raise Unauthorized('Access denied')
220     return HttpResponse(status=202)
221
222 @api_method('GET', format_allowed=True)
223 def container_list(request, v_account):
224     # Normal Response Codes: 200, 204
225     # Error Response Codes: serviceUnavailable (503),
226     #                       itemNotFound (404),
227     #                       unauthorized (401),
228     #                       badRequest (400)
229     
230     until = get_int_parameter(request.GET.get('until'))
231     try:
232         meta = backend.get_account_meta(request.user, v_account, until)
233         groups = backend.get_account_groups(request.user, v_account)
234     except NotAllowedError:
235         raise Unauthorized('Access denied')
236     
237     validate_modification_preconditions(request, meta)
238     
239     response = HttpResponse()
240     put_account_headers(response, meta, groups)
241     
242     marker = request.GET.get('marker')
243     limit = get_int_parameter(request.GET.get('limit'))
244     if not limit:
245         limit = 10000
246     
247     shared = False
248     if 'shared' in request.GET:
249         shared = True
250     
251     try:
252         containers = backend.list_containers(request.user, v_account, marker, limit, shared, until)
253     except NotAllowedError:
254         raise Unauthorized('Access denied')
255     except NameError:
256         containers = []
257     
258     if request.serialization == 'text':
259         if len(containers) == 0:
260             # The cloudfiles python bindings expect 200 if json/xml.
261             response.status_code = 204
262             return response
263         response.status_code = 200
264         response.content = '\n'.join(containers) + '\n'
265         return response
266     
267     container_meta = []
268     for x in containers:
269         try:
270             meta = backend.get_container_meta(request.user, v_account, x, until)
271             policy = backend.get_container_policy(request.user, v_account, x)
272         except NotAllowedError:
273             raise Unauthorized('Access denied')
274         except NameError:
275             pass
276         else:
277             rename_meta_key(meta, 'modified', 'last_modified')
278             rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
279             for k, v in policy.iteritems():
280                 meta['X-Container-Policy-' + k] = v
281             container_meta.append(printable_header_dict(meta))
282     if request.serialization == 'xml':
283         data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
284     elif request.serialization  == 'json':
285         data = json.dumps(container_meta)
286     response.status_code = 200
287     response.content = data
288     return response
289
290 @api_method('HEAD')
291 def container_meta(request, v_account, v_container):
292     # Normal Response Codes: 204
293     # Error Response Codes: serviceUnavailable (503),
294     #                       itemNotFound (404),
295     #                       unauthorized (401),
296     #                       badRequest (400)
297     
298     until = get_int_parameter(request.GET.get('until'))
299     try:
300         meta = backend.get_container_meta(request.user, v_account, v_container, until)
301         meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
302         policy = backend.get_container_policy(request.user, v_account, v_container)
303     except NotAllowedError:
304         raise Unauthorized('Access denied')
305     except NameError:
306         raise ItemNotFound('Container does not exist')
307     
308     validate_modification_preconditions(request, meta)
309     
310     response = HttpResponse(status=204)
311     put_container_headers(response, meta, policy)
312     return response
313
314 @api_method('PUT')
315 def container_create(request, v_account, v_container):
316     # Normal Response Codes: 201, 202
317     # Error Response Codes: serviceUnavailable (503),
318     #                       itemNotFound (404),
319     #                       unauthorized (401),
320     #                       badRequest (400)
321     
322     meta, policy = get_container_headers(request)
323     
324     try:
325         backend.put_container(request.user, v_account, v_container, policy)
326         ret = 201
327     except NotAllowedError:
328         raise Unauthorized('Access denied')
329     except ValueError:
330         raise BadRequest('Invalid policy header')
331     except NameError:
332         ret = 202
333     
334     if len(meta) > 0:
335         try:
336             backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
337         except NotAllowedError:
338             raise Unauthorized('Access denied')
339         except NameError:
340             raise ItemNotFound('Container does not exist')
341     
342     return HttpResponse(status=ret)
343
344 @api_method('POST')
345 def container_update(request, v_account, v_container):
346     # Normal Response Codes: 202
347     # Error Response Codes: serviceUnavailable (503),
348     #                       itemNotFound (404),
349     #                       unauthorized (401),
350     #                       badRequest (400)
351     
352     meta, policy = get_container_headers(request)
353     replace = True
354     if 'update' in request.GET:
355         replace = False
356     if policy:
357         try:
358             backend.update_container_policy(request.user, v_account, v_container, policy, replace)
359         except NotAllowedError:
360             raise Unauthorized('Access denied')
361         except NameError:
362             raise ItemNotFound('Container does not exist')
363         except ValueError:
364             raise BadRequest('Invalid policy header')
365     try:
366         backend.update_container_meta(request.user, v_account, v_container, meta, replace)
367     except NotAllowedError:
368         raise Unauthorized('Access denied')
369     except NameError:
370         raise ItemNotFound('Container does not exist')
371     return HttpResponse(status=202)
372
373 @api_method('DELETE')
374 def container_delete(request, v_account, v_container):
375     # Normal Response Codes: 204
376     # Error Response Codes: serviceUnavailable (503),
377     #                       conflict (409),
378     #                       itemNotFound (404),
379     #                       unauthorized (401),
380     #                       badRequest (400)
381     
382     until = get_int_parameter(request.GET.get('until'))
383     try:
384         backend.delete_container(request.user, v_account, v_container, until)
385     except NotAllowedError:
386         raise Unauthorized('Access denied')
387     except NameError:
388         raise ItemNotFound('Container does not exist')
389     except IndexError:
390         raise Conflict('Container is not empty')
391     return HttpResponse(status=204)
392
393 @api_method('GET', format_allowed=True)
394 def object_list(request, v_account, v_container):
395     # Normal Response Codes: 200, 204
396     # Error Response Codes: serviceUnavailable (503),
397     #                       itemNotFound (404),
398     #                       unauthorized (401),
399     #                       badRequest (400)
400     
401     until = get_int_parameter(request.GET.get('until'))
402     try:
403         meta = backend.get_container_meta(request.user, v_account, v_container, until)
404         meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
405         policy = backend.get_container_policy(request.user, v_account, v_container)
406     except NotAllowedError:
407         raise Unauthorized('Access denied')
408     except NameError:
409         raise ItemNotFound('Container does not exist')
410     
411     validate_modification_preconditions(request, meta)
412     
413     response = HttpResponse()
414     put_container_headers(response, meta, policy)
415     
416     path = request.GET.get('path')
417     prefix = request.GET.get('prefix')
418     delimiter = request.GET.get('delimiter')
419     
420     # Path overrides prefix and delimiter.
421     virtual = True
422     if path:
423         prefix = path
424         delimiter = '/'
425         virtual = False
426     
427     # Naming policy.
428     if prefix and delimiter:
429         prefix = prefix + delimiter
430     if not prefix:
431         prefix = ''
432     prefix = prefix.lstrip('/')
433     
434     marker = request.GET.get('marker')
435     limit = get_int_parameter(request.GET.get('limit'))
436     if not limit:
437         limit = 10000
438     
439     keys = request.GET.get('meta')
440     if keys:
441         keys = keys.split(',')
442         keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in keys if x.strip() != '']
443     else:
444         keys = []
445     
446     shared = False
447     if 'shared' in request.GET:
448         shared = True
449     
450     try:
451         objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
452     except NotAllowedError:
453         raise Unauthorized('Access denied')
454     except NameError:
455         raise ItemNotFound('Container does not exist')
456     
457     if request.serialization == 'text':
458         if len(objects) == 0:
459             # The cloudfiles python bindings expect 200 if json/xml.
460             response.status_code = 204
461             return response
462         response.status_code = 200
463         response.content = '\n'.join([x[0] for x in objects]) + '\n'
464         return response
465     
466     object_meta = []
467     for x in objects:
468         if x[1] is None:
469             # Virtual objects/directories.
470             object_meta.append({'subdir': x[0]})
471         else:
472             try:
473                 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
474                 if until is None:
475                     permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
476                     public = backend.get_object_public(request.user, v_account, v_container, x[0])
477                 else:
478                     permissions = None
479                     public = None
480             except NotAllowedError:
481                 raise Unauthorized('Access denied')
482             except NameError:
483                 pass
484             else:
485                 rename_meta_key(meta, 'modified', 'last_modified')
486                 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
487                 rename_meta_key(meta, 'version', 'x_object_version')
488                 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
489                 update_sharing_meta(permissions, v_account, v_container, x[0], meta)
490                 update_public_meta(public, meta)
491                 object_meta.append(printable_header_dict(meta))
492     if request.serialization == 'xml':
493         data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
494     elif request.serialization  == 'json':
495         data = json.dumps(object_meta)
496     response.status_code = 200
497     response.content = data
498     return response
499
500 @api_method('HEAD')
501 def object_meta(request, v_account, v_container, v_object):
502     # Normal Response Codes: 204
503     # Error Response Codes: serviceUnavailable (503),
504     #                       itemNotFound (404),
505     #                       unauthorized (401),
506     #                       badRequest (400)
507     
508     version = request.GET.get('version')
509     try:
510         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
511         if version is None:
512             permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
513             public = backend.get_object_public(request.user, v_account, v_container, v_object)
514         else:
515             permissions = None
516             public = None
517     except NotAllowedError:
518         raise Unauthorized('Access denied')
519     except NameError:
520         raise ItemNotFound('Object does not exist')
521     except IndexError:
522         raise ItemNotFound('Version does not exist')
523     
524     update_manifest_meta(request, v_account, meta)
525     update_sharing_meta(permissions, v_account, v_container, v_object, meta)
526     update_public_meta(public, meta)
527     
528     # Evaluate conditions.
529     validate_modification_preconditions(request, meta)
530     try:
531         validate_matching_preconditions(request, meta)
532     except NotModified:
533         response = HttpResponse(status=304)
534         response['ETag'] = meta['hash']
535         return response
536     
537     response = HttpResponse(status=200)
538     put_object_headers(response, meta)
539     return response
540
541 @api_method('GET', format_allowed=True)
542 def object_read(request, v_account, v_container, v_object):
543     # Normal Response Codes: 200, 206
544     # Error Response Codes: serviceUnavailable (503),
545     #                       rangeNotSatisfiable (416),
546     #                       preconditionFailed (412),
547     #                       itemNotFound (404),
548     #                       unauthorized (401),
549     #                       badRequest (400),
550     #                       notModified (304)
551     
552     version = request.GET.get('version')
553     
554     # Reply with the version list. Do this first, as the object may be deleted.
555     if version == 'list':
556         if request.serialization == 'text':
557             raise BadRequest('No format specified for version list.')
558         
559         try:
560             v = backend.list_versions(request.user, v_account, v_container, v_object)
561         except NotAllowedError:
562             raise Unauthorized('Access denied')
563         d = {'versions': v}
564         if request.serialization == 'xml':
565             d['object'] = v_object
566             data = render_to_string('versions.xml', d)
567         elif request.serialization  == 'json':
568             data = json.dumps(d)
569         
570         response = HttpResponse(data, status=200)
571         response['Content-Length'] = len(data)
572         return response
573     
574     try:
575         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
576         if version is None:
577             permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
578             public = backend.get_object_public(request.user, v_account, v_container, v_object)
579         else:
580             permissions = None
581             public = None
582     except NotAllowedError:
583         raise Unauthorized('Access denied')
584     except NameError:
585         raise ItemNotFound('Object does not exist')
586     except IndexError:
587         raise ItemNotFound('Version does not exist')
588     
589     update_manifest_meta(request, v_account, meta)
590     update_sharing_meta(permissions, v_account, v_container, v_object, meta)
591     update_public_meta(public, meta)
592     
593     # Evaluate conditions.
594     validate_modification_preconditions(request, meta)
595     try:
596         validate_matching_preconditions(request, meta)
597     except NotModified:
598         response = HttpResponse(status=304)
599         response['ETag'] = meta['hash']
600         return response
601     
602     sizes = []
603     hashmaps = []
604     if 'X-Object-Manifest' in meta:
605         try:
606             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
607             objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
608         except NotAllowedError:
609             raise Unauthorized('Access denied')
610         except ValueError:
611             raise BadRequest('Invalid X-Object-Manifest header')
612         except NameError:
613             raise ItemNotFound('Container does not exist')
614         
615         try:
616             for x in objects:
617                 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
618                 sizes.append(s)
619                 hashmaps.append(h)
620         except NotAllowedError:
621             raise Unauthorized('Access denied')
622         except NameError:
623             raise ItemNotFound('Object does not exist')
624         except IndexError:
625             raise ItemNotFound('Version does not exist')
626     else:
627         try:
628             s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
629             sizes.append(s)
630             hashmaps.append(h)
631         except NotAllowedError:
632             raise Unauthorized('Access denied')
633         except NameError:
634             raise ItemNotFound('Object does not exist')
635         except IndexError:
636             raise ItemNotFound('Version does not exist')
637     
638     # Reply with the hashmap.
639     if request.serialization != 'text':
640         size = sum(sizes)
641         hashmap = sum(hashmaps, [])
642         d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
643         if request.serialization == 'xml':
644             d['object'] = v_object
645             data = render_to_string('hashes.xml', d)
646         elif request.serialization  == 'json':
647             data = json.dumps(d)
648         
649         response = HttpResponse(data, status=200)
650         put_object_headers(response, meta)
651         response['Content-Length'] = len(data)
652         return response
653     
654     return object_data_response(request, sizes, hashmaps, meta)
655
656 @api_method('PUT', format_allowed=True)
657 def object_write(request, v_account, v_container, v_object):
658     # Normal Response Codes: 201
659     # Error Response Codes: serviceUnavailable (503),
660     #                       unprocessableEntity (422),
661     #                       lengthRequired (411),
662     #                       conflict (409),
663     #                       itemNotFound (404),
664     #                       unauthorized (401),
665     #                       badRequest (400)
666     
667     if not request.GET.get('format'):
668         request.serialization = 'text'
669     
670     # Evaluate conditions.
671     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
672         try:
673             meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
674         except NotAllowedError:
675             raise Unauthorized('Access denied')
676         except NameError:
677             meta = {}
678         validate_matching_preconditions(request, meta)
679     
680     copy_from = request.META.get('HTTP_X_COPY_FROM')
681     move_from = request.META.get('HTTP_X_MOVE_FROM')
682     if copy_from or move_from:
683         content_length = get_content_length(request) # Required by the API.
684         
685         if move_from:
686             try:
687                 src_container, src_name = split_container_object_string(move_from)
688             except ValueError:
689                 raise BadRequest('Invalid X-Move-From header')
690             copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
691         else:
692             try:
693                 src_container, src_name = split_container_object_string(copy_from)
694             except ValueError:
695                 raise BadRequest('Invalid X-Copy-From header')
696             copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
697         return HttpResponse(status=201)
698     
699     meta, permissions, public = get_object_headers(request)
700     content_length = -1
701     if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
702         content_length = get_content_length(request)
703     # Should be BadRequest, but API says otherwise.
704     if 'Content-Type' not in meta:
705         raise LengthRequired('Missing Content-Type header')
706     
707     if request.serialization != 'text':
708         data = ''
709         for block in socket_read_iterator(request, content_length, backend.block_size):
710             data = '%s%s' % (data, block)
711         
712         if request.serialization == 'json':
713             d = json.loads(data)
714             if not hasattr(d, '__getitem__'):
715                 raise BadRequest('Invalid data formating')
716             try:
717                 hashmap = d['hashes']
718                 size = d['bytes']
719             except KeyError:
720                 raise BadRequest('Invalid data formatting')
721         elif request.serialization == 'xml':
722             try:
723                 xml = minidom.parseString(data)
724                 obj = xml.getElementsByTagName('object')[0]
725                 size = obj.attributes['bytes'].value
726                 
727                 hashes = xml.getElementsByTagName('hash')
728                 hashmap = []
729                 for hash in hashes:
730                     hashmap.append(hash.firstChild.data)
731             except Exception:
732                 raise BadRequest('Invalid data formatting')
733         
734         meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
735     else:
736         md5 = hashlib.md5()
737         size = 0
738         hashmap = []
739         for data in socket_read_iterator(request, content_length, backend.block_size):
740             # TODO: Raise 408 (Request Timeout) if this takes too long.
741             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
742             size += len(data)
743             hashmap.append(backend.put_block(data))
744             md5.update(data)
745         
746         meta['hash'] = md5.hexdigest().lower()
747         etag = request.META.get('HTTP_ETAG')
748         if etag and parse_etags(etag)[0].lower() != meta['hash']:
749             raise UnprocessableEntity('Object ETag does not match')
750     
751     try:
752         backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
753     except NotAllowedError:
754         raise Unauthorized('Access denied')
755     except IndexError, e:
756         raise Conflict(json.dumps(e.data))
757     except NameError:
758         raise ItemNotFound('Container does not exist')
759     except ValueError:
760         raise BadRequest('Invalid sharing header')
761     except AttributeError, e:
762         raise Conflict(json.dumps(e.data))
763     if public is not None:
764         try:
765             backend.update_object_public(request.user, v_account, v_container, v_object, public)
766         except NotAllowedError:
767             raise Unauthorized('Access denied')
768         except NameError:
769             raise ItemNotFound('Object does not exist')
770     
771     response = HttpResponse(status=201)
772     response['ETag'] = meta['hash']
773     return response
774
775 @api_method('POST')
776 def object_write_form(request, v_account, v_container, v_object):
777     # Normal Response Codes: 201
778     # Error Response Codes: serviceUnavailable (503),
779     #                       itemNotFound (404),
780     #                       unauthorized (401),
781     #                       badRequest (400)
782     
783     if not request.FILES.has_key('X-Object-Data'):
784         raise BadRequest('Missing X-Object-Data field')
785     file = request.FILES['X-Object-Data']
786     
787     meta = {}
788     meta['Content-Type'] = file.content_type
789     
790     md5 = hashlib.md5()
791     size = 0
792     hashmap = []
793     for data in file.chunks(backend.block_size):
794         size += len(data)
795         hashmap.append(backend.put_block(data))
796         md5.update(data)
797     
798     meta['hash'] = md5.hexdigest().lower()
799     
800     try:
801         backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
802     except NotAllowedError:
803         raise Unauthorized('Access denied')
804     except NameError:
805         raise ItemNotFound('Container does not exist')
806     
807     response = HttpResponse(status=201)
808     response['ETag'] = meta['hash']
809     return response
810
811 @api_method('COPY')
812 def object_copy(request, v_account, v_container, v_object):
813     # Normal Response Codes: 201
814     # Error Response Codes: serviceUnavailable (503),
815     #                       itemNotFound (404),
816     #                       unauthorized (401),
817     #                       badRequest (400)
818     
819     dest_path = request.META.get('HTTP_DESTINATION')
820     if not dest_path:
821         raise BadRequest('Missing Destination header')
822     try:
823         dest_container, dest_name = split_container_object_string(dest_path)
824     except ValueError:
825         raise BadRequest('Invalid Destination header')
826     
827     # Evaluate conditions.
828     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
829         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
830         try:
831             meta = backend.get_object_meta(request.user, v_account, v_container, v_object, src_version)
832         except NotAllowedError:
833             raise Unauthorized('Access denied')
834         except (NameError, IndexError):
835             raise ItemNotFound('Container or object does not exist')
836         validate_matching_preconditions(request, meta)
837     
838     copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
839     return HttpResponse(status=201)
840
841 @api_method('MOVE')
842 def object_move(request, v_account, v_container, v_object):
843     # Normal Response Codes: 201
844     # Error Response Codes: serviceUnavailable (503),
845     #                       itemNotFound (404),
846     #                       unauthorized (401),
847     #                       badRequest (400)
848     
849     dest_path = request.META.get('HTTP_DESTINATION')
850     if not dest_path:
851         raise BadRequest('Missing Destination header')
852     try:
853         dest_container, dest_name = split_container_object_string(dest_path)
854     except ValueError:
855         raise BadRequest('Invalid Destination header')
856     
857     # Evaluate conditions.
858     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
859         try:
860             meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
861         except NotAllowedError:
862             raise Unauthorized('Access denied')
863         except NameError:
864             raise ItemNotFound('Container or object does not exist')
865         validate_matching_preconditions(request, meta)
866     
867     copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
868     return HttpResponse(status=201)
869
870 @api_method('POST')
871 def object_update(request, v_account, v_container, v_object):
872     # Normal Response Codes: 202, 204
873     # Error Response Codes: serviceUnavailable (503),
874     #                       conflict (409),
875     #                       itemNotFound (404),
876     #                       unauthorized (401),
877     #                       badRequest (400)
878     meta, permissions, public = get_object_headers(request)
879     content_type = meta.get('Content-Type')
880     if content_type:
881         del(meta['Content-Type']) # Do not allow changing the Content-Type.
882     
883     try:
884         prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
885     except NotAllowedError:
886         raise Unauthorized('Access denied')
887     except NameError:
888         raise ItemNotFound('Object does not exist')
889     
890     # Evaluate conditions.
891     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
892         validate_matching_preconditions(request, prev_meta)
893     
894     # If replacing, keep previous values of 'Content-Type' and 'hash'.
895     replace = True
896     if 'update' in request.GET:
897         replace = False
898     if replace:
899         for k in ('Content-Type', 'hash'):
900             if k in prev_meta:
901                 meta[k] = prev_meta[k]
902     
903     # A Content-Type or X-Source-Object header indicates data updates.
904     src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
905     if (not content_type or content_type != 'application/octet-stream') and not src_object:
906         # Do permissions first, as it may fail easier.
907         if permissions is not None:
908             try:
909                 backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
910             except NotAllowedError:
911                 raise Unauthorized('Access denied')
912             except NameError:
913                 raise ItemNotFound('Object does not exist')
914             except ValueError:
915                 raise BadRequest('Invalid sharing header')
916             except AttributeError, e:
917                 raise Conflict(json.dumps(e.data))
918         if public is not None:
919             try:
920                 backend.update_object_public(request.user, v_account, v_container, v_object, public)
921             except NotAllowedError:
922                 raise Unauthorized('Access denied')
923             except NameError:
924                 raise ItemNotFound('Object does not exist')
925         try:
926             backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace)
927         except NotAllowedError:
928             raise Unauthorized('Access denied')
929         except NameError:
930             raise ItemNotFound('Object does not exist')
931         return HttpResponse(status=202)
932     
933     # Single range update. Range must be in Content-Range.
934     # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
935     # (with the addition that '*' is allowed for the range - will append).
936     content_range = request.META.get('HTTP_CONTENT_RANGE')
937     if not content_range:
938         raise BadRequest('Missing Content-Range header')
939     ranges = get_content_range(request)
940     if not ranges:
941         raise RangeNotSatisfiable('Invalid Content-Range header')
942     
943     try:
944         size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
945     except NotAllowedError:
946         raise Unauthorized('Access denied')
947     except NameError:
948         raise ItemNotFound('Object does not exist')
949     
950     offset, length, total = ranges
951     if offset is None:
952         offset = size
953     elif offset > size:
954         raise RangeNotSatisfiable('Supplied offset is beyond object limits')
955     if src_object:
956         src_container, src_name = split_container_object_string(src_object)
957         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
958         try:
959             src_size, src_hashmap = backend.get_object_hashmap(request.user, v_account, src_container, src_name, src_version)
960         except NotAllowedError:
961             raise Unauthorized('Access denied')
962         except NameError:
963             raise ItemNotFound('Source object does not exist')
964         
965         if length is None:
966             length = src_size
967         elif length > src_size:
968             raise BadRequest('Object length is smaller than range length')
969     else:
970         # Require either a Content-Length, or 'chunked' Transfer-Encoding.
971         content_length = -1
972         if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
973             content_length = get_content_length(request)
974         
975         if length is None:
976             length = content_length
977         else:
978             if content_length == -1:
979                 # TODO: Get up to length bytes in chunks.
980                 length = content_length
981             elif length != content_length:
982                 raise BadRequest('Content length does not match range length')
983     if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
984         raise RangeNotSatisfiable('Supplied range will change provided object limits')
985     
986     dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
987     if dest_bytes is not None:
988         dest_bytes = get_int_parameter(dest_bytes)
989         if dest_bytes is None:
990             raise BadRequest('Invalid X-Object-Bytes header')
991     
992     if src_object:
993         if offset % backend.block_size == 0:
994             # Update the hashes only.
995             sbi = 0
996             while length > 0:
997                 bi = int(offset / backend.block_size)
998                 bl = min(length, backend.block_size)
999                 if bi < len(hashmap):
1000                     if bl == backend.block_size:
1001                         hashmap[bi] = src_hashmap[sbi]
1002                     else:
1003                         data = backend.get_block(src_hashmap[sbi])
1004                         hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], 0)
1005                 else:
1006                     hashmap.append(src_hashmap[sbi])
1007                 offset += bl
1008                 length -= bl
1009                 sbi += 1
1010         else:
1011             data = ''
1012             sbi = 0
1013             while length > 0:
1014                 data += backend.get_block(src_hashmap[sbi])
1015                 if length < backend.block_size:
1016                     data = data[:length]
1017                 bytes = put_object_block(hashmap, data, offset)
1018                 offset += bytes
1019                 data = data[bytes:]
1020                 length -= bytes
1021                 sbi += 1
1022     else:
1023         data = ''
1024         for d in socket_read_iterator(request, length, backend.block_size):
1025             # TODO: Raise 408 (Request Timeout) if this takes too long.
1026             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1027             data += d
1028             bytes = put_object_block(hashmap, data, offset)
1029             offset += bytes
1030             data = data[bytes:]
1031         if len(data) > 0:
1032             put_object_block(hashmap, data, offset)
1033     
1034     if offset > size:
1035         size = offset
1036     if dest_bytes is not None and dest_bytes < size:
1037         size = dest_bytes
1038         hashmap = hashmap[:(int((size - 1) / backend.block_size) + 1)]
1039     meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
1040     try:
1041         backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, replace, permissions)
1042     except NotAllowedError:
1043         raise Unauthorized('Access denied')
1044     except NameError:
1045         raise ItemNotFound('Container does not exist')
1046     except ValueError:
1047         raise BadRequest('Invalid sharing header')
1048     except AttributeError, e:
1049         raise Conflict(json.dumps(e.data))
1050     if public is not None:
1051         try:
1052             backend.update_object_public(request.user, v_account, v_container, v_object, public)
1053         except NotAllowedError:
1054             raise Unauthorized('Access denied')
1055         except NameError:
1056             raise ItemNotFound('Object does not exist')
1057     
1058     response = HttpResponse(status=204)
1059     response['ETag'] = meta['hash']
1060     return response
1061
1062 @api_method('DELETE')
1063 def object_delete(request, v_account, v_container, v_object):
1064     # Normal Response Codes: 204
1065     # Error Response Codes: serviceUnavailable (503),
1066     #                       itemNotFound (404),
1067     #                       unauthorized (401),
1068     #                       badRequest (400)
1069     
1070     until = get_int_parameter(request.GET.get('until'))
1071     try:
1072         backend.delete_object(request.user, v_account, v_container, v_object, until)
1073     except NotAllowedError:
1074         raise Unauthorized('Access denied')
1075     except NameError:
1076         raise ItemNotFound('Object does not exist')
1077     return HttpResponse(status=204)
1078
1079 @api_method()
1080 def method_not_allowed(request):
1081     raise BadRequest('Method not allowed')