Statistics
| Branch: | Tag: | Revision:

root / pithos / api / functions.py @ 88283e9e

History | View | Annotate | Download (48.9 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_str
43
from xml.dom import minidom
44

    
45
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound, Conflict,
46
    LengthRequired, PreconditionFailed, RequestEntityTooLarge, 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, SaveToBackendHandler,
52
    object_data_response, put_object_block, hashmap_hash, api_method, json_encode_decimal)
53
from pithos.backends.base import NotAllowedError, QuotaError
54

    
55

    
56
logger = logging.getLogger(__name__)
57

    
58

    
59
def top_demux(request):
60
    if request.method == 'GET':
61
        if getattr(request, 'user', None) is not None:
62
            return account_list(request)
63
        return authenticate(request)
64
    else:
65
        return method_not_allowed(request)
66

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

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

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

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

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

    
184
@api_method('HEAD')
185
def account_meta(request, v_account):
186
    # Normal Response Codes: 204
187
    # Error Response Codes: serviceUnavailable (503),
188
    #                       forbidden (403),
189
    #                       badRequest (400)
190
    
191
    until = get_int_parameter(request.GET.get('until'))
192
    try:
193
        meta = request.backend.get_account_meta(request.user_uniq, v_account, until)
194
        groups = request.backend.get_account_groups(request.user_uniq, v_account)
195
        policy = request.backend.get_account_policy(request.user_uniq, v_account)
196
    except NotAllowedError:
197
        raise Forbidden('Not allowed')
198
    
199
    validate_modification_preconditions(request, meta)
200
    
201
    response = HttpResponse(status=204)
202
    put_account_headers(response, meta, groups, policy)
203
    return response
204

    
205
@api_method('POST')
206
def account_update(request, v_account):
207
    # Normal Response Codes: 202
208
    # Error Response Codes: serviceUnavailable (503),
209
    #                       forbidden (403),
210
    #                       badRequest (400)
211
    
212
    meta, groups = get_account_headers(request)
213
    replace = True
214
    if 'update' in request.GET:
215
        replace = False
216
    if groups:
217
        try:
218
            request.backend.update_account_groups(request.user_uniq, v_account,
219
                                                    groups, replace)
220
        except NotAllowedError:
221
            raise Forbidden('Not allowed')
222
        except ValueError:
223
            raise BadRequest('Invalid groups header')
224
    if meta or replace:
225
        try:
226
            request.backend.update_account_meta(request.user_uniq, v_account, meta,
227
                                                replace)
228
        except NotAllowedError:
229
            raise Forbidden('Not allowed')
230
    return HttpResponse(status=202)
231

    
232
@api_method('GET', format_allowed=True)
233
def container_list(request, v_account):
234
    # Normal Response Codes: 200, 204
235
    # Error Response Codes: serviceUnavailable (503),
236
    #                       itemNotFound (404),
237
    #                       forbidden (403),
238
    #                       badRequest (400)
239
    
240
    until = get_int_parameter(request.GET.get('until'))
241
    try:
242
        meta = request.backend.get_account_meta(request.user_uniq, v_account, until)
243
        groups = request.backend.get_account_groups(request.user_uniq, v_account)
244
        policy = request.backend.get_account_policy(request.user_uniq, v_account)
245
    except NotAllowedError:
246
        raise Forbidden('Not allowed')
247
    
248
    validate_modification_preconditions(request, meta)
249
    
250
    response = HttpResponse()
251
    put_account_headers(response, meta, groups, policy)
252
    
253
    marker = request.GET.get('marker')
254
    limit = get_int_parameter(request.GET.get('limit'))
255
    if not limit:
256
        limit = 10000
257
    
258
    shared = False
259
    if 'shared' in request.GET:
260
        shared = True
261
    
262
    try:
263
        containers = request.backend.list_containers(request.user_uniq, v_account,
264
                                                marker, limit, shared, until)
265
    except NotAllowedError:
266
        raise Forbidden('Not allowed')
267
    except NameError:
268
        containers = []
269
    
270
    if request.serialization == 'text':
271
        if len(containers) == 0:
272
            # The cloudfiles python bindings expect 200 if json/xml.
273
            response.status_code = 204
274
            return response
275
        response.status_code = 200
276
        response.content = '\n'.join(containers) + '\n'
277
        return response
278
    
279
    container_meta = []
280
    for x in containers:
281
        try:
282
            meta = request.backend.get_container_meta(request.user_uniq, v_account,
283
                                                        x, until)
284
            policy = request.backend.get_container_policy(request.user_uniq,
285
                                                            v_account, x)
286
        except NotAllowedError:
287
            raise Forbidden('Not allowed')
288
        except NameError:
289
            pass
290
        else:
291
            rename_meta_key(meta, 'modified', 'last_modified')
292
            rename_meta_key(meta, 'until_timestamp', 'x_container_until_timestamp')
293
            m = dict([(k[17:], v) for k, v in meta.iteritems() if k.startswith('X-Container-Meta-')])
294
            for k in m:
295
                del(meta['X-Container-Meta-' + k])
296
            if m:
297
                meta['X-Container-Meta'] = printable_header_dict(m)
298
            if policy:
299
                meta['X-Container-Policy'] = printable_header_dict(dict([(k, v) for k, v in policy.iteritems()]))
300
            container_meta.append(printable_header_dict(meta))
301
    if request.serialization == 'xml':
302
        data = render_to_string('containers.xml', {'account': v_account, 'containers': container_meta})
303
    elif request.serialization  == 'json':
304
        data = json.dumps(container_meta)
305
    response.status_code = 200
306
    response.content = data
307
    return response
308

    
309
@api_method('HEAD')
310
def container_meta(request, v_account, v_container):
311
    # Normal Response Codes: 204
312
    # Error Response Codes: serviceUnavailable (503),
313
    #                       itemNotFound (404),
314
    #                       forbidden (403),
315
    #                       badRequest (400)
316
    
317
    until = get_int_parameter(request.GET.get('until'))
318
    try:
319
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
320
                                                    v_container, until)
321
        meta['object_meta'] = request.backend.list_object_meta(request.user_uniq,
322
                                                v_account, v_container, until)
323
        policy = request.backend.get_container_policy(request.user_uniq, v_account,
324
                                                        v_container)
325
    except NotAllowedError:
326
        raise Forbidden('Not allowed')
327
    except NameError:
328
        raise ItemNotFound('Container does not exist')
329
    
330
    validate_modification_preconditions(request, meta)
331
    
332
    response = HttpResponse(status=204)
333
    put_container_headers(request, response, meta, policy)
334
    return response
335

    
336
@api_method('PUT')
337
def container_create(request, v_account, v_container):
338
    # Normal Response Codes: 201, 202
339
    # Error Response Codes: serviceUnavailable (503),
340
    #                       itemNotFound (404),
341
    #                       forbidden (403),
342
    #                       badRequest (400)
343
    
344
    meta, policy = get_container_headers(request)
345
    
346
    try:
347
        request.backend.put_container(request.user_uniq, v_account, v_container, policy)
348
        ret = 201
349
    except NotAllowedError:
350
        raise Forbidden('Not allowed')
351
    except ValueError:
352
        raise BadRequest('Invalid policy header')
353
    except NameError:
354
        ret = 202
355
    
356
    if ret == 202 and policy:
357
        try:
358
            request.backend.update_container_policy(request.user_uniq, v_account,
359
                                            v_container, policy, replace=False)
360
        except NotAllowedError:
361
            raise Forbidden('Not allowed')
362
        except NameError:
363
            raise ItemNotFound('Container does not exist')
364
        except ValueError:
365
            raise BadRequest('Invalid policy header')
366
    if meta:
367
        try:
368
            request.backend.update_container_meta(request.user_uniq, v_account,
369
                                            v_container, meta, replace=False)
370
        except NotAllowedError:
371
            raise Forbidden('Not allowed')
372
        except NameError:
373
            raise ItemNotFound('Container does not exist')
374
    
375
    return HttpResponse(status=ret)
376

    
377
@api_method('POST')
378
def container_update(request, v_account, v_container):
379
    # Normal Response Codes: 202
380
    # Error Response Codes: serviceUnavailable (503),
381
    #                       itemNotFound (404),
382
    #                       forbidden (403),
383
    #                       badRequest (400)
384
    
385
    meta, policy = get_container_headers(request)
386
    replace = True
387
    if 'update' in request.GET:
388
        replace = False
389
    if policy:
390
        try:
391
            request.backend.update_container_policy(request.user_uniq, v_account,
392
                                                v_container, policy, replace)
393
        except NotAllowedError:
394
            raise Forbidden('Not allowed')
395
        except NameError:
396
            raise ItemNotFound('Container does not exist')
397
        except ValueError:
398
            raise BadRequest('Invalid policy header')
399
    if meta or replace:
400
        try:
401
            request.backend.update_container_meta(request.user_uniq, v_account,
402
                                                    v_container, meta, replace)
403
        except NotAllowedError:
404
            raise Forbidden('Not allowed')
405
        except NameError:
406
            raise ItemNotFound('Container does not exist')
407
    
408
    content_length = -1
409
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
410
        content_length = get_int_parameter(request.META.get('CONTENT_LENGTH', 0))
411
    content_type = request.META.get('CONTENT_TYPE')
412
    hashmap = []
413
    if content_type and content_type == 'application/octet-stream' and content_length != 0:
414
        for data in socket_read_iterator(request, content_length,
415
                                            request.backend.block_size):
416
            # TODO: Raise 408 (Request Timeout) if this takes too long.
417
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
418
            hashmap.append(request.backend.put_block(data))
419
    
420
    response = HttpResponse(status=202)
421
    if hashmap:
422
        response.content = '\n'.join(hashmap) + '\n'
423
    return response
424

    
425
@api_method('DELETE')
426
def container_delete(request, v_account, v_container):
427
    # Normal Response Codes: 204
428
    # Error Response Codes: serviceUnavailable (503),
429
    #                       conflict (409),
430
    #                       itemNotFound (404),
431
    #                       forbidden (403),
432
    #                       badRequest (400)
433
    
434
    until = get_int_parameter(request.GET.get('until'))
435
    try:
436
        request.backend.delete_container(request.user_uniq, v_account, v_container,
437
                                            until)
438
    except NotAllowedError:
439
        raise Forbidden('Not allowed')
440
    except NameError:
441
        raise ItemNotFound('Container does not exist')
442
    except IndexError:
443
        raise Conflict('Container is not empty')
444
    return HttpResponse(status=204)
445

    
446
@api_method('GET', format_allowed=True)
447
def object_list(request, v_account, v_container):
448
    # Normal Response Codes: 200, 204
449
    # Error Response Codes: serviceUnavailable (503),
450
    #                       itemNotFound (404),
451
    #                       forbidden (403),
452
    #                       badRequest (400)
453
    
454
    until = get_int_parameter(request.GET.get('until'))
455
    try:
456
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
457
                                                    v_container, until)
458
        meta['object_meta'] = request.backend.list_object_meta(request.user_uniq,
459
                                                v_account, v_container, until)
460
        policy = request.backend.get_container_policy(request.user_uniq, v_account,
461
                                                        v_container)
462
    except NotAllowedError:
463
        raise Forbidden('Not allowed')
464
    except NameError:
465
        raise ItemNotFound('Container does not exist')
466
    
467
    validate_modification_preconditions(request, meta)
468
    
469
    response = HttpResponse()
470
    put_container_headers(request, response, meta, policy)
471
    
472
    path = request.GET.get('path')
473
    prefix = request.GET.get('prefix')
474
    delimiter = request.GET.get('delimiter')
475
    
476
    # Path overrides prefix and delimiter.
477
    virtual = True
478
    if path:
479
        prefix = path
480
        delimiter = '/'
481
        virtual = False
482
    
483
    # Naming policy.
484
    if prefix and delimiter:
485
        prefix = prefix + delimiter
486
    if not prefix:
487
        prefix = ''
488
    prefix = prefix.lstrip('/')
489
    
490
    marker = request.GET.get('marker')
491
    limit = get_int_parameter(request.GET.get('limit'))
492
    if not limit:
493
        limit = 10000
494
    
495
    keys = request.GET.get('meta')
496
    if keys:
497
        keys = keys.split(',')
498
        l = [smart_str(x) for x in keys if x.strip() != '']
499
        keys = [format_header_key('X-Object-Meta-' + x.strip()) for x in l]
500
    else:
501
        keys = []
502
    
503
    shared = False
504
    if 'shared' in request.GET:
505
        shared = True
506
    
507
    try:
508
        objects = request.backend.list_objects(request.user_uniq, v_account,
509
                                    v_container, prefix, delimiter, marker,
510
                                    limit, virtual, keys, shared, until)
511
    except NotAllowedError:
512
        raise Forbidden('Not allowed')
513
    except NameError:
514
        raise ItemNotFound('Container does not exist')
515
    
516
    if request.serialization == 'text':
517
        if len(objects) == 0:
518
            # The cloudfiles python bindings expect 200 if json/xml.
519
            response.status_code = 204
520
            return response
521
        response.status_code = 200
522
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
523
        return response
524
    
525
    object_meta = []
526
    for x in objects:
527
        if x[1] is None:
528
            # Virtual objects/directories.
529
            object_meta.append({'subdir': x[0]})
530
        else:
531
            try:
532
                meta = request.backend.get_object_meta(request.user_uniq, v_account,
533
                                                        v_container, x[0], x[1])
534
                if until is None:
535
                    permissions = request.backend.get_object_permissions(
536
                                    request.user_uniq, v_account, v_container, x[0])
537
                    public = request.backend.get_object_public(request.user_uniq,
538
                                                v_account, v_container, x[0])
539
                else:
540
                    permissions = None
541
                    public = None
542
            except NotAllowedError:
543
                raise Forbidden('Not allowed')
544
            except NameError:
545
                pass
546
            else:
547
                rename_meta_key(meta, 'hash', 'x_object_hash') # Will be replaced by ETag.
548
                rename_meta_key(meta, 'ETag', 'hash')
549
                rename_meta_key(meta, 'modified', 'last_modified')
550
                rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
551
                rename_meta_key(meta, 'version', 'x_object_version')
552
                rename_meta_key(meta, 'version_timestamp', 'x_object_version_timestamp')
553
                m = dict([(k[14:], v) for k, v in meta.iteritems() if k.startswith('X-Object-Meta-')])
554
                for k in m:
555
                    del(meta['X-Object-Meta-' + k])
556
                if m:
557
                    meta['X-Object-Meta'] = printable_header_dict(m)
558
                update_sharing_meta(request, permissions, v_account, v_container, x[0], meta)
559
                update_public_meta(public, meta)
560
                object_meta.append(printable_header_dict(meta))
561
    if request.serialization == 'xml':
562
        data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
563
    elif request.serialization  == 'json':
564
        data = json.dumps(object_meta, default=json_encode_decimal)
565
    response.status_code = 200
566
    response.content = data
567
    return response
568

    
569
@api_method('HEAD')
570
def object_meta(request, v_account, v_container, v_object):
571
    # Normal Response Codes: 204
572
    # Error Response Codes: serviceUnavailable (503),
573
    #                       itemNotFound (404),
574
    #                       forbidden (403),
575
    #                       badRequest (400)
576
    
577
    version = request.GET.get('version')
578
    try:
579
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
580
                                                v_container, v_object, version)
581
        if version is None:
582
            permissions = request.backend.get_object_permissions(request.user_uniq,
583
                                            v_account, v_container, v_object)
584
            public = request.backend.get_object_public(request.user_uniq, v_account,
585
                                                        v_container, v_object)
586
        else:
587
            permissions = None
588
            public = None
589
    except NotAllowedError:
590
        raise Forbidden('Not allowed')
591
    except NameError:
592
        raise ItemNotFound('Object does not exist')
593
    except IndexError:
594
        raise ItemNotFound('Version does not exist')
595
    
596
    update_manifest_meta(request, v_account, meta)
597
    update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
598
    update_public_meta(public, meta)
599
    
600
    # Evaluate conditions.
601
    validate_modification_preconditions(request, meta)
602
    try:
603
        validate_matching_preconditions(request, meta)
604
    except NotModified:
605
        response = HttpResponse(status=304)
606
        response['ETag'] = meta['ETag']
607
        return response
608
    
609
    response = HttpResponse(status=200)
610
    put_object_headers(response, meta)
611
    return response
612

    
613
@api_method('GET', format_allowed=True)
614
def object_read(request, v_account, v_container, v_object):
615
    # Normal Response Codes: 200, 206
616
    # Error Response Codes: serviceUnavailable (503),
617
    #                       rangeNotSatisfiable (416),
618
    #                       preconditionFailed (412),
619
    #                       itemNotFound (404),
620
    #                       forbidden (403),
621
    #                       badRequest (400),
622
    #                       notModified (304)
623
    
624
    version = request.GET.get('version')
625
    
626
    # Reply with the version list. Do this first, as the object may be deleted.
627
    if version == 'list':
628
        if request.serialization == 'text':
629
            raise BadRequest('No format specified for version list.')
630
        
631
        try:
632
            v = request.backend.list_versions(request.user_uniq, v_account,
633
                                                v_container, v_object)
634
        except NotAllowedError:
635
            raise Forbidden('Not allowed')
636
        d = {'versions': v}
637
        if request.serialization == 'xml':
638
            d['object'] = v_object
639
            data = render_to_string('versions.xml', d)
640
        elif request.serialization  == 'json':
641
            data = json.dumps(d, default=json_encode_decimal)
642
        
643
        response = HttpResponse(data, status=200)
644
        response['Content-Length'] = len(data)
645
        return response
646
    
647
    try:
648
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
649
                                                v_container, v_object, version)
650
        if version is None:
651
            permissions = request.backend.get_object_permissions(request.user_uniq,
652
                                            v_account, v_container, v_object)
653
            public = request.backend.get_object_public(request.user_uniq, v_account,
654
                                                        v_container, v_object)
655
        else:
656
            permissions = None
657
            public = None
658
    except NotAllowedError:
659
        raise Forbidden('Not allowed')
660
    except NameError:
661
        raise ItemNotFound('Object does not exist')
662
    except IndexError:
663
        raise ItemNotFound('Version does not exist')
664
    
665
    update_manifest_meta(request, v_account, meta)
666
    update_sharing_meta(request, permissions, v_account, v_container, v_object, meta)
667
    update_public_meta(public, meta)
668
    
669
    # Evaluate conditions.
670
    validate_modification_preconditions(request, meta)
671
    try:
672
        validate_matching_preconditions(request, meta)
673
    except NotModified:
674
        response = HttpResponse(status=304)
675
        response['ETag'] = meta['ETag']
676
        return response
677
    
678
    sizes = []
679
    hashmaps = []
680
    if 'X-Object-Manifest' in meta:
681
        try:
682
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
683
            objects = request.backend.list_objects(request.user_uniq, v_account,
684
                                src_container, prefix=src_name, virtual=False)
685
        except NotAllowedError:
686
            raise Forbidden('Not allowed')
687
        except ValueError:
688
            raise BadRequest('Invalid X-Object-Manifest header')
689
        except NameError:
690
            raise ItemNotFound('Container does not exist')
691
        
692
        try:
693
            for x in objects:
694
                s, h = request.backend.get_object_hashmap(request.user_uniq,
695
                                        v_account, src_container, x[0], x[1])
696
                sizes.append(s)
697
                hashmaps.append(h)
698
        except NotAllowedError:
699
            raise Forbidden('Not allowed')
700
        except NameError:
701
            raise ItemNotFound('Object does not exist')
702
        except IndexError:
703
            raise ItemNotFound('Version does not exist')
704
    else:
705
        try:
706
            s, h = request.backend.get_object_hashmap(request.user_uniq, v_account,
707
                                                v_container, v_object, version)
708
            sizes.append(s)
709
            hashmaps.append(h)
710
        except NotAllowedError:
711
            raise Forbidden('Not allowed')
712
        except NameError:
713
            raise ItemNotFound('Object does not exist')
714
        except IndexError:
715
            raise ItemNotFound('Version does not exist')
716
    
717
    # Reply with the hashmap.
718
    if 'hashmap' in request.GET and request.serialization != 'text':
719
        size = sum(sizes)
720
        hashmap = sum(hashmaps, [])
721
        d = {
722
            'block_size': request.backend.block_size,
723
            'block_hash': request.backend.hash_algorithm,
724
            'bytes': size,
725
            'hashes': hashmap}
726
        if request.serialization == 'xml':
727
            d['object'] = v_object
728
            data = render_to_string('hashes.xml', d)
729
        elif request.serialization  == 'json':
730
            data = json.dumps(d)
731
        
732
        response = HttpResponse(data, status=200)
733
        put_object_headers(response, meta)
734
        response['Content-Length'] = len(data)
735
        return response
736
    
737
    request.serialization = 'text' # Unset.
738
    return object_data_response(request, sizes, hashmaps, meta)
739

    
740
@api_method('PUT', format_allowed=True)
741
def object_write(request, v_account, v_container, v_object):
742
    # Normal Response Codes: 201
743
    # Error Response Codes: serviceUnavailable (503),
744
    #                       unprocessableEntity (422),
745
    #                       lengthRequired (411),
746
    #                       conflict (409),
747
    #                       itemNotFound (404),
748
    #                       forbidden (403),
749
    #                       badRequest (400)
750
    
751
    # Evaluate conditions.
752
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
753
        try:
754
            meta = request.backend.get_object_meta(request.user_uniq, v_account,
755
                                                        v_container, v_object)
756
        except NotAllowedError:
757
            raise Forbidden('Not allowed')
758
        except NameError:
759
            meta = {}
760
        validate_matching_preconditions(request, meta)
761
    
762
    copy_from = request.META.get('HTTP_X_COPY_FROM')
763
    move_from = request.META.get('HTTP_X_MOVE_FROM')
764
    if copy_from or move_from:
765
        content_length = get_content_length(request) # Required by the API.
766
        
767
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
768
        if not src_account:
769
            src_account = request.user_uniq
770
        if move_from:
771
            try:
772
                src_container, src_name = split_container_object_string(move_from)
773
            except ValueError:
774
                raise BadRequest('Invalid X-Move-From header')
775
            version_id = copy_or_move_object(request, src_account, src_container, src_name,
776
                                                v_account, v_container, v_object, move=True)
777
        else:
778
            try:
779
                src_container, src_name = split_container_object_string(copy_from)
780
            except ValueError:
781
                raise BadRequest('Invalid X-Copy-From header')
782
            version_id = copy_or_move_object(request, src_account, src_container, src_name,
783
                                                v_account, v_container, v_object, move=False)
784
        response = HttpResponse(status=201)
785
        response['X-Object-Version'] = version_id
786
        return response
787
    
788
    meta, permissions, public = get_object_headers(request)
789
    content_length = -1
790
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
791
        content_length = get_content_length(request)
792
    # Should be BadRequest, but API says otherwise.
793
    if 'Content-Type' not in meta:
794
        raise LengthRequired('Missing Content-Type header')
795
    
796
    if 'hashmap' in request.GET:
797
        if request.serialization not in ('json', 'xml'):
798
            raise BadRequest('Invalid hashmap format')
799
        
800
        data = ''
801
        for block in socket_read_iterator(request, content_length,
802
                                            request.backend.block_size):
803
            data = '%s%s' % (data, block)
804
        
805
        if request.serialization == 'json':
806
            d = json.loads(data)
807
            if not hasattr(d, '__getitem__'):
808
                raise BadRequest('Invalid data formating')
809
            try:
810
                hashmap = d['hashes']
811
                size = int(d['bytes'])
812
            except:
813
                raise BadRequest('Invalid data formatting')
814
        elif request.serialization == 'xml':
815
            try:
816
                xml = minidom.parseString(data)
817
                obj = xml.getElementsByTagName('object')[0]
818
                size = int(obj.attributes['bytes'].value)
819
                
820
                hashes = xml.getElementsByTagName('hash')
821
                hashmap = []
822
                for hash in hashes:
823
                    hashmap.append(hash.firstChild.data)
824
            except:
825
                raise BadRequest('Invalid data formatting')
826
        
827
        meta.update({'ETag': hashmap_hash(request, hashmap)}) # Update ETag.
828
    else:
829
        md5 = hashlib.md5()
830
        size = 0
831
        hashmap = []
832
        for data in socket_read_iterator(request, content_length,
833
                                            request.backend.block_size):
834
            # TODO: Raise 408 (Request Timeout) if this takes too long.
835
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
836
            size += len(data)
837
            hashmap.append(request.backend.put_block(data))
838
            md5.update(data)
839
        
840
        meta['ETag'] = md5.hexdigest().lower()
841
        etag = request.META.get('HTTP_ETAG')
842
        if etag and parse_etags(etag)[0].lower() != meta['ETag']:
843
            raise UnprocessableEntity('Object ETag does not match')
844
    
845
    try:
846
        version_id = request.backend.update_object_hashmap(request.user_uniq,
847
                        v_account, v_container, v_object, size, hashmap, meta,
848
                        True, permissions)
849
    except NotAllowedError:
850
        raise Forbidden('Not allowed')
851
    except IndexError, e:
852
        raise Conflict('\n'.join(e.data) + '\n')
853
    except NameError:
854
        raise ItemNotFound('Container does not exist')
855
    except ValueError:
856
        raise BadRequest('Invalid sharing header')
857
    except AttributeError, e:
858
        raise Conflict('\n'.join(e.data) + '\n')
859
    except QuotaError:
860
        raise RequestEntityTooLarge('Quota exceeded')
861
    if public is not None:
862
        try:
863
            request.backend.update_object_public(request.user_uniq, v_account,
864
                                                v_container, v_object, public)
865
        except NotAllowedError:
866
            raise Forbidden('Not allowed')
867
        except NameError:
868
            raise ItemNotFound('Object does not exist')
869
    
870
    response = HttpResponse(status=201)
871
    response['ETag'] = meta['ETag']
872
    response['X-Object-Version'] = version_id
873
    return response
874

    
875
@api_method('POST')
876
def object_write_form(request, v_account, v_container, v_object):
877
    # Normal Response Codes: 201
878
    # Error Response Codes: serviceUnavailable (503),
879
    #                       itemNotFound (404),
880
    #                       forbidden (403),
881
    #                       badRequest (400)
882
    
883
    request.upload_handlers = [SaveToBackendHandler(request)]
884
    if not request.FILES.has_key('X-Object-Data'):
885
        raise BadRequest('Missing X-Object-Data field')
886
    file = request.FILES['X-Object-Data']
887
    
888
    meta = {}
889
    meta['Content-Type'] = file.content_type
890
    meta['ETag'] = file.etag
891
    
892
    try:
893
        version_id = request.backend.update_object_hashmap(request.user_uniq,
894
                        v_account, v_container, v_object, file.size, file.hashmap, meta, True)
895
    except NotAllowedError:
896
        raise Forbidden('Not allowed')
897
    except NameError:
898
        raise ItemNotFound('Container does not exist')
899
    except QuotaError:
900
        raise RequestEntityTooLarge('Quota exceeded')
901
    
902
    response = HttpResponse(status=201)
903
    response['ETag'] = meta['ETag']
904
    response['X-Object-Version'] = version_id
905
    return response
906

    
907
@api_method('COPY')
908
def object_copy(request, v_account, v_container, v_object):
909
    # Normal Response Codes: 201
910
    # Error Response Codes: serviceUnavailable (503),
911
    #                       itemNotFound (404),
912
    #                       forbidden (403),
913
    #                       badRequest (400)
914
    
915
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
916
    if not dest_account:
917
        dest_account = request.user_uniq
918
    dest_path = request.META.get('HTTP_DESTINATION')
919
    if not dest_path:
920
        raise BadRequest('Missing Destination header')
921
    try:
922
        dest_container, dest_name = split_container_object_string(dest_path)
923
    except ValueError:
924
        raise BadRequest('Invalid Destination header')
925
    
926
    # Evaluate conditions.
927
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
928
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
929
        try:
930
            meta = request.backend.get_object_meta(request.user_uniq, v_account,
931
                                            v_container, v_object, src_version)
932
        except NotAllowedError:
933
            raise Forbidden('Not allowed')
934
        except (NameError, IndexError):
935
            raise ItemNotFound('Container or object does not exist')
936
        validate_matching_preconditions(request, meta)
937
    
938
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
939
                                        dest_account, dest_container, dest_name, move=False)
940
    response = HttpResponse(status=201)
941
    response['X-Object-Version'] = version_id
942
    return response
943

    
944
@api_method('MOVE')
945
def object_move(request, v_account, v_container, v_object):
946
    # Normal Response Codes: 201
947
    # Error Response Codes: serviceUnavailable (503),
948
    #                       itemNotFound (404),
949
    #                       forbidden (403),
950
    #                       badRequest (400)
951
    
952
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
953
    if not dest_account:
954
        dest_account = request.user_uniq
955
    dest_path = request.META.get('HTTP_DESTINATION')
956
    if not dest_path:
957
        raise BadRequest('Missing Destination header')
958
    try:
959
        dest_container, dest_name = split_container_object_string(dest_path)
960
    except ValueError:
961
        raise BadRequest('Invalid Destination header')
962
    
963
    # Evaluate conditions.
964
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
965
        try:
966
            meta = request.backend.get_object_meta(request.user_uniq, v_account,
967
                                                    v_container, v_object)
968
        except NotAllowedError:
969
            raise Forbidden('Not allowed')
970
        except NameError:
971
            raise ItemNotFound('Container or object does not exist')
972
        validate_matching_preconditions(request, meta)
973
    
974
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
975
                                        dest_account, dest_container, dest_name, move=True)
976
    response = HttpResponse(status=201)
977
    response['X-Object-Version'] = version_id
978
    return response
979

    
980
@api_method('POST')
981
def object_update(request, v_account, v_container, v_object):
982
    # Normal Response Codes: 202, 204
983
    # Error Response Codes: serviceUnavailable (503),
984
    #                       conflict (409),
985
    #                       itemNotFound (404),
986
    #                       forbidden (403),
987
    #                       badRequest (400)
988
    meta, permissions, public = get_object_headers(request)
989
    content_type = meta.get('Content-Type')
990
    if content_type:
991
        del(meta['Content-Type']) # Do not allow changing the Content-Type.
992
    
993
    try:
994
        prev_meta = request.backend.get_object_meta(request.user_uniq, v_account,
995
                                                    v_container, v_object)
996
    except NotAllowedError:
997
        raise Forbidden('Not allowed')
998
    except NameError:
999
        raise ItemNotFound('Object does not exist')
1000
    
1001
    # Evaluate conditions.
1002
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1003
        validate_matching_preconditions(request, prev_meta)
1004
    
1005
    # If replacing, keep previous values of 'Content-Type' and 'ETag'.
1006
    replace = True
1007
    if 'update' in request.GET:
1008
        replace = False
1009
    if replace:
1010
        for k in ('Content-Type', 'ETag'):
1011
            if k in prev_meta:
1012
                meta[k] = prev_meta[k]
1013
    
1014
    # A Content-Type or X-Source-Object header indicates data updates.
1015
    src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1016
    if (not content_type or content_type != 'application/octet-stream') and not src_object:
1017
        response = HttpResponse(status=202)
1018
        
1019
        # Do permissions first, as it may fail easier.
1020
        if permissions is not None:
1021
            try:
1022
                request.backend.update_object_permissions(request.user_uniq,
1023
                                v_account, v_container, v_object, permissions)
1024
            except NotAllowedError:
1025
                raise Forbidden('Not allowed')
1026
            except NameError:
1027
                raise ItemNotFound('Object does not exist')
1028
            except ValueError:
1029
                raise BadRequest('Invalid sharing header')
1030
            except AttributeError, e:
1031
                raise Conflict('\n'.join(e.data) + '\n')
1032
        if public is not None:
1033
            try:
1034
                request.backend.update_object_public(request.user_uniq, v_account,
1035
                                                v_container, v_object, public)
1036
            except NotAllowedError:
1037
                raise Forbidden('Not allowed')
1038
            except NameError:
1039
                raise ItemNotFound('Object does not exist')
1040
        if meta or replace:
1041
            try:
1042
                version_id = request.backend.update_object_meta(request.user_uniq,
1043
                                v_account, v_container, v_object, meta, replace)
1044
            except NotAllowedError:
1045
                raise Forbidden('Not allowed')
1046
            except NameError:
1047
                raise ItemNotFound('Object does not exist')        
1048
            response['X-Object-Version'] = version_id
1049
        
1050
        return response
1051
    
1052
    # Single range update. Range must be in Content-Range.
1053
    # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1054
    # (with the addition that '*' is allowed for the range - will append).
1055
    content_range = request.META.get('HTTP_CONTENT_RANGE')
1056
    if not content_range:
1057
        raise BadRequest('Missing Content-Range header')
1058
    ranges = get_content_range(request)
1059
    if not ranges:
1060
        raise RangeNotSatisfiable('Invalid Content-Range header')
1061
    
1062
    try:
1063
        size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1064
                                            v_account, v_container, v_object)
1065
    except NotAllowedError:
1066
        raise Forbidden('Not allowed')
1067
    except NameError:
1068
        raise ItemNotFound('Object does not exist')
1069
    
1070
    offset, length, total = ranges
1071
    if offset is None:
1072
        offset = size
1073
    elif offset > size:
1074
        raise RangeNotSatisfiable('Supplied offset is beyond object limits')
1075
    if src_object:
1076
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1077
        if not src_account:
1078
            src_account = request.user_uniq
1079
        src_container, src_name = split_container_object_string(src_object)
1080
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1081
        try:
1082
            src_size, src_hashmap = request.backend.get_object_hashmap(request.user_uniq,
1083
                                        src_account, src_container, src_name, src_version)
1084
        except NotAllowedError:
1085
            raise Forbidden('Not allowed')
1086
        except NameError:
1087
            raise ItemNotFound('Source object does not exist')
1088
        
1089
        if length is None:
1090
            length = src_size
1091
        elif length > src_size:
1092
            raise BadRequest('Object length is smaller than range length')
1093
    else:
1094
        # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1095
        content_length = -1
1096
        if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1097
            content_length = get_content_length(request)
1098
        
1099
        if length is None:
1100
            length = content_length
1101
        else:
1102
            if content_length == -1:
1103
                # TODO: Get up to length bytes in chunks.
1104
                length = content_length
1105
            elif length != content_length:
1106
                raise BadRequest('Content length does not match range length')
1107
    if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1108
        raise RangeNotSatisfiable('Supplied range will change provided object limits')
1109
    
1110
    dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1111
    if dest_bytes is not None:
1112
        dest_bytes = get_int_parameter(dest_bytes)
1113
        if dest_bytes is None:
1114
            raise BadRequest('Invalid X-Object-Bytes header')
1115
    
1116
    if src_object:
1117
        if offset % request.backend.block_size == 0:
1118
            # Update the hashes only.
1119
            sbi = 0
1120
            while length > 0:
1121
                bi = int(offset / request.backend.block_size)
1122
                bl = min(length, request.backend.block_size)
1123
                if bi < len(hashmap):
1124
                    if bl == request.backend.block_size:
1125
                        hashmap[bi] = src_hashmap[sbi]
1126
                    else:
1127
                        data = request.backend.get_block(src_hashmap[sbi])
1128
                        hashmap[bi] = request.backend.update_block(hashmap[bi],
1129
                                                                data[:bl], 0)
1130
                else:
1131
                    hashmap.append(src_hashmap[sbi])
1132
                offset += bl
1133
                length -= bl
1134
                sbi += 1
1135
        else:
1136
            data = ''
1137
            sbi = 0
1138
            while length > 0:
1139
                data += request.backend.get_block(src_hashmap[sbi])
1140
                if length < request.backend.block_size:
1141
                    data = data[:length]
1142
                bytes = put_object_block(request, hashmap, data, offset)
1143
                offset += bytes
1144
                data = data[bytes:]
1145
                length -= bytes
1146
                sbi += 1
1147
    else:
1148
        data = ''
1149
        for d in socket_read_iterator(request, length,
1150
                                        request.backend.block_size):
1151
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1152
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1153
            data += d
1154
            bytes = put_object_block(request, hashmap, data, offset)
1155
            offset += bytes
1156
            data = data[bytes:]
1157
        if len(data) > 0:
1158
            put_object_block(request, hashmap, data, offset)
1159
    
1160
    if offset > size:
1161
        size = offset
1162
    if dest_bytes is not None and dest_bytes < size:
1163
        size = dest_bytes
1164
        hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1165
    meta.update({'ETag': hashmap_hash(request, hashmap)}) # Update ETag.
1166
    try:
1167
        version_id = request.backend.update_object_hashmap(request.user_uniq,
1168
                        v_account, v_container, v_object, size, hashmap, meta,
1169
                        replace, permissions)
1170
    except NotAllowedError:
1171
        raise Forbidden('Not allowed')
1172
    except NameError:
1173
        raise ItemNotFound('Container does not exist')
1174
    except ValueError:
1175
        raise BadRequest('Invalid sharing header')
1176
    except AttributeError, e:
1177
        raise Conflict('\n'.join(e.data) + '\n')
1178
    except QuotaError:
1179
        raise RequestEntityTooLarge('Quota exceeded')
1180
    if public is not None:
1181
        try:
1182
            request.backend.update_object_public(request.user_uniq, v_account,
1183
                                                v_container, v_object, public)
1184
        except NotAllowedError:
1185
            raise Forbidden('Not allowed')
1186
        except NameError:
1187
            raise ItemNotFound('Object does not exist')
1188
    
1189
    response = HttpResponse(status=204)
1190
    response['ETag'] = meta['ETag']
1191
    response['X-Object-Version'] = version_id
1192
    return response
1193

    
1194
@api_method('DELETE')
1195
def object_delete(request, v_account, v_container, v_object):
1196
    # Normal Response Codes: 204
1197
    # Error Response Codes: serviceUnavailable (503),
1198
    #                       itemNotFound (404),
1199
    #                       forbidden (403),
1200
    #                       badRequest (400)
1201
    
1202
    until = get_int_parameter(request.GET.get('until'))
1203
    try:
1204
        request.backend.delete_object(request.user_uniq, v_account, v_container,
1205
                                        v_object, until)
1206
    except NotAllowedError:
1207
        raise Forbidden('Not allowed')
1208
    except NameError:
1209
        raise ItemNotFound('Object does not exist')
1210
    return HttpResponse(status=204)
1211

    
1212
@api_method()
1213
def method_not_allowed(request):
1214
    raise BadRequest('Method not allowed')