Make default authentication return the password as the key.
[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 len(meta) > 0:
336         try:
337             backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
338         except NotAllowedError:
339             raise Unauthorized('Access denied')
340         except NameError:
341             raise ItemNotFound('Container does not exist')
342     
343     return HttpResponse(status=ret)
344
345 @api_method('POST')
346 def container_update(request, v_account, v_container):
347     # Normal Response Codes: 202
348     # Error Response Codes: serviceUnavailable (503),
349     #                       itemNotFound (404),
350     #                       unauthorized (401),
351     #                       badRequest (400)
352     
353     meta, policy = get_container_headers(request)
354     replace = True
355     if 'update' in request.GET:
356         replace = False
357     if policy:
358         try:
359             backend.update_container_policy(request.user, v_account, v_container, policy, replace)
360         except NotAllowedError:
361             raise Unauthorized('Access denied')
362         except NameError:
363             raise ItemNotFound('Container does not exist')
364         except ValueError:
365             raise BadRequest('Invalid policy header')
366     if meta or replace:
367         try:
368             backend.update_container_meta(request.user, v_account, v_container, meta, replace)
369         except NotAllowedError:
370             raise Unauthorized('Access denied')
371         except NameError:
372             raise ItemNotFound('Container does not exist')
373     return HttpResponse(status=202)
374
375 @api_method('DELETE')
376 def container_delete(request, v_account, v_container):
377     # Normal Response Codes: 204
378     # Error Response Codes: serviceUnavailable (503),
379     #                       conflict (409),
380     #                       itemNotFound (404),
381     #                       unauthorized (401),
382     #                       badRequest (400)
383     
384     until = get_int_parameter(request.GET.get('until'))
385     try:
386         backend.delete_container(request.user, v_account, v_container, until)
387     except NotAllowedError:
388         raise Unauthorized('Access denied')
389     except NameError:
390         raise ItemNotFound('Container does not exist')
391     except IndexError:
392         raise Conflict('Container is not empty')
393     return HttpResponse(status=204)
394
395 @api_method('GET', format_allowed=True)
396 def object_list(request, v_account, v_container):
397     # Normal Response Codes: 200, 204
398     # Error Response Codes: serviceUnavailable (503),
399     #                       itemNotFound (404),
400     #                       unauthorized (401),
401     #                       badRequest (400)
402     
403     until = get_int_parameter(request.GET.get('until'))
404     try:
405         meta = backend.get_container_meta(request.user, v_account, v_container, until)
406         meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
407         policy = backend.get_container_policy(request.user, v_account, v_container)
408     except NotAllowedError:
409         raise Unauthorized('Access denied')
410     except NameError:
411         raise ItemNotFound('Container does not exist')
412     
413     validate_modification_preconditions(request, meta)
414     
415     response = HttpResponse()
416     put_container_headers(response, meta, policy)
417     
418     path = request.GET.get('path')
419     prefix = request.GET.get('prefix')
420     delimiter = request.GET.get('delimiter')
421     
422     # Path overrides prefix and delimiter.
423     virtual = True
424     if path:
425         prefix = path
426         delimiter = '/'
427         virtual = False
428     
429     # Naming policy.
430     if prefix and delimiter:
431         prefix = prefix + delimiter
432     if not prefix:
433         prefix = ''
434     prefix = prefix.lstrip('/')
435     
436     marker = request.GET.get('marker')
437     limit = get_int_parameter(request.GET.get('limit'))
438     if not limit:
439         limit = 10000
440     
441     keys = request.GET.get('meta')
442     if keys:
443         keys = keys.split(',')
444         l = [smart_str(x) for x in keys if x.strip() != '']
445         keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
446     else:
447         keys = []
448     
449     shared = False
450     if 'shared' in request.GET:
451         shared = True
452     
453     try:
454         objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
455     except NotAllowedError:
456         raise Unauthorized('Access denied')
457     except NameError:
458         raise ItemNotFound('Container does not exist')
459     
460     if request.serialization == 'text':
461         if len(objects) == 0:
462             # The cloudfiles python bindings expect 200 if json/xml.
463             response.status_code = 204
464             return response
465         response.status_code = 200
466         response.content = '\n'.join([x[0] for x in objects]) + '\n'
467         return response
468     
469     object_meta = []
470     for x in objects:
471         if x[1] is None:
472             # Virtual objects/directories.
473             object_meta.append({'subdir': x[0]})
474         else:
475             try:
476                 meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
477                 if until is None:
478                     permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
479                     public = backend.get_object_public(request.user, v_account, v_container, x[0])
480                 else:
481                     permissions = None
482                     public = None
483             except NotAllowedError:
484                 raise Unauthorized('Access denied')
485             except NameError:
486                 pass
487             else:
488                 rename_meta_key(meta, 'modified', 'last_modified')
489                 rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
490                 rename_meta_key(meta, 'version', 'x_object_version')
491                 rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
492                 update_sharing_meta(permissions, v_account, v_container, x[0], meta)
493                 update_public_meta(public, meta)
494                 object_meta.append(printable_header_dict(meta))
495     if request.serialization == 'xml':
496         data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
497     elif request.serialization  == 'json':
498         data = json.dumps(object_meta)
499     response.status_code = 200
500     response.content = data
501     return response
502
503 @api_method('HEAD')
504 def object_meta(request, v_account, v_container, v_object):
505     # Normal Response Codes: 204
506     # Error Response Codes: serviceUnavailable (503),
507     #                       itemNotFound (404),
508     #                       unauthorized (401),
509     #                       badRequest (400)
510     
511     version = request.GET.get('version')
512     try:
513         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
514         if version is None:
515             permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
516             public = backend.get_object_public(request.user, v_account, v_container, v_object)
517         else:
518             permissions = None
519             public = None
520     except NotAllowedError:
521         raise Unauthorized('Access denied')
522     except NameError:
523         raise ItemNotFound('Object does not exist')
524     except IndexError:
525         raise ItemNotFound('Version does not exist')
526     
527     update_manifest_meta(request, v_account, meta)
528     update_sharing_meta(permissions, v_account, v_container, v_object, meta)
529     update_public_meta(public, meta)
530     
531     # Evaluate conditions.
532     validate_modification_preconditions(request, meta)
533     try:
534         validate_matching_preconditions(request, meta)
535     except NotModified:
536         response = HttpResponse(status=304)
537         response['ETag'] = meta['hash']
538         return response
539     
540     response = HttpResponse(status=200)
541     put_object_headers(response, meta)
542     return response
543
544 @api_method('GET', format_allowed=True)
545 def object_read(request, v_account, v_container, v_object):
546     # Normal Response Codes: 200, 206
547     # Error Response Codes: serviceUnavailable (503),
548     #                       rangeNotSatisfiable (416),
549     #                       preconditionFailed (412),
550     #                       itemNotFound (404),
551     #                       unauthorized (401),
552     #                       badRequest (400),
553     #                       notModified (304)
554     
555     version = request.GET.get('version')
556     
557     # Reply with the version list. Do this first, as the object may be deleted.
558     if version == 'list':
559         if request.serialization == 'text':
560             raise BadRequest('No format specified for version list.')
561         
562         try:
563             v = backend.list_versions(request.user, v_account, v_container, v_object)
564         except NotAllowedError:
565             raise Unauthorized('Access denied')
566         d = {'versions': v}
567         if request.serialization == 'xml':
568             d['object'] = v_object
569             data = render_to_string('versions.xml', d)
570         elif request.serialization  == 'json':
571             data = json.dumps(d)
572         
573         response = HttpResponse(data, status=200)
574         response['Content-Length'] = len(data)
575         return response
576     
577     try:
578         meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
579         if version is None:
580             permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
581             public = backend.get_object_public(request.user, v_account, v_container, v_object)
582         else:
583             permissions = None
584             public = None
585     except NotAllowedError:
586         raise Unauthorized('Access denied')
587     except NameError:
588         raise ItemNotFound('Object does not exist')
589     except IndexError:
590         raise ItemNotFound('Version does not exist')
591     
592     update_manifest_meta(request, v_account, meta)
593     update_sharing_meta(permissions, v_account, v_container, v_object, meta)
594     update_public_meta(public, meta)
595     
596     # Evaluate conditions.
597     validate_modification_preconditions(request, meta)
598     try:
599         validate_matching_preconditions(request, meta)
600     except NotModified:
601         response = HttpResponse(status=304)
602         response['ETag'] = meta['hash']
603         return response
604     
605     sizes = []
606     hashmaps = []
607     if 'X-Object-Manifest' in meta:
608         try:
609             src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
610             objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
611         except NotAllowedError:
612             raise Unauthorized('Access denied')
613         except ValueError:
614             raise BadRequest('Invalid X-Object-Manifest header')
615         except NameError:
616             raise ItemNotFound('Container does not exist')
617         
618         try:
619             for x in objects:
620                 s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
621                 sizes.append(s)
622                 hashmaps.append(h)
623         except NotAllowedError:
624             raise Unauthorized('Access denied')
625         except NameError:
626             raise ItemNotFound('Object does not exist')
627         except IndexError:
628             raise ItemNotFound('Version does not exist')
629     else:
630         try:
631             s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
632             sizes.append(s)
633             hashmaps.append(h)
634         except NotAllowedError:
635             raise Unauthorized('Access denied')
636         except NameError:
637             raise ItemNotFound('Object does not exist')
638         except IndexError:
639             raise ItemNotFound('Version does not exist')
640     
641     # Reply with the hashmap.
642     if request.serialization != 'text':
643         size = sum(sizes)
644         hashmap = sum(hashmaps, [])
645         d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
646         if request.serialization == 'xml':
647             d['object'] = v_object
648             data = render_to_string('hashes.xml', d)
649         elif request.serialization  == 'json':
650             data = json.dumps(d)
651         
652         response = HttpResponse(data, status=200)
653         put_object_headers(response, meta)
654         response['Content-Length'] = len(data)
655         return response
656     
657     return object_data_response(request, sizes, hashmaps, meta)
658
659 @api_method('PUT', format_allowed=True)
660 def object_write(request, v_account, v_container, v_object):
661     # Normal Response Codes: 201
662     # Error Response Codes: serviceUnavailable (503),
663     #                       unprocessableEntity (422),
664     #                       lengthRequired (411),
665     #                       conflict (409),
666     #                       itemNotFound (404),
667     #                       unauthorized (401),
668     #                       badRequest (400)
669     
670     if not request.GET.get('format'):
671         request.serialization = 'text'
672     
673     # Evaluate conditions.
674     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
675         try:
676             meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
677         except NotAllowedError:
678             raise Unauthorized('Access denied')
679         except NameError:
680             meta = {}
681         validate_matching_preconditions(request, meta)
682     
683     copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
684     move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
685     if copy_from or move_from:
686         content_length = get_content_length(request) # Required by the API.
687         
688         if move_from:
689             try:
690                 src_container, src_name = split_container_object_string(move_from)
691             except ValueError:
692                 raise BadRequest('Invalid X-Move-From header')
693             version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
694         else:
695             try:
696                 src_container, src_name = split_container_object_string(copy_from)
697             except ValueError:
698                 raise BadRequest('Invalid X-Copy-From header')
699             version_id = copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
700         response = HttpResponse(status=201)
701         response['X-Object-Version'] = version_id
702         return response
703     
704     meta, permissions, public = get_object_headers(request)
705     content_length = -1
706     if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
707         content_length = get_content_length(request)
708     # Should be BadRequest, but API says otherwise.
709     if 'Content-Type' not in meta:
710         raise LengthRequired('Missing Content-Type header')
711     
712     if request.serialization != 'text':
713         data = ''
714         for block in socket_read_iterator(request, content_length, backend.block_size):
715             data = '%s%s' % (data, block)
716         
717         if request.serialization == 'json':
718             d = json.loads(data)
719             if not hasattr(d, '__getitem__'):
720                 raise BadRequest('Invalid data formating')
721             try:
722                 hashmap = d['hashes']
723                 size = int(d['bytes'])
724             except:
725                 raise BadRequest('Invalid data formatting')
726         elif request.serialization == 'xml':
727             try:
728                 xml = minidom.parseString(data)
729                 obj = xml.getElementsByTagName('object')[0]
730                 size = int(obj.attributes['bytes'].value)
731                 
732                 hashes = xml.getElementsByTagName('hash')
733                 hashmap = []
734                 for hash in hashes:
735                     hashmap.append(hash.firstChild.data)
736             except:
737                 raise BadRequest('Invalid data formatting')
738         
739         meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
740     else:
741         md5 = hashlib.md5()
742         size = 0
743         hashmap = []
744         for data in socket_read_iterator(request, content_length, backend.block_size):
745             # TODO: Raise 408 (Request Timeout) if this takes too long.
746             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
747             size += len(data)
748             hashmap.append(backend.put_block(data))
749             md5.update(data)
750         
751         meta['hash'] = md5.hexdigest().lower()
752         etag = request.META.get('HTTP_ETAG')
753         if etag and parse_etags(etag)[0].lower() != meta['hash']:
754             raise UnprocessableEntity('Object ETag does not match')
755     
756     try:
757         version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
758     except NotAllowedError:
759         raise Unauthorized('Access denied')
760     except IndexError, e:
761         raise Conflict('\n'.join(e.data) + '\n')
762     except NameError:
763         raise ItemNotFound('Container does not exist')
764     except ValueError:
765         raise BadRequest('Invalid sharing header')
766     except AttributeError, e:
767         raise Conflict('\n'.join(e.data) + '\n')
768     if public is not None:
769         try:
770             backend.update_object_public(request.user, v_account, v_container, v_object, public)
771         except NotAllowedError:
772             raise Unauthorized('Access denied')
773         except NameError:
774             raise ItemNotFound('Object does not exist')
775     
776     response = HttpResponse(status=201)
777     response['ETag'] = meta['hash']
778     response['X-Object-Version'] = version_id
779     return response
780
781 @api_method('POST')
782 def object_write_form(request, v_account, v_container, v_object):
783     # Normal Response Codes: 201
784     # Error Response Codes: serviceUnavailable (503),
785     #                       itemNotFound (404),
786     #                       unauthorized (401),
787     #                       badRequest (400)
788     
789     if not request.FILES.has_key('X-Object-Data'):
790         raise BadRequest('Missing X-Object-Data field')
791     file = request.FILES['X-Object-Data']
792     
793     meta = {}
794     meta['Content-Type'] = file.content_type
795     
796     md5 = hashlib.md5()
797     size = 0
798     hashmap = []
799     for data in file.chunks(backend.block_size):
800         size += len(data)
801         hashmap.append(backend.put_block(data))
802         md5.update(data)
803     
804     meta['hash'] = md5.hexdigest().lower()
805     
806     try:
807         version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
808     except NotAllowedError:
809         raise Unauthorized('Access denied')
810     except NameError:
811         raise ItemNotFound('Container does not exist')
812     
813     response = HttpResponse(status=201)
814     response['ETag'] = meta['hash']
815     response['X-Object-Version'] = version_id
816     return response
817
818 @api_method('COPY')
819 def object_copy(request, v_account, v_container, v_object):
820     # Normal Response Codes: 201
821     # Error Response Codes: serviceUnavailable (503),
822     #                       itemNotFound (404),
823     #                       unauthorized (401),
824     #                       badRequest (400)
825     
826     dest_path = request.META.get('HTTP_DESTINATION')
827     if not dest_path:
828         raise BadRequest('Missing Destination header')
829     try:
830         dest_container, dest_name = split_container_object_string(dest_path)
831     except ValueError:
832         raise BadRequest('Invalid Destination header')
833     
834     # Evaluate conditions.
835     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
836         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
837         try:
838             meta = backend.get_object_meta(request.user, v_account, v_container, v_object, src_version)
839         except NotAllowedError:
840             raise Unauthorized('Access denied')
841         except (NameError, IndexError):
842             raise ItemNotFound('Container or object does not exist')
843         validate_matching_preconditions(request, meta)
844     
845     version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
846     response = HttpResponse(status=201)
847     response['X-Object-Version'] = version_id
848     return response
849
850 @api_method('MOVE')
851 def object_move(request, v_account, v_container, v_object):
852     # Normal Response Codes: 201
853     # Error Response Codes: serviceUnavailable (503),
854     #                       itemNotFound (404),
855     #                       unauthorized (401),
856     #                       badRequest (400)
857     
858     dest_path = request.META.get('HTTP_DESTINATION')
859     if not dest_path:
860         raise BadRequest('Missing Destination header')
861     try:
862         dest_container, dest_name = split_container_object_string(dest_path)
863     except ValueError:
864         raise BadRequest('Invalid Destination header')
865     
866     # Evaluate conditions.
867     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
868         try:
869             meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
870         except NotAllowedError:
871             raise Unauthorized('Access denied')
872         except NameError:
873             raise ItemNotFound('Container or object does not exist')
874         validate_matching_preconditions(request, meta)
875     
876     version_id = copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
877     response = HttpResponse(status=201)
878     response['X-Object-Version'] = version_id
879     return response
880
881 @api_method('POST')
882 def object_update(request, v_account, v_container, v_object):
883     # Normal Response Codes: 202, 204
884     # Error Response Codes: serviceUnavailable (503),
885     #                       conflict (409),
886     #                       itemNotFound (404),
887     #                       unauthorized (401),
888     #                       badRequest (400)
889     meta, permissions, public = get_object_headers(request)
890     content_type = meta.get('Content-Type')
891     if content_type:
892         del(meta['Content-Type']) # Do not allow changing the Content-Type.
893     
894     try:
895         prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
896     except NotAllowedError:
897         raise Unauthorized('Access denied')
898     except NameError:
899         raise ItemNotFound('Object does not exist')
900     
901     # Evaluate conditions.
902     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
903         validate_matching_preconditions(request, prev_meta)
904     
905     # If replacing, keep previous values of 'Content-Type' and 'hash'.
906     replace = True
907     if 'update' in request.GET:
908         replace = False
909     if replace:
910         for k in ('Content-Type', 'hash'):
911             if k in prev_meta:
912                 meta[k] = prev_meta[k]
913     
914     # A Content-Type or X-Source-Object header indicates data updates.
915     src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
916     if (not content_type or content_type != 'application/octet-stream') and not src_object:
917         response = HttpResponse(status=202)
918         
919         # Do permissions first, as it may fail easier.
920         if permissions is not None:
921             try:
922                 backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
923             except NotAllowedError:
924                 raise Unauthorized('Access denied')
925             except NameError:
926                 raise ItemNotFound('Object does not exist')
927             except ValueError:
928                 raise BadRequest('Invalid sharing header')
929             except AttributeError, e:
930                 raise Conflict('\n'.join(e.data) + '\n')
931         if public is not None:
932             try:
933                 backend.update_object_public(request.user, v_account, v_container, v_object, public)
934             except NotAllowedError:
935                 raise Unauthorized('Access denied')
936             except NameError:
937                 raise ItemNotFound('Object does not exist')
938         if meta or replace:
939             try:
940                 version_id = backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace)
941             except NotAllowedError:
942                 raise Unauthorized('Access denied')
943             except NameError:
944                 raise ItemNotFound('Object does not exist')        
945             response['X-Object-Version'] = version_id
946         
947         return response
948     
949     # Single range update. Range must be in Content-Range.
950     # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
951     # (with the addition that '*' is allowed for the range - will append).
952     content_range = request.META.get('HTTP_CONTENT_RANGE')
953     if not content_range:
954         raise BadRequest('Missing Content-Range header')
955     ranges = get_content_range(request)
956     if not ranges:
957         raise RangeNotSatisfiable('Invalid Content-Range header')
958     
959     try:
960         size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
961     except NotAllowedError:
962         raise Unauthorized('Access denied')
963     except NameError:
964         raise ItemNotFound('Object does not exist')
965     
966     offset, length, total = ranges
967     if offset is None:
968         offset = size
969     elif offset > size:
970         raise RangeNotSatisfiable('Supplied offset is beyond object limits')
971     if src_object:
972         src_container, src_name = split_container_object_string(src_object)
973         src_container = smart_unicode(src_container, strings_only=True)
974         src_name = smart_unicode(src_name, strings_only=True)
975         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
976         try:
977             src_size, src_hashmap = backend.get_object_hashmap(request.user, v_account, src_container, src_name, src_version)
978         except NotAllowedError:
979             raise Unauthorized('Access denied')
980         except NameError:
981             raise ItemNotFound('Source object does not exist')
982         
983         if length is None:
984             length = src_size
985         elif length > src_size:
986             raise BadRequest('Object length is smaller than range length')
987     else:
988         # Require either a Content-Length, or 'chunked' Transfer-Encoding.
989         content_length = -1
990         if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
991             content_length = get_content_length(request)
992         
993         if length is None:
994             length = content_length
995         else:
996             if content_length == -1:
997                 # TODO: Get up to length bytes in chunks.
998                 length = content_length
999             elif length != content_length:
1000                 raise BadRequest('Content length does not match range length')
1001     if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1002         raise RangeNotSatisfiable('Supplied range will change provided object limits')
1003     
1004     dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1005     if dest_bytes is not None:
1006         dest_bytes = get_int_parameter(dest_bytes)
1007         if dest_bytes is None:
1008             raise BadRequest('Invalid X-Object-Bytes header')
1009     
1010     if src_object:
1011         if offset % backend.block_size == 0:
1012             # Update the hashes only.
1013             sbi = 0
1014             while length > 0:
1015                 bi = int(offset / backend.block_size)
1016                 bl = min(length, backend.block_size)
1017                 if bi < len(hashmap):
1018                     if bl == backend.block_size:
1019                         hashmap[bi] = src_hashmap[sbi]
1020                     else:
1021                         data = backend.get_block(src_hashmap[sbi])
1022                         hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], 0)
1023                 else:
1024                     hashmap.append(src_hashmap[sbi])
1025                 offset += bl
1026                 length -= bl
1027                 sbi += 1
1028         else:
1029             data = ''
1030             sbi = 0
1031             while length > 0:
1032                 data += backend.get_block(src_hashmap[sbi])
1033                 if length < backend.block_size:
1034                     data = data[:length]
1035                 bytes = put_object_block(hashmap, data, offset)
1036                 offset += bytes
1037                 data = data[bytes:]
1038                 length -= bytes
1039                 sbi += 1
1040     else:
1041         data = ''
1042         for d in socket_read_iterator(request, length, backend.block_size):
1043             # TODO: Raise 408 (Request Timeout) if this takes too long.
1044             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1045             data += d
1046             bytes = put_object_block(hashmap, data, offset)
1047             offset += bytes
1048             data = data[bytes:]
1049         if len(data) > 0:
1050             put_object_block(hashmap, data, offset)
1051     
1052     if offset > size:
1053         size = offset
1054     if dest_bytes is not None and dest_bytes < size:
1055         size = dest_bytes
1056         hashmap = hashmap[:(int((size - 1) / backend.block_size) + 1)]
1057     meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
1058     try:
1059         version_id = backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, replace, permissions)
1060     except NotAllowedError:
1061         raise Unauthorized('Access denied')
1062     except NameError:
1063         raise ItemNotFound('Container does not exist')
1064     except ValueError:
1065         raise BadRequest('Invalid sharing header')
1066     except AttributeError, e:
1067         raise Conflict('\n'.join(e.data) + '\n')
1068     if public is not None:
1069         try:
1070             backend.update_object_public(request.user, v_account, v_container, v_object, public)
1071         except NotAllowedError:
1072             raise Unauthorized('Access denied')
1073         except NameError:
1074             raise ItemNotFound('Object does not exist')
1075     
1076     response = HttpResponse(status=204)
1077     response['ETag'] = meta['hash']
1078     response['X-Object-Version'] = version_id
1079     return response
1080
1081 @api_method('DELETE')
1082 def object_delete(request, v_account, v_container, v_object):
1083     # Normal Response Codes: 204
1084     # Error Response Codes: serviceUnavailable (503),
1085     #                       itemNotFound (404),
1086     #                       unauthorized (401),
1087     #                       badRequest (400)
1088     
1089     until = get_int_parameter(request.GET.get('until'))
1090     try:
1091         backend.delete_object(request.user, v_account, v_container, v_object, until)
1092     except NotAllowedError:
1093         raise Unauthorized('Access denied')
1094     except NameError:
1095         raise ItemNotFound('Object does not exist')
1096     return HttpResponse(status=204)
1097
1098 @api_method()
1099 def method_not_allowed(request):
1100     raise BadRequest('Method not allowed')