Add backend object retrieve by UUID. Expose UUID at the frontend. Document.
[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_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_hash, 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         
833         meta.update({'ETag': hashmap_hash(request, hashmap)}) # Update ETag.
834     else:
835         md5 = hashlib.md5()
836         size = 0
837         hashmap = []
838         for data in socket_read_iterator(request, content_length,
839                                             request.backend.block_size):
840             # TODO: Raise 408 (Request Timeout) if this takes too long.
841             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
842             size += len(data)
843             hashmap.append(request.backend.put_block(data))
844             md5.update(data)
845         
846         meta['ETag'] = md5.hexdigest().lower()
847         etag = request.META.get('HTTP_ETAG')
848         if etag and parse_etags(etag)[0].lower() != meta['ETag']:
849             raise UnprocessableEntity('Object ETag does not match')
850     
851     try:
852         version_id = request.backend.update_object_hashmap(request.user_uniq,
853                         v_account, v_container, v_object, size, hashmap,
854                         'pithos', meta, True, permissions)
855     except NotAllowedError:
856         raise Forbidden('Not allowed')
857     except IndexError, e:
858         raise Conflict('\n'.join(e.data) + '\n')
859     except NameError:
860         raise ItemNotFound('Container does not exist')
861     except ValueError:
862         raise BadRequest('Invalid sharing header')
863     except AttributeError, e:
864         raise Conflict('\n'.join(e.data) + '\n')
865     except QuotaError:
866         raise RequestEntityTooLarge('Quota exceeded')
867     if public is not None:
868         try:
869             request.backend.update_object_public(request.user_uniq, v_account,
870                                                 v_container, v_object, public)
871         except NotAllowedError:
872             raise Forbidden('Not allowed')
873         except NameError:
874             raise ItemNotFound('Object does not exist')
875     
876     response = HttpResponse(status=201)
877     response['ETag'] = meta['ETag']
878     response['X-Object-Version'] = version_id
879     return response
880
881 @api_method('POST')
882 def object_write_form(request, v_account, v_container, v_object):
883     # Normal Response Codes: 201
884     # Error Response Codes: serviceUnavailable (503),
885     #                       itemNotFound (404),
886     #                       forbidden (403),
887     #                       badRequest (400)
888     
889     request.upload_handlers = [SaveToBackendHandler(request)]
890     if not request.FILES.has_key('X-Object-Data'):
891         raise BadRequest('Missing X-Object-Data field')
892     file = request.FILES['X-Object-Data']
893     
894     meta = {}
895     meta['Content-Type'] = file.content_type
896     meta['ETag'] = file.etag
897     
898     try:
899         version_id = request.backend.update_object_hashmap(request.user_uniq,
900                         v_account, v_container, v_object, file.size, file.hashmap,
901                         'pithos', meta, True)
902     except NotAllowedError:
903         raise Forbidden('Not allowed')
904     except NameError:
905         raise ItemNotFound('Container does not exist')
906     except QuotaError:
907         raise RequestEntityTooLarge('Quota exceeded')
908     
909     response = HttpResponse(status=201)
910     response['ETag'] = meta['ETag']
911     response['X-Object-Version'] = version_id
912     return response
913
914 @api_method('COPY')
915 def object_copy(request, v_account, v_container, v_object):
916     # Normal Response Codes: 201
917     # Error Response Codes: serviceUnavailable (503),
918     #                       itemNotFound (404),
919     #                       forbidden (403),
920     #                       badRequest (400)
921     
922     dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
923     if not dest_account:
924         dest_account = request.user_uniq
925     dest_path = request.META.get('HTTP_DESTINATION')
926     if not dest_path:
927         raise BadRequest('Missing Destination header')
928     try:
929         dest_container, dest_name = split_container_object_string(dest_path)
930     except ValueError:
931         raise BadRequest('Invalid Destination header')
932     
933     # Evaluate conditions.
934     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
935         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
936         try:
937             meta = request.backend.get_object_meta(request.user_uniq, v_account,
938                                             v_container, v_object, 'pithos', src_version)
939         except NotAllowedError:
940             raise Forbidden('Not allowed')
941         except (NameError, IndexError):
942             raise ItemNotFound('Container or object does not exist')
943         validate_matching_preconditions(request, meta)
944     
945     version_id = copy_or_move_object(request, v_account, v_container, v_object,
946                                         dest_account, dest_container, dest_name, move=False)
947     response = HttpResponse(status=201)
948     response['X-Object-Version'] = version_id
949     return response
950
951 @api_method('MOVE')
952 def object_move(request, v_account, v_container, v_object):
953     # Normal Response Codes: 201
954     # Error Response Codes: serviceUnavailable (503),
955     #                       itemNotFound (404),
956     #                       forbidden (403),
957     #                       badRequest (400)
958     
959     dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
960     if not dest_account:
961         dest_account = request.user_uniq
962     dest_path = request.META.get('HTTP_DESTINATION')
963     if not dest_path:
964         raise BadRequest('Missing Destination header')
965     try:
966         dest_container, dest_name = split_container_object_string(dest_path)
967     except ValueError:
968         raise BadRequest('Invalid Destination header')
969     
970     # Evaluate conditions.
971     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
972         try:
973             meta = request.backend.get_object_meta(request.user_uniq, v_account,
974                                                     v_container, v_object, 'pithos')
975         except NotAllowedError:
976             raise Forbidden('Not allowed')
977         except NameError:
978             raise ItemNotFound('Container or object does not exist')
979         validate_matching_preconditions(request, meta)
980     
981     version_id = copy_or_move_object(request, v_account, v_container, v_object,
982                                         dest_account, dest_container, dest_name, move=True)
983     response = HttpResponse(status=201)
984     response['X-Object-Version'] = version_id
985     return response
986
987 @api_method('POST')
988 def object_update(request, v_account, v_container, v_object):
989     # Normal Response Codes: 202, 204
990     # Error Response Codes: serviceUnavailable (503),
991     #                       conflict (409),
992     #                       itemNotFound (404),
993     #                       forbidden (403),
994     #                       badRequest (400)
995     
996     meta, permissions, public = get_object_headers(request)
997     content_type = meta.get('Content-Type')
998     if content_type:
999         del(meta['Content-Type']) # Do not allow changing the Content-Type.
1000     
1001     try:
1002         prev_meta = request.backend.get_object_meta(request.user_uniq, v_account,
1003                                                     v_container, v_object, 'pithos')
1004     except NotAllowedError:
1005         raise Forbidden('Not allowed')
1006     except NameError:
1007         raise ItemNotFound('Object does not exist')
1008     
1009     # Evaluate conditions.
1010     if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1011         validate_matching_preconditions(request, prev_meta)
1012     
1013     # If replacing, keep previous values of 'Content-Type' and 'ETag'.
1014     replace = True
1015     if 'update' in request.GET:
1016         replace = False
1017     if replace:
1018         for k in ('Content-Type', 'ETag'):
1019             if k in prev_meta:
1020                 meta[k] = prev_meta[k]
1021     
1022     # A Content-Type or X-Source-Object header indicates data updates.
1023     src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1024     if (not content_type or content_type != 'application/octet-stream') and not src_object:
1025         response = HttpResponse(status=202)
1026         
1027         # Do permissions first, as it may fail easier.
1028         if permissions is not None:
1029             try:
1030                 request.backend.update_object_permissions(request.user_uniq,
1031                                 v_account, v_container, v_object, permissions)
1032             except NotAllowedError:
1033                 raise Forbidden('Not allowed')
1034             except NameError:
1035                 raise ItemNotFound('Object does not exist')
1036             except ValueError:
1037                 raise BadRequest('Invalid sharing header')
1038             except AttributeError, e:
1039                 raise Conflict('\n'.join(e.data) + '\n')
1040         if public is not None:
1041             try:
1042                 request.backend.update_object_public(request.user_uniq, v_account,
1043                                                 v_container, v_object, public)
1044             except NotAllowedError:
1045                 raise Forbidden('Not allowed')
1046             except NameError:
1047                 raise ItemNotFound('Object does not exist')
1048         if meta or replace:
1049             try:
1050                 version_id = request.backend.update_object_meta(request.user_uniq,
1051                                 v_account, v_container, v_object, 'pithos', meta, replace)
1052             except NotAllowedError:
1053                 raise Forbidden('Not allowed')
1054             except NameError:
1055                 raise ItemNotFound('Object does not exist')        
1056             response['X-Object-Version'] = version_id
1057         
1058         return response
1059     
1060     # Single range update. Range must be in Content-Range.
1061     # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1062     # (with the addition that '*' is allowed for the range - will append).
1063     content_range = request.META.get('HTTP_CONTENT_RANGE')
1064     if not content_range:
1065         raise BadRequest('Missing Content-Range header')
1066     ranges = get_content_range(request)
1067     if not ranges:
1068         raise RangeNotSatisfiable('Invalid Content-Range header')
1069     
1070     try:
1071         size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1072                                             v_account, v_container, v_object)
1073     except NotAllowedError:
1074         raise Forbidden('Not allowed')
1075     except NameError:
1076         raise ItemNotFound('Object does not exist')
1077     
1078     offset, length, total = ranges
1079     if offset is None:
1080         offset = size
1081     elif offset > size:
1082         raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1083     if src_object:
1084         src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1085         if not src_account:
1086             src_account = request.user_uniq
1087         src_container, src_name = split_container_object_string(src_object)
1088         src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1089         try:
1090             src_size, src_hashmap = request.backend.get_object_hashmap(request.user_uniq,
1091                                         src_account, src_container, src_name, src_version)
1092         except NotAllowedError:
1093             raise Forbidden('Not allowed')
1094         except NameError:
1095             raise ItemNotFound('Source object does not exist')
1096         
1097         if length is None:
1098             length = src_size
1099         elif length > src_size:
1100             raise BadRequest('Object length is smaller than range length')
1101     else:
1102         # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1103         content_length = -1
1104         if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1105             content_length = get_content_length(request)
1106         
1107         if length is None:
1108             length = content_length
1109         else:
1110             if content_length == -1:
1111                 # TODO: Get up to length bytes in chunks.
1112                 length = content_length
1113             elif length != content_length:
1114                 raise BadRequest('Content length does not match range length')
1115     if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1116         raise RangeNotSatisfiable('Supplied range will change provided object limits')
1117     
1118     dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1119     if dest_bytes is not None:
1120         dest_bytes = get_int_parameter(dest_bytes)
1121         if dest_bytes is None:
1122             raise BadRequest('Invalid X-Object-Bytes header')
1123     
1124     if src_object:
1125         if offset % request.backend.block_size == 0:
1126             # Update the hashes only.
1127             sbi = 0
1128             while length > 0:
1129                 bi = int(offset / request.backend.block_size)
1130                 bl = min(length, request.backend.block_size)
1131                 if bi < len(hashmap):
1132                     if bl == request.backend.block_size:
1133                         hashmap[bi] = src_hashmap[sbi]
1134                     else:
1135                         data = request.backend.get_block(src_hashmap[sbi])
1136                         hashmap[bi] = request.backend.update_block(hashmap[bi],
1137                                                                 data[:bl], 0)
1138                 else:
1139                     hashmap.append(src_hashmap[sbi])
1140                 offset += bl
1141                 length -= bl
1142                 sbi += 1
1143         else:
1144             data = ''
1145             sbi = 0
1146             while length > 0:
1147                 data += request.backend.get_block(src_hashmap[sbi])
1148                 if length < request.backend.block_size:
1149                     data = data[:length]
1150                 bytes = put_object_block(request, hashmap, data, offset)
1151                 offset += bytes
1152                 data = data[bytes:]
1153                 length -= bytes
1154                 sbi += 1
1155     else:
1156         data = ''
1157         for d in socket_read_iterator(request, length,
1158                                         request.backend.block_size):
1159             # TODO: Raise 408 (Request Timeout) if this takes too long.
1160             # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1161             data += d
1162             bytes = put_object_block(request, hashmap, data, offset)
1163             offset += bytes
1164             data = data[bytes:]
1165         if len(data) > 0:
1166             put_object_block(request, hashmap, data, offset)
1167     
1168     if offset > size:
1169         size = offset
1170     if dest_bytes is not None and dest_bytes < size:
1171         size = dest_bytes
1172         hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1173     meta.update({'ETag': hashmap_hash(request, hashmap)}) # Update ETag.
1174     try:
1175         version_id = request.backend.update_object_hashmap(request.user_uniq,
1176                         v_account, v_container, v_object, size, hashmap,
1177                         'pithos', meta, replace, permissions)
1178     except NotAllowedError:
1179         raise Forbidden('Not allowed')
1180     except NameError:
1181         raise ItemNotFound('Container does not exist')
1182     except ValueError:
1183         raise BadRequest('Invalid sharing header')
1184     except AttributeError, e:
1185         raise Conflict('\n'.join(e.data) + '\n')
1186     except QuotaError:
1187         raise RequestEntityTooLarge('Quota exceeded')
1188     if public is not None:
1189         try:
1190             request.backend.update_object_public(request.user_uniq, v_account,
1191                                                 v_container, v_object, public)
1192         except NotAllowedError:
1193             raise Forbidden('Not allowed')
1194         except NameError:
1195             raise ItemNotFound('Object does not exist')
1196     
1197     response = HttpResponse(status=204)
1198     response['ETag'] = meta['ETag']
1199     response['X-Object-Version'] = version_id
1200     return response
1201
1202 @api_method('DELETE')
1203 def object_delete(request, v_account, v_container, v_object):
1204     # Normal Response Codes: 204
1205     # Error Response Codes: serviceUnavailable (503),
1206     #                       itemNotFound (404),
1207     #                       forbidden (403),
1208     #                       badRequest (400)
1209     
1210     until = get_int_parameter(request.GET.get('until'))
1211     try:
1212         request.backend.delete_object(request.user_uniq, v_account, v_container,
1213                                         v_object, until)
1214     except NotAllowedError:
1215         raise Forbidden('Not allowed')
1216     except NameError:
1217         raise ItemNotFound('Object does not exist')
1218     return HttpResponse(status=204)
1219
1220 @api_method()
1221 def method_not_allowed(request):
1222     raise BadRequest('Method not allowed')