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