Statistics
| Branch: | Tag: | Revision:

root / pithos / api / functions.py @ e7b51248

History | View | Annotate | Download (42 kB)

1
# Copyright 2011 GRNET S.A. All rights reserved.
2
# 
3
# Redistribution and use in source and binary forms, with or
4
# without modification, are permitted provided that the following
5
# conditions are met:
6
# 
7
#   1. Redistributions of source code must retain the above
8
#      copyright notice, this list of conditions and the following
9
#      disclaimer.
10
# 
11
#   2. Redistributions in binary form must reproduce the above
12
#      copyright notice, this list of conditions and the following
13
#      disclaimer in the documentation and/or other materials
14
#      provided with the distribution.
15
# 
16
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27
# POSSIBILITY OF SUCH DAMAGE.
28
# 
29
# The views and conclusions contained in the software and
30
# documentation are those of the authors and should not be
31
# interpreted as representing official policies, either expressed
32
# or implied, of GRNET S.A.
33

    
34
import logging
35
import hashlib
36

    
37
from django.conf import settings
38
from django.http import HttpResponse
39
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41
from django.utils.http import parse_etags
42
from django.utils.encoding import smart_unicode, smart_str
43
from xml.dom import minidom
44

    
45
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
46
    LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
47
from pithos.api.util import (rename_meta_key, format_header_key, printable_header_dict, get_account_headers,
48
    put_account_headers, get_container_headers, put_container_headers, get_object_headers, put_object_headers,
49
    update_manifest_meta, update_sharing_meta, update_public_meta, validate_modification_preconditions,
50
    validate_matching_preconditions, split_container_object_string, copy_or_move_object,
51
    get_int_parameter, get_content_length, get_content_range, socket_read_iterator,
52
    object_data_response, put_object_block, hashmap_hash, api_method)
53
from pithos.backends import backend
54
from pithos.backends.base import NotAllowedError
55

    
56

    
57
logger = logging.getLogger(__name__)
58

    
59

    
60
def top_demux(request):
61
    if request.method == 'GET':
62
        if request.user:
63
            return account_list(request)
64
        return authenticate(request)
65
    else:
66
        return method_not_allowed(request)
67

    
68
def account_demux(request, v_account):
69
    if request.method == 'HEAD':
70
        return account_meta(request, v_account)
71
    elif request.method == 'POST':
72
        return account_update(request, v_account)
73
    elif request.method == 'GET':
74
        return container_list(request, v_account)
75
    else:
76
        return method_not_allowed(request)
77

    
78
def container_demux(request, v_account, v_container):
79
    if request.method == 'HEAD':
80
        return container_meta(request, v_account, v_container)
81
    elif request.method == 'PUT':
82
        return container_create(request, v_account, v_container)
83
    elif request.method == 'POST':
84
        return container_update(request, v_account, v_container)
85
    elif request.method == 'DELETE':
86
        return container_delete(request, v_account, v_container)
87
    elif request.method == 'GET':
88
        return object_list(request, v_account, v_container)
89
    else:
90
        return method_not_allowed(request)
91

    
92
def object_demux(request, v_account, v_container, v_object):
93
    if request.method == 'HEAD':
94
        return object_meta(request, v_account, v_container, v_object)
95
    elif request.method == 'GET':
96
        return object_read(request, v_account, v_container, v_object)
97
    elif request.method == 'PUT':
98
        return object_write(request, v_account, v_container, v_object)
99
    elif request.method == 'COPY':
100
        return object_copy(request, v_account, v_container, v_object)
101
    elif request.method == 'MOVE':
102
        return object_move(request, v_account, v_container, v_object)
103
    elif request.method == 'POST':
104
        if request.META.get('CONTENT_TYPE', '').startswith('multipart/form-data'):
105
            return object_write_form(request, v_account, v_container, v_object)
106
        return object_update(request, v_account, v_container, v_object)
107
    elif request.method == 'DELETE':
108
        return object_delete(request, v_account, v_container, v_object)
109
    else:
110
        return method_not_allowed(request)
111

    
112
@api_method('GET')
113
def authenticate(request):
114
    # Normal Response Codes: 204
115
    # Error Response Codes: serviceUnavailable (503),
116
    #                       unauthorized (401),
117
    #                       badRequest (400)
118
    
119
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
120
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
121
    if not x_auth_user or not x_auth_key:
122
        raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
123
    response = HttpResponse(status=204)
124
    
125
    inv_auth_tokens = dict((v, k) for k, v in settings.AUTH_TOKENS.items())
126
    uri = request.build_absolute_uri()
127
    if '?' in uri:
128
        uri = uri[:uri.find('?')]
129
    
130
    response['X-Auth-Token'] = inv_auth_tokens.get(x_auth_user, '0000')
131
    response['X-Storage-Url'] = uri + (uri.endswith('/') and '' or '/') + x_auth_user
132
    return response
133

    
134
@api_method('GET', format_allowed=True)
135
def account_list(request):
136
    # Normal Response Codes: 200, 204
137
    # Error Response Codes: serviceUnavailable (503),
138
    #                       badRequest (400)
139
    
140
    response = HttpResponse()
141
    
142
    marker = request.GET.get('marker')
143
    limit = get_int_parameter(request.GET.get('limit'))
144
    if not limit:
145
        limit = 10000
146
    
147
    accounts = backend.list_accounts(request.user, marker, limit)
148
    
149
    if request.serialization == 'text':
150
        if len(accounts) == 0:
151
            # The cloudfiles python bindings expect 200 if json/xml.
152
            response.status_code = 204
153
            return response
154
        response.status_code = 200
155
        response.content = '\n'.join(accounts) + '\n'
156
        return response
157
    
158
    account_meta = []
159
    for x in accounts:
160
        try:
161
            meta = backend.get_account_meta(request.user, x)
162
            groups = backend.get_account_groups(request.user, x)
163
        except NotAllowedError:
164
            raise Unauthorized('Access denied')
165
        else:
166
            rename_meta_key(meta, 'modified', 'last_modified')
167
            rename_meta_key(meta, 'until_timestamp', 'x_account_until_timestamp')
168
            for k, v in groups.iteritems():
169
                meta['X-Container-Group-' + k] = ','.join(v)
170
            account_meta.append(printable_header_dict(meta))
171
    if request.serialization == 'xml':
172
        data = render_to_string('accounts.xml', {'accounts': account_meta})
173
    elif request.serialization  == 'json':
174
        data = json.dumps(account_meta)
175
    response.status_code = 200
176
    response.content = data
177
    return response
178

    
179
@api_method('HEAD')
180
def account_meta(request, v_account):
181
    # Normal Response Codes: 204
182
    # Error Response Codes: serviceUnavailable (503),
183
    #                       unauthorized (401),
184
    #                       badRequest (400)
185
    
186
    until = get_int_parameter(request.GET.get('until'))
187
    try:
188
        meta = backend.get_account_meta(request.user, v_account, until)
189
        groups = backend.get_account_groups(request.user, v_account)
190
    except NotAllowedError:
191
        raise Unauthorized('Access denied')
192
    
193
    validate_modification_preconditions(request, meta)
194
    
195
    response = HttpResponse(status=204)
196
    put_account_headers(response, meta, groups)
197
    return response
198

    
199
@api_method('POST')
200
def account_update(request, v_account):
201
    # Normal Response Codes: 202
202
    # Error Response Codes: serviceUnavailable (503),
203
    #                       unauthorized (401),
204
    #                       badRequest (400)
205
    
206
    meta, groups = get_account_headers(request)
207
    replace = True
208
    if 'update' in request.GET:
209
        replace = False
210
    if groups:
211
        try:
212
            backend.update_account_groups(request.user, v_account, groups, replace)
213
        except NotAllowedError:
214
            raise Unauthorized('Access denied')
215
        except ValueError:
216
            raise BadRequest('Invalid groups header')
217
    try:
218
        backend.update_account_meta(request.user, v_account, meta, replace)
219
    except NotAllowedError:
220
        raise Unauthorized('Access denied')
221
    return HttpResponse(status=202)
222

    
223
@api_method('GET', format_allowed=True)
224
def container_list(request, v_account):
225
    # Normal Response Codes: 200, 204
226
    # Error Response Codes: serviceUnavailable (503),
227
    #                       itemNotFound (404),
228
    #                       unauthorized (401),
229
    #                       badRequest (400)
230
    
231
    until = get_int_parameter(request.GET.get('until'))
232
    try:
233
        meta = backend.get_account_meta(request.user, v_account, until)
234
        groups = backend.get_account_groups(request.user, v_account)
235
    except NotAllowedError:
236
        raise Unauthorized('Access denied')
237
    
238
    validate_modification_preconditions(request, meta)
239
    
240
    response = HttpResponse()
241
    put_account_headers(response, meta, groups)
242
    
243
    marker = request.GET.get('marker')
244
    limit = get_int_parameter(request.GET.get('limit'))
245
    if not limit:
246
        limit = 10000
247
    
248
    shared = False
249
    if 'shared' in request.GET:
250
        shared = True
251
    
252
    try:
253
        containers = backend.list_containers(request.user, v_account, marker, limit, shared, until)
254
    except NotAllowedError:
255
        raise Unauthorized('Access denied')
256
    except NameError:
257
        containers = []
258
    
259
    if request.serialization == 'text':
260
        if len(containers) == 0:
261
            # The cloudfiles python bindings expect 200 if json/xml.
262
            response.status_code = 204
263
            return response
264
        response.status_code = 200
265
        response.content = '\n'.join(containers) + '\n'
266
        return response
267
    
268
    container_meta = []
269
    for x in containers:
270
        try:
271
            meta = backend.get_container_meta(request.user, v_account, x, until)
272
            policy = backend.get_container_policy(request.user, v_account, x)
273
        except NotAllowedError:
274
            raise Unauthorized('Access denied')
275
        except NameError:
276
            pass
277
        else:
278
            rename_meta_key(meta, 'modified', 'last_modified')
279
            rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
280
            for k, v in policy.iteritems():
281
                meta['X-Container-Policy-' + k] = v
282
            container_meta.append(printable_header_dict(meta))
283
    if request.serialization == 'xml':
284
        data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
285
    elif request.serialization  == 'json':
286
        data = json.dumps(container_meta)
287
    response.status_code = 200
288
    response.content = data
289
    return response
290

    
291
@api_method('HEAD')
292
def container_meta(request, v_account, v_container):
293
    # Normal Response Codes: 204
294
    # Error Response Codes: serviceUnavailable (503),
295
    #                       itemNotFound (404),
296
    #                       unauthorized (401),
297
    #                       badRequest (400)
298
    
299
    until = get_int_parameter(request.GET.get('until'))
300
    try:
301
        meta = backend.get_container_meta(request.user, v_account, v_container, until)
302
        meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
303
        policy = backend.get_container_policy(request.user, v_account, v_container)
304
    except NotAllowedError:
305
        raise Unauthorized('Access denied')
306
    except NameError:
307
        raise ItemNotFound('Container does not exist')
308
    
309
    validate_modification_preconditions(request, meta)
310
    
311
    response = HttpResponse(status=204)
312
    put_container_headers(response, meta, policy)
313
    return response
314

    
315
@api_method('PUT')
316
def container_create(request, v_account, v_container):
317
    # Normal Response Codes: 201, 202
318
    # Error Response Codes: serviceUnavailable (503),
319
    #                       itemNotFound (404),
320
    #                       unauthorized (401),
321
    #                       badRequest (400)
322
    
323
    meta, policy = get_container_headers(request)
324
    
325
    try:
326
        backend.put_container(request.user, v_account, v_container, policy)
327
        ret = 201
328
    except NotAllowedError:
329
        raise Unauthorized('Access denied')
330
    except ValueError:
331
        raise BadRequest('Invalid policy header')
332
    except NameError:
333
        ret = 202
334
    
335
    if len(meta) > 0:
336
        try:
337
            backend.update_container_meta(request.user, v_account, v_container, meta, replace=True)
338
        except NotAllowedError:
339
            raise Unauthorized('Access denied')
340
        except NameError:
341
            raise ItemNotFound('Container does not exist')
342
    
343
    return HttpResponse(status=ret)
344

    
345
@api_method('POST')
346
def container_update(request, v_account, v_container):
347
    # Normal Response Codes: 202
348
    # Error Response Codes: serviceUnavailable (503),
349
    #                       itemNotFound (404),
350
    #                       unauthorized (401),
351
    #                       badRequest (400)
352
    
353
    meta, policy = get_container_headers(request)
354
    replace = True
355
    if 'update' in request.GET:
356
        replace = False
357
    if policy:
358
        try:
359
            backend.update_container_policy(request.user, v_account, v_container, policy, replace)
360
        except NotAllowedError:
361
            raise Unauthorized('Access denied')
362
        except NameError:
363
            raise ItemNotFound('Container does not exist')
364
        except ValueError:
365
            raise BadRequest('Invalid policy header')
366
    try:
367
        backend.update_container_meta(request.user, v_account, v_container, meta, replace)
368
    except NotAllowedError:
369
        raise Unauthorized('Access denied')
370
    except NameError:
371
        raise ItemNotFound('Container does not exist')
372
    return HttpResponse(status=202)
373

    
374
@api_method('DELETE')
375
def container_delete(request, v_account, v_container):
376
    # Normal Response Codes: 204
377
    # Error Response Codes: serviceUnavailable (503),
378
    #                       conflict (409),
379
    #                       itemNotFound (404),
380
    #                       unauthorized (401),
381
    #                       badRequest (400)
382
    
383
    until = get_int_parameter(request.GET.get('until'))
384
    try:
385
        backend.delete_container(request.user, v_account, v_container, until)
386
    except NotAllowedError:
387
        raise Unauthorized('Access denied')
388
    except NameError:
389
        raise ItemNotFound('Container does not exist')
390
    except IndexError:
391
        raise Conflict('Container is not empty')
392
    return HttpResponse(status=204)
393

    
394
@api_method('GET', format_allowed=True)
395
def object_list(request, v_account, v_container):
396
    # Normal Response Codes: 200, 204
397
    # Error Response Codes: serviceUnavailable (503),
398
    #                       itemNotFound (404),
399
    #                       unauthorized (401),
400
    #                       badRequest (400)
401
    
402
    until = get_int_parameter(request.GET.get('until'))
403
    try:
404
        meta = backend.get_container_meta(request.user, v_account, v_container, until)
405
        meta['object_meta'] = backend.list_object_meta(request.user, v_account, v_container, until)
406
        policy = backend.get_container_policy(request.user, v_account, v_container)
407
    except NotAllowedError:
408
        raise Unauthorized('Access denied')
409
    except NameError:
410
        raise ItemNotFound('Container does not exist')
411
    
412
    validate_modification_preconditions(request, meta)
413
    
414
    response = HttpResponse()
415
    put_container_headers(response, meta, policy)
416
    
417
    path = request.GET.get('path')
418
    prefix = request.GET.get('prefix')
419
    delimiter = request.GET.get('delimiter')
420
    
421
    # Path overrides prefix and delimiter.
422
    virtual = True
423
    if path:
424
        prefix = path
425
        delimiter = '/'
426
        virtual = False
427
    
428
    # Naming policy.
429
    if prefix and delimiter:
430
        prefix = prefix + delimiter
431
    if not prefix:
432
        prefix = ''
433
    prefix = prefix.lstrip('/')
434
    
435
    marker = request.GET.get('marker')
436
    limit = get_int_parameter(request.GET.get('limit'))
437
    if not limit:
438
        limit = 10000
439
    
440
    keys = request.GET.get('meta')
441
    if keys:
442
        keys = keys.split(',')
443
        l = [smart_str(x) for x in keys if x.strip() != '']
444
        keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
445
    else:
446
        keys = []
447
    
448
    shared = False
449
    if 'shared' in request.GET:
450
        shared = True
451
    
452
    try:
453
        objects = backend.list_objects(request.user, v_account, v_container, prefix, delimiter, marker, limit, virtual, keys, shared, until)
454
    except NotAllowedError:
455
        raise Unauthorized('Access denied')
456
    except NameError:
457
        raise ItemNotFound('Container does not exist')
458
    
459
    if request.serialization == 'text':
460
        if len(objects) == 0:
461
            # The cloudfiles python bindings expect 200 if json/xml.
462
            response.status_code = 204
463
            return response
464
        response.status_code = 200
465
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
466
        return response
467
    
468
    object_meta = []
469
    for x in objects:
470
        if x[1] is None:
471
            # Virtual objects/directories.
472
            object_meta.append({'subdir': x[0]})
473
        else:
474
            try:
475
                meta = backend.get_object_meta(request.user, v_account, v_container, x[0], x[1])
476
                if until is None:
477
                    permissions = backend.get_object_permissions(request.user, v_account, v_container, x[0])
478
                    public = backend.get_object_public(request.user, v_account, v_container, x[0])
479
                else:
480
                    permissions = None
481
                    public = None
482
            except NotAllowedError:
483
                raise Unauthorized('Access denied')
484
            except NameError:
485
                pass
486
            else:
487
                rename_meta_key(meta, 'modified', 'last_modified')
488
                rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
489
                rename_meta_key(meta, 'version', 'x_object_version')
490
                rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
491
                update_sharing_meta(permissions, v_account, v_container, x[0], meta)
492
                update_public_meta(public, meta)
493
                object_meta.append(printable_header_dict(meta))
494
    if request.serialization == 'xml':
495
        data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
496
    elif request.serialization  == 'json':
497
        data = json.dumps(object_meta)
498
    response.status_code = 200
499
    response.content = data
500
    return response
501

    
502
@api_method('HEAD')
503
def object_meta(request, v_account, v_container, v_object):
504
    # Normal Response Codes: 204
505
    # Error Response Codes: serviceUnavailable (503),
506
    #                       itemNotFound (404),
507
    #                       unauthorized (401),
508
    #                       badRequest (400)
509
    
510
    version = request.GET.get('version')
511
    try:
512
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
513
        if version is None:
514
            permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
515
            public = backend.get_object_public(request.user, v_account, v_container, v_object)
516
        else:
517
            permissions = None
518
            public = None
519
    except NotAllowedError:
520
        raise Unauthorized('Access denied')
521
    except NameError:
522
        raise ItemNotFound('Object does not exist')
523
    except IndexError:
524
        raise ItemNotFound('Version does not exist')
525
    
526
    update_manifest_meta(request, v_account, meta)
527
    update_sharing_meta(permissions, v_account, v_container, v_object, meta)
528
    update_public_meta(public, meta)
529
    
530
    # Evaluate conditions.
531
    validate_modification_preconditions(request, meta)
532
    try:
533
        validate_matching_preconditions(request, meta)
534
    except NotModified:
535
        response = HttpResponse(status=304)
536
        response['ETag'] = meta['hash']
537
        return response
538
    
539
    response = HttpResponse(status=200)
540
    put_object_headers(response, meta)
541
    return response
542

    
543
@api_method('GET', format_allowed=True)
544
def object_read(request, v_account, v_container, v_object):
545
    # Normal Response Codes: 200, 206
546
    # Error Response Codes: serviceUnavailable (503),
547
    #                       rangeNotSatisfiable (416),
548
    #                       preconditionFailed (412),
549
    #                       itemNotFound (404),
550
    #                       unauthorized (401),
551
    #                       badRequest (400),
552
    #                       notModified (304)
553
    
554
    version = request.GET.get('version')
555
    
556
    # Reply with the version list. Do this first, as the object may be deleted.
557
    if version == 'list':
558
        if request.serialization == 'text':
559
            raise BadRequest('No format specified for version list.')
560
        
561
        try:
562
            v = backend.list_versions(request.user, v_account, v_container, v_object)
563
        except NotAllowedError:
564
            raise Unauthorized('Access denied')
565
        d = {'versions': v}
566
        if request.serialization == 'xml':
567
            d['object'] = v_object
568
            data = render_to_string('versions.xml', d)
569
        elif request.serialization  == 'json':
570
            data = json.dumps(d)
571
        
572
        response = HttpResponse(data, status=200)
573
        response['Content-Length'] = len(data)
574
        return response
575
    
576
    try:
577
        meta = backend.get_object_meta(request.user, v_account, v_container, v_object, version)
578
        if version is None:
579
            permissions = backend.get_object_permissions(request.user, v_account, v_container, v_object)
580
            public = backend.get_object_public(request.user, v_account, v_container, v_object)
581
        else:
582
            permissions = None
583
            public = None
584
    except NotAllowedError:
585
        raise Unauthorized('Access denied')
586
    except NameError:
587
        raise ItemNotFound('Object does not exist')
588
    except IndexError:
589
        raise ItemNotFound('Version does not exist')
590
    
591
    update_manifest_meta(request, v_account, meta)
592
    update_sharing_meta(permissions, v_account, v_container, v_object, meta)
593
    update_public_meta(public, meta)
594
    
595
    # Evaluate conditions.
596
    validate_modification_preconditions(request, meta)
597
    try:
598
        validate_matching_preconditions(request, meta)
599
    except NotModified:
600
        response = HttpResponse(status=304)
601
        response['ETag'] = meta['hash']
602
        return response
603
    
604
    sizes = []
605
    hashmaps = []
606
    if 'X-Object-Manifest' in meta:
607
        try:
608
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
609
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
610
        except NotAllowedError:
611
            raise Unauthorized('Access denied')
612
        except ValueError:
613
            raise BadRequest('Invalid X-Object-Manifest header')
614
        except NameError:
615
            raise ItemNotFound('Container does not exist')
616
        
617
        try:
618
            for x in objects:
619
                s, h = backend.get_object_hashmap(request.user, v_account, src_container, x[0], x[1])
620
                sizes.append(s)
621
                hashmaps.append(h)
622
        except NotAllowedError:
623
            raise Unauthorized('Access denied')
624
        except NameError:
625
            raise ItemNotFound('Object does not exist')
626
        except IndexError:
627
            raise ItemNotFound('Version does not exist')
628
    else:
629
        try:
630
            s, h = backend.get_object_hashmap(request.user, v_account, v_container, v_object, version)
631
            sizes.append(s)
632
            hashmaps.append(h)
633
        except NotAllowedError:
634
            raise Unauthorized('Access denied')
635
        except NameError:
636
            raise ItemNotFound('Object does not exist')
637
        except IndexError:
638
            raise ItemNotFound('Version does not exist')
639
    
640
    # Reply with the hashmap.
641
    if request.serialization != 'text':
642
        size = sum(sizes)
643
        hashmap = sum(hashmaps, [])
644
        d = {'block_size': backend.block_size, 'block_hash': backend.hash_algorithm, 'bytes': size, 'hashes': hashmap}
645
        if request.serialization == 'xml':
646
            d['object'] = v_object
647
            data = render_to_string('hashes.xml', d)
648
        elif request.serialization  == 'json':
649
            data = json.dumps(d)
650
        
651
        response = HttpResponse(data, status=200)
652
        put_object_headers(response, meta)
653
        response['Content-Length'] = len(data)
654
        return response
655
    
656
    return object_data_response(request, sizes, hashmaps, meta)
657

    
658
@api_method('PUT', format_allowed=True)
659
def object_write(request, v_account, v_container, v_object):
660
    # Normal Response Codes: 201
661
    # Error Response Codes: serviceUnavailable (503),
662
    #                       unprocessableEntity (422),
663
    #                       lengthRequired (411),
664
    #                       conflict (409),
665
    #                       itemNotFound (404),
666
    #                       unauthorized (401),
667
    #                       badRequest (400)
668
    
669
    if not request.GET.get('format'):
670
        request.serialization = 'text'
671
    
672
    # Evaluate conditions.
673
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
674
        try:
675
            meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
676
        except NotAllowedError:
677
            raise Unauthorized('Access denied')
678
        except NameError:
679
            meta = {}
680
        validate_matching_preconditions(request, meta)
681
    
682
    copy_from = smart_unicode(request.META.get('HTTP_X_COPY_FROM'), strings_only=True)
683
    move_from = smart_unicode(request.META.get('HTTP_X_MOVE_FROM'), strings_only=True)
684
    if copy_from or move_from:
685
        content_length = get_content_length(request) # Required by the API.
686
        
687
        if move_from:
688
            try:
689
                src_container, src_name = split_container_object_string(move_from)
690
            except ValueError:
691
                raise BadRequest('Invalid X-Move-From header')
692
            copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=True)
693
        else:
694
            try:
695
                src_container, src_name = split_container_object_string(copy_from)
696
            except ValueError:
697
                raise BadRequest('Invalid X-Copy-From header')
698
            copy_or_move_object(request, v_account, src_container, src_name, v_container, v_object, move=False)
699
        return HttpResponse(status=201)
700
    
701
    meta, permissions, public = get_object_headers(request)
702
    content_length = -1
703
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
704
        content_length = get_content_length(request)
705
    # Should be BadRequest, but API says otherwise.
706
    if 'Content-Type' not in meta:
707
        raise LengthRequired('Missing Content-Type header')
708
    
709
    if request.serialization != 'text':
710
        data = ''
711
        for block in socket_read_iterator(request, content_length, backend.block_size):
712
            data = '%s%s' % (data, block)
713
        
714
        if request.serialization == 'json':
715
            d = json.loads(data)
716
            if not hasattr(d, '__getitem__'):
717
                raise BadRequest('Invalid data formating')
718
            try:
719
                hashmap = d['hashes']
720
                size = d['bytes']
721
            except KeyError:
722
                raise BadRequest('Invalid data formatting')
723
        elif request.serialization == 'xml':
724
            try:
725
                xml = minidom.parseString(data)
726
                obj = xml.getElementsByTagName('object')[0]
727
                size = obj.attributes['bytes'].value
728
                
729
                hashes = xml.getElementsByTagName('hash')
730
                hashmap = []
731
                for hash in hashes:
732
                    hashmap.append(hash.firstChild.data)
733
            except Exception:
734
                raise BadRequest('Invalid data formatting')
735
        
736
        meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
737
    else:
738
        md5 = hashlib.md5()
739
        size = 0
740
        hashmap = []
741
        for data in socket_read_iterator(request, content_length, backend.block_size):
742
            # TODO: Raise 408 (Request Timeout) if this takes too long.
743
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
744
            size += len(data)
745
            hashmap.append(backend.put_block(data))
746
            md5.update(data)
747
        
748
        meta['hash'] = md5.hexdigest().lower()
749
        etag = request.META.get('HTTP_ETAG')
750
        if etag and parse_etags(etag)[0].lower() != meta['hash']:
751
            raise UnprocessableEntity('Object ETag does not match')
752
    
753
    try:
754
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True, permissions)
755
    except NotAllowedError:
756
        raise Unauthorized('Access denied')
757
    except IndexError, e:
758
        raise Conflict(json.dumps(e.data))
759
    except NameError:
760
        raise ItemNotFound('Container does not exist')
761
    except ValueError:
762
        raise BadRequest('Invalid sharing header')
763
    except AttributeError, e:
764
        raise Conflict(json.dumps(e.data))
765
    if public is not None:
766
        try:
767
            backend.update_object_public(request.user, v_account, v_container, v_object, public)
768
        except NotAllowedError:
769
            raise Unauthorized('Access denied')
770
        except NameError:
771
            raise ItemNotFound('Object does not exist')
772
    
773
    response = HttpResponse(status=201)
774
    response['ETag'] = meta['hash']
775
    return response
776

    
777
@api_method('POST')
778
def object_write_form(request, v_account, v_container, v_object):
779
    # Normal Response Codes: 201
780
    # Error Response Codes: serviceUnavailable (503),
781
    #                       itemNotFound (404),
782
    #                       unauthorized (401),
783
    #                       badRequest (400)
784
    
785
    if not request.FILES.has_key('X-Object-Data'):
786
        raise BadRequest('Missing X-Object-Data field')
787
    file = request.FILES['X-Object-Data']
788
    
789
    meta = {}
790
    meta['Content-Type'] = file.content_type
791
    
792
    md5 = hashlib.md5()
793
    size = 0
794
    hashmap = []
795
    for data in file.chunks(backend.block_size):
796
        size += len(data)
797
        hashmap.append(backend.put_block(data))
798
        md5.update(data)
799
    
800
    meta['hash'] = md5.hexdigest().lower()
801
    
802
    try:
803
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, True)
804
    except NotAllowedError:
805
        raise Unauthorized('Access denied')
806
    except NameError:
807
        raise ItemNotFound('Container does not exist')
808
    
809
    response = HttpResponse(status=201)
810
    response['ETag'] = meta['hash']
811
    return response
812

    
813
@api_method('COPY')
814
def object_copy(request, v_account, v_container, v_object):
815
    # Normal Response Codes: 201
816
    # Error Response Codes: serviceUnavailable (503),
817
    #                       itemNotFound (404),
818
    #                       unauthorized (401),
819
    #                       badRequest (400)
820
    
821
    dest_path = request.META.get('HTTP_DESTINATION')
822
    if not dest_path:
823
        raise BadRequest('Missing Destination header')
824
    try:
825
        dest_container, dest_name = split_container_object_string(dest_path)
826
    except ValueError:
827
        raise BadRequest('Invalid Destination header')
828
    
829
    # Evaluate conditions.
830
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
831
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
832
        try:
833
            meta = backend.get_object_meta(request.user, v_account, v_container, v_object, src_version)
834
        except NotAllowedError:
835
            raise Unauthorized('Access denied')
836
        except (NameError, IndexError):
837
            raise ItemNotFound('Container or object does not exist')
838
        validate_matching_preconditions(request, meta)
839
    
840
    copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=False)
841
    return HttpResponse(status=201)
842

    
843
@api_method('MOVE')
844
def object_move(request, v_account, v_container, v_object):
845
    # Normal Response Codes: 201
846
    # Error Response Codes: serviceUnavailable (503),
847
    #                       itemNotFound (404),
848
    #                       unauthorized (401),
849
    #                       badRequest (400)
850
    
851
    dest_path = request.META.get('HTTP_DESTINATION')
852
    if not dest_path:
853
        raise BadRequest('Missing Destination header')
854
    try:
855
        dest_container, dest_name = split_container_object_string(dest_path)
856
    except ValueError:
857
        raise BadRequest('Invalid Destination header')
858
    
859
    # Evaluate conditions.
860
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
861
        try:
862
            meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
863
        except NotAllowedError:
864
            raise Unauthorized('Access denied')
865
        except NameError:
866
            raise ItemNotFound('Container or object does not exist')
867
        validate_matching_preconditions(request, meta)
868
    
869
    copy_or_move_object(request, v_account, v_container, v_object, dest_container, dest_name, move=True)
870
    return HttpResponse(status=201)
871

    
872
@api_method('POST')
873
def object_update(request, v_account, v_container, v_object):
874
    # Normal Response Codes: 202, 204
875
    # Error Response Codes: serviceUnavailable (503),
876
    #                       conflict (409),
877
    #                       itemNotFound (404),
878
    #                       unauthorized (401),
879
    #                       badRequest (400)
880
    meta, permissions, public = get_object_headers(request)
881
    content_type = meta.get('Content-Type')
882
    if content_type:
883
        del(meta['Content-Type']) # Do not allow changing the Content-Type.
884
    
885
    try:
886
        prev_meta = backend.get_object_meta(request.user, v_account, v_container, v_object)
887
    except NotAllowedError:
888
        raise Unauthorized('Access denied')
889
    except NameError:
890
        raise ItemNotFound('Object does not exist')
891
    
892
    # Evaluate conditions.
893
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
894
        validate_matching_preconditions(request, prev_meta)
895
    
896
    # If replacing, keep previous values of 'Content-Type' and 'hash'.
897
    replace = True
898
    if 'update' in request.GET:
899
        replace = False
900
    if replace:
901
        for k in ('Content-Type', 'hash'):
902
            if k in prev_meta:
903
                meta[k] = prev_meta[k]
904
    
905
    # A Content-Type or X-Source-Object header indicates data updates.
906
    src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
907
    if (not content_type or content_type != 'application/octet-stream') and not src_object:
908
        # Do permissions first, as it may fail easier.
909
        if permissions is not None:
910
            try:
911
                backend.update_object_permissions(request.user, v_account, v_container, v_object, permissions)
912
            except NotAllowedError:
913
                raise Unauthorized('Access denied')
914
            except NameError:
915
                raise ItemNotFound('Object does not exist')
916
            except ValueError:
917
                raise BadRequest('Invalid sharing header')
918
            except AttributeError, e:
919
                raise Conflict(json.dumps(e.data))
920
        if public is not None:
921
            try:
922
                backend.update_object_public(request.user, v_account, v_container, v_object, public)
923
            except NotAllowedError:
924
                raise Unauthorized('Access denied')
925
            except NameError:
926
                raise ItemNotFound('Object does not exist')
927
        try:
928
            backend.update_object_meta(request.user, v_account, v_container, v_object, meta, replace)
929
        except NotAllowedError:
930
            raise Unauthorized('Access denied')
931
        except NameError:
932
            raise ItemNotFound('Object does not exist')
933
        return HttpResponse(status=202)
934
    
935
    # Single range update. Range must be in Content-Range.
936
    # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
937
    # (with the addition that '*' is allowed for the range - will append).
938
    content_range = request.META.get('HTTP_CONTENT_RANGE')
939
    if not content_range:
940
        raise BadRequest('Missing Content-Range header')
941
    ranges = get_content_range(request)
942
    if not ranges:
943
        raise RangeNotSatisfiable('Invalid Content-Range header')
944
    
945
    try:
946
        size, hashmap = backend.get_object_hashmap(request.user, v_account, v_container, v_object)
947
    except NotAllowedError:
948
        raise Unauthorized('Access denied')
949
    except NameError:
950
        raise ItemNotFound('Object does not exist')
951
    
952
    offset, length, total = ranges
953
    if offset is None:
954
        offset = size
955
    elif offset > size:
956
        raise RangeNotSatisfiable('Supplied offset is beyond object limits')
957
    if src_object:
958
        src_container, src_name = split_container_object_string(src_object)
959
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
960
        try:
961
            src_size, src_hashmap = backend.get_object_hashmap(request.user, v_account, src_container, src_name, src_version)
962
        except NotAllowedError:
963
            raise Unauthorized('Access denied')
964
        except NameError:
965
            raise ItemNotFound('Source object does not exist')
966
        
967
        if length is None:
968
            length = src_size
969
        elif length > src_size:
970
            raise BadRequest('Object length is smaller than range length')
971
    else:
972
        # Require either a Content-Length, or 'chunked' Transfer-Encoding.
973
        content_length = -1
974
        if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
975
            content_length = get_content_length(request)
976
        
977
        if length is None:
978
            length = content_length
979
        else:
980
            if content_length == -1:
981
                # TODO: Get up to length bytes in chunks.
982
                length = content_length
983
            elif length != content_length:
984
                raise BadRequest('Content length does not match range length')
985
    if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
986
        raise RangeNotSatisfiable('Supplied range will change provided object limits')
987
    
988
    dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
989
    if dest_bytes is not None:
990
        dest_bytes = get_int_parameter(dest_bytes)
991
        if dest_bytes is None:
992
            raise BadRequest('Invalid X-Object-Bytes header')
993
    
994
    if src_object:
995
        if offset % backend.block_size == 0:
996
            # Update the hashes only.
997
            sbi = 0
998
            while length > 0:
999
                bi = int(offset / backend.block_size)
1000
                bl = min(length, backend.block_size)
1001
                if bi < len(hashmap):
1002
                    if bl == backend.block_size:
1003
                        hashmap[bi] = src_hashmap[sbi]
1004
                    else:
1005
                        data = backend.get_block(src_hashmap[sbi])
1006
                        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], 0)
1007
                else:
1008
                    hashmap.append(src_hashmap[sbi])
1009
                offset += bl
1010
                length -= bl
1011
                sbi += 1
1012
        else:
1013
            data = ''
1014
            sbi = 0
1015
            while length > 0:
1016
                data += backend.get_block(src_hashmap[sbi])
1017
                if length < backend.block_size:
1018
                    data = data[:length]
1019
                bytes = put_object_block(hashmap, data, offset)
1020
                offset += bytes
1021
                data = data[bytes:]
1022
                length -= bytes
1023
                sbi += 1
1024
    else:
1025
        data = ''
1026
        for d in socket_read_iterator(request, length, backend.block_size):
1027
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1028
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1029
            data += d
1030
            bytes = put_object_block(hashmap, data, offset)
1031
            offset += bytes
1032
            data = data[bytes:]
1033
        if len(data) > 0:
1034
            put_object_block(hashmap, data, offset)
1035
    
1036
    if offset > size:
1037
        size = offset
1038
    if dest_bytes is not None and dest_bytes < size:
1039
        size = dest_bytes
1040
        hashmap = hashmap[:(int((size - 1) / backend.block_size) + 1)]
1041
    meta.update({'hash': hashmap_hash(hashmap)}) # Update ETag.
1042
    try:
1043
        backend.update_object_hashmap(request.user, v_account, v_container, v_object, size, hashmap, meta, replace, permissions)
1044
    except NotAllowedError:
1045
        raise Unauthorized('Access denied')
1046
    except NameError:
1047
        raise ItemNotFound('Container does not exist')
1048
    except ValueError:
1049
        raise BadRequest('Invalid sharing header')
1050
    except AttributeError, e:
1051
        raise Conflict(json.dumps(e.data))
1052
    if public is not None:
1053
        try:
1054
            backend.update_object_public(request.user, v_account, v_container, v_object, public)
1055
        except NotAllowedError:
1056
            raise Unauthorized('Access denied')
1057
        except NameError:
1058
            raise ItemNotFound('Object does not exist')
1059
    
1060
    response = HttpResponse(status=204)
1061
    response['ETag'] = meta['hash']
1062
    return response
1063

    
1064
@api_method('DELETE')
1065
def object_delete(request, v_account, v_container, v_object):
1066
    # Normal Response Codes: 204
1067
    # Error Response Codes: serviceUnavailable (503),
1068
    #                       itemNotFound (404),
1069
    #                       unauthorized (401),
1070
    #                       badRequest (400)
1071
    
1072
    until = get_int_parameter(request.GET.get('until'))
1073
    try:
1074
        backend.delete_object(request.user, v_account, v_container, v_object, until)
1075
    except NotAllowedError:
1076
        raise Unauthorized('Access denied')
1077
    except NameError:
1078
        raise ItemNotFound('Object does not exist')
1079
    return HttpResponse(status=204)
1080

    
1081
@api_method()
1082
def method_not_allowed(request):
1083
    raise BadRequest('Method not allowed')