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