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