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