Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / functions.py @ 133e3fcf

History | View | Annotate | Download (58.3 kB)

1
# Copyright 2011-2012 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
from xml.dom import minidom
35

    
36
from django.http import HttpResponse
37
from django.template.loader import render_to_string
38
from django.utils import simplejson as json
39
from django.utils.http import parse_etags
40
from django.utils.encoding import smart_str
41
from django.views.decorators.csrf import csrf_exempt
42

    
43
from astakosclient import AstakosClient
44

    
45
from snf_django.lib import api
46
from snf_django.lib.api import faults
47

    
48
from pithos.api.util import (
49
    json_encode_decimal, rename_meta_key, format_header_key,
50
    printable_header_dict, get_account_headers, put_account_headers,
51
    get_container_headers, put_container_headers, get_object_headers,
52
    put_object_headers, update_manifest_meta, update_sharing_meta,
53
    update_public_meta, validate_modification_preconditions,
54
    validate_matching_preconditions, split_container_object_string,
55
    copy_or_move_object, get_int_parameter, get_content_length,
56
    get_content_range, socket_read_iterator, SaveToBackendHandler,
57
    object_data_response, put_object_block, hashmap_md5, simple_list_response,
58
    api_method, is_uuid, retrieve_uuid, retrieve_uuids,
59
    retrieve_displaynames, get_pithos_usage, Checksum, NoChecksum
60
)
61

    
62
from pithos.api.settings import (UPDATE_MD5, TRANSLATE_UUIDS,
63
                                 SERVICE_TOKEN, ASTAKOS_BASE_URL)
64

    
65
from pithos.api import settings
66

    
67
from pithos.backends.base import (
68
    NotAllowedError, QuotaError, ContainerNotEmpty, ItemNotExists,
69
    VersionNotExists, ContainerExists)
70

    
71
from pithos.backends.filter import parse_filters
72

    
73
import logging
74
logger = logging.getLogger(__name__)
75

    
76

    
77
def get_uuids(names):
78
    try:
79
        astakos = AstakosClient(ASTAKOS_BASE_URL, retry=2,
80
                                use_pool=True, logger=logger)
81
        uuids = astakos.service_get_uuids(SERVICE_TOKEN, names)
82
    except Exception, e:
83
        logger.exception(e)
84
        return {}
85

    
86
    return uuids
87

    
88

    
89
@csrf_exempt
90
def top_demux(request):
91
    if request.method == 'GET':
92
        try:
93
            request.GET['X-Auth-Token']
94
        except KeyError:
95
            try:
96
                request.META['HTTP_X_AUTH_TOKEN']
97
            except KeyError:
98
                return authenticate(request)
99
        return account_list(request)
100
    else:
101
        return api.api_method_not_allowed(request)
102

    
103

    
104
@csrf_exempt
105
def account_demux(request, v_account):
106
    if TRANSLATE_UUIDS:
107
        if not is_uuid(v_account):
108
            uuids = get_uuids([v_account])
109
            if not uuids or not v_account in uuids:
110
                return HttpResponse(status=404)
111
            v_account = uuids[v_account]
112

    
113
    if request.method == 'HEAD':
114
        return account_meta(request, v_account)
115
    elif request.method == 'POST':
116
        return account_update(request, v_account)
117
    elif request.method == 'GET':
118
        return container_list(request, v_account)
119
    else:
120
        return api.api_method_not_allowed(request)
121

    
122

    
123
@csrf_exempt
124
def container_demux(request, v_account, v_container):
125
    if TRANSLATE_UUIDS:
126
        if not is_uuid(v_account):
127
            uuids = get_uuids([v_account])
128
            if not uuids or not v_account in uuids:
129
                return HttpResponse(status=404)
130
            v_account = uuids[v_account]
131

    
132
    if request.method == 'HEAD':
133
        return container_meta(request, v_account, v_container)
134
    elif request.method == 'PUT':
135
        return container_create(request, v_account, v_container)
136
    elif request.method == 'POST':
137
        return container_update(request, v_account, v_container)
138
    elif request.method == 'DELETE':
139
        return container_delete(request, v_account, v_container)
140
    elif request.method == 'GET':
141
        return object_list(request, v_account, v_container)
142
    else:
143
        return api.api_method_not_allowed(request)
144

    
145

    
146
@csrf_exempt
147
def object_demux(request, v_account, v_container, v_object):
148
    # Helper to avoid placing the token in the URL
149
    # when loading objects from a browser.
150
    if TRANSLATE_UUIDS:
151
        if not is_uuid(v_account):
152
            uuids = get_uuids([v_account])
153
            if not uuids or not v_account in uuids:
154
                return HttpResponse(status=404)
155
            v_account = uuids[v_account]
156

    
157
    if request.method == 'HEAD':
158
        return object_meta(request, v_account, v_container, v_object)
159
    elif request.method == 'GET':
160
        return object_read(request, v_account, v_container, v_object)
161
    elif request.method == 'PUT':
162
        return object_write(request, v_account, v_container, v_object)
163
    elif request.method == 'COPY':
164
        return object_copy(request, v_account, v_container, v_object)
165
    elif request.method == 'MOVE':
166
        return object_move(request, v_account, v_container, v_object)
167
    elif request.method == 'POST':
168
        if request.META.get(
169
                'CONTENT_TYPE', '').startswith('multipart/form-data'):
170
            return object_write_form(request, v_account, v_container, v_object)
171
        return object_update(request, v_account, v_container, v_object)
172
    elif request.method == 'DELETE':
173
        return object_delete(request, v_account, v_container, v_object)
174
    else:
175
        return api.api_method_not_allowed(request)
176

    
177

    
178
@api_method('GET', user_required=False, logger=logger)
179
def authenticate(request):
180
    # Normal Response Codes: 204
181
    # Error Response Codes: internalServerError (500),
182
    #                       forbidden (403),
183
    #                       badRequest (400)
184

    
185
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
186
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
187
    if not x_auth_user or not x_auth_key:
188
        raise faults.BadRequest('Missing X-Auth-User or X-Auth-Key header')
189
    response = HttpResponse(status=204)
190

    
191
    uri = request.build_absolute_uri()
192
    if '?' in uri:
193
        uri = uri[:uri.find('?')]
194

    
195
    response['X-Auth-Token'] = x_auth_key
196
    response['X-Storage-Url'] = uri + ('' if uri.endswith('/')
197
                                       else '/') + x_auth_user
198
    return response
199

    
200

    
201
@api_method('GET', format_allowed=True, user_required=True, logger=logger)
202
def account_list(request):
203
    # Normal Response Codes: 200, 204
204
    # Error Response Codes: internalServerError (500),
205
    #                       badRequest (400)
206
    response = HttpResponse()
207

    
208
    marker = request.GET.get('marker')
209
    limit = get_int_parameter(request.GET.get('limit'))
210
    if not limit:
211
        limit = settings.API_LIST_LIMIT
212

    
213
    accounts = request.backend.list_accounts(request.user_uniq, marker, limit)
214

    
215
    if request.serialization == 'text':
216
        if TRANSLATE_UUIDS:
217
            accounts = retrieve_displaynames(
218
                getattr(request, 'token', None), accounts)
219
        if len(accounts) == 0:
220
            # The cloudfiles python bindings expect 200 if json/xml.
221
            response.status_code = 204
222
            return response
223
        response.status_code = 200
224
        response.content = '\n'.join(accounts) + '\n'
225
        return response
226

    
227
    account_meta = []
228
    for x in accounts:
229
        if x == request.user_uniq:
230
            continue
231
        usage = get_pithos_usage(request.x_auth_token)
232
        try:
233
            meta = request.backend.get_account_meta(
234
                request.user_uniq, x, 'pithos', include_user_defined=False,
235
                external_quota=usage)
236
            groups = request.backend.get_account_groups(request.user_uniq, x)
237
        except NotAllowedError:
238
            raise faults.Forbidden('Not allowed')
239
        else:
240
            rename_meta_key(meta, 'modified', 'last_modified')
241
            rename_meta_key(
242
                meta, 'until_timestamp', 'x_account_until_timestamp')
243
            if groups:
244
                meta['X-Account-Group'] = printable_header_dict(
245
                    dict([(k, ','.join(v)) for k, v in groups.iteritems()]))
246
            account_meta.append(printable_header_dict(meta))
247

    
248
    if TRANSLATE_UUIDS:
249
        uuids = list(d['name'] for d in account_meta)
250
        catalog = retrieve_displaynames(
251
            getattr(request, 'token', None), uuids, return_dict=True)
252
        for meta in account_meta:
253
            meta['name'] = catalog.get(meta.get('name'))
254

    
255
    if request.serialization == 'xml':
256
        data = render_to_string('accounts.xml', {'accounts': account_meta})
257
    elif request.serialization == 'json':
258
        data = json.dumps(account_meta)
259
    response.status_code = 200
260
    response.content = data
261
    return response
262

    
263

    
264
@api_method('HEAD', user_required=True, logger=logger)
265
def account_meta(request, v_account):
266
    # Normal Response Codes: 204
267
    # Error Response Codes: internalServerError (500),
268
    #                       forbidden (403),
269
    #                       badRequest (400)
270

    
271
    until = get_int_parameter(request.GET.get('until'))
272
    usage = get_pithos_usage(request.x_auth_token)
273
    try:
274
        meta = request.backend.get_account_meta(
275
            request.user_uniq, v_account, 'pithos', until,
276
            external_quota=usage)
277
        groups = request.backend.get_account_groups(
278
            request.user_uniq, v_account)
279

    
280
        if TRANSLATE_UUIDS:
281
            for k in groups:
282
                groups[k] = retrieve_displaynames(
283
                    getattr(request, 'token', None), groups[k])
284
        policy = request.backend.get_account_policy(
285
            request.user_uniq, v_account, external_quota=usage)
286
    except NotAllowedError:
287
        raise faults.Forbidden('Not allowed')
288

    
289
    validate_modification_preconditions(request, meta)
290

    
291
    response = HttpResponse(status=204)
292
    put_account_headers(response, meta, groups, policy)
293
    return response
294

    
295

    
296
@api_method('POST', user_required=True, logger=logger)
297
def account_update(request, v_account):
298
    # Normal Response Codes: 202
299
    # Error Response Codes: internalServerError (500),
300
    #                       forbidden (403),
301
    #                       badRequest (400)
302

    
303
    meta, groups = get_account_headers(request)
304
    for k in groups:
305
        if TRANSLATE_UUIDS:
306
            try:
307
                groups[k] = retrieve_uuids(
308
                    getattr(request, 'token', None),
309
                    groups[k],
310
                    fail_silently=False)
311
            except ItemNotExists, e:
312
                raise faults.BadRequest(
313
                    'Bad X-Account-Group header value: %s' % e)
314
        else:
315
            try:
316
                retrieve_displaynames(
317
                    getattr(request, 'token', None),
318
                    groups[k],
319
                    fail_silently=False)
320
            except ItemNotExists, e:
321
                raise faults.BadRequest(
322
                    'Bad X-Account-Group header value: %s' % e)
323
    replace = True
324
    if 'update' in request.GET:
325
        replace = False
326
    if groups:
327
        try:
328
            request.backend.update_account_groups(request.user_uniq, v_account,
329
                                                  groups, replace)
330
        except NotAllowedError:
331
            raise faults.Forbidden('Not allowed')
332
        except ValueError:
333
            raise faults.BadRequest('Invalid groups header')
334
    if meta or replace:
335
        try:
336
            request.backend.update_account_meta(request.user_uniq, v_account,
337
                                                'pithos', meta, replace)
338
        except NotAllowedError:
339
            raise faults.Forbidden('Not allowed')
340
    return HttpResponse(status=202)
341

    
342

    
343
@api_method('GET', format_allowed=True, user_required=True, logger=logger,
344
            serializations=["text", "xml", "json"])
345
def container_list(request, v_account):
346
    # Normal Response Codes: 200, 204
347
    # Error Response Codes: internalServerError (500),
348
    #                       itemNotFound (404),
349
    #                       forbidden (403),
350
    #                       badRequest (400)
351

    
352
    until = get_int_parameter(request.GET.get('until'))
353
    usage = get_pithos_usage(request.x_auth_token)
354
    try:
355
        meta = request.backend.get_account_meta(
356
            request.user_uniq, v_account, 'pithos', until,
357
            external_quota=usage)
358
        groups = request.backend.get_account_groups(
359
            request.user_uniq, v_account)
360
        policy = request.backend.get_account_policy(
361
            request.user_uniq, v_account, external_quota=usage)
362
    except NotAllowedError:
363
        raise faults.Forbidden('Not allowed')
364

    
365
    validate_modification_preconditions(request, meta)
366

    
367
    response = HttpResponse()
368
    put_account_headers(response, meta, groups, policy)
369

    
370
    marker = request.GET.get('marker')
371
    limit = get_int_parameter(request.GET.get('limit'))
372
    if not limit:
373
        limit = settings.API_LIST_LIMIT
374

    
375
    shared = False
376
    if 'shared' in request.GET:
377
        shared = True
378

    
379
    public_requested = 'public' in request.GET
380
    public_granted = public_requested and request.user_uniq == v_account
381

    
382
    if public_requested and not public_granted:
383
        raise faults.Forbidden(
384
            'PUblic container listing is not allowed to non path owners')
385

    
386
    try:
387
        containers = request.backend.list_containers(
388
            request.user_uniq, v_account,
389
            marker, limit, shared, until, public_granted)
390
    except NotAllowedError:
391
        raise faults.Forbidden('Not allowed')
392
    except NameError:
393
        containers = []
394

    
395
    if request.serialization == 'text':
396
        if len(containers) == 0:
397
            # The cloudfiles python bindings expect 200 if json/xml.
398
            response.status_code = 204
399
            return response
400
        response.status_code = 200
401
        response.content = '\n'.join(containers) + '\n'
402
        return response
403

    
404
    container_meta = []
405
    for x in containers:
406
        try:
407
            meta = request.backend.get_container_meta(
408
                request.user_uniq, v_account,
409
                x, 'pithos', until, include_user_defined=False)
410
            policy = request.backend.get_container_policy(request.user_uniq,
411
                                                          v_account, x)
412
        except NotAllowedError:
413
            raise faults.Forbidden('Not allowed')
414
        except NameError:
415
            pass
416
        else:
417
            rename_meta_key(meta, 'modified', 'last_modified')
418
            rename_meta_key(
419
                meta, 'until_timestamp', 'x_container_until_timestamp')
420
            if policy:
421
                meta['X-Container-Policy'] = printable_header_dict(
422
                    dict([(k, v) for k, v in policy.iteritems()]))
423
            container_meta.append(printable_header_dict(meta))
424
    if request.serialization == 'xml':
425
        data = render_to_string('containers.xml', {'account':
426
                                v_account, 'containers': container_meta})
427
    elif request.serialization == 'json':
428
        data = json.dumps(container_meta)
429
    response.status_code = 200
430
    response.content = data
431
    return response
432

    
433

    
434
@api_method('HEAD', user_required=True, logger=logger)
435
def container_meta(request, v_account, v_container):
436
    # Normal Response Codes: 204
437
    # Error Response Codes: internalServerError (500),
438
    #                       itemNotFound (404),
439
    #                       forbidden (403),
440
    #                       badRequest (400)
441

    
442
    until = get_int_parameter(request.GET.get('until'))
443
    try:
444
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
445
                                                  v_container, 'pithos', until)
446
        meta['object_meta'] = \
447
            request.backend.list_container_meta(request.user_uniq,
448
                                                v_account, v_container,
449
                                                'pithos', until)
450
        policy = request.backend.get_container_policy(
451
            request.user_uniq, v_account,
452
            v_container)
453
    except NotAllowedError:
454
        raise faults.Forbidden('Not allowed')
455
    except ItemNotExists:
456
        raise faults.ItemNotFound('Container does not exist')
457

    
458
    validate_modification_preconditions(request, meta)
459

    
460
    response = HttpResponse(status=204)
461
    put_container_headers(request, response, meta, policy)
462
    return response
463

    
464

    
465
@api_method('PUT', user_required=True, logger=logger)
466
def container_create(request, v_account, v_container):
467
    # Normal Response Codes: 201, 202
468
    # Error Response Codes: internalServerError (500),
469
    #                       itemNotFound (404),
470
    #                       forbidden (403),
471
    #                       badRequest (400)
472

    
473
    meta, policy = get_container_headers(request)
474

    
475
    try:
476
        request.backend.put_container(
477
            request.user_uniq, v_account, v_container, policy)
478
        ret = 201
479
    except NotAllowedError:
480
        raise faults.Forbidden('Not allowed')
481
    except ValueError:
482
        raise faults.BadRequest('Invalid policy header')
483
    except ContainerExists:
484
        ret = 202
485

    
486
    if ret == 202 and policy:
487
        try:
488
            request.backend.update_container_policy(
489
                request.user_uniq, v_account,
490
                v_container, policy, replace=False)
491
        except NotAllowedError:
492
            raise faults.Forbidden('Not allowed')
493
        except ItemNotExists:
494
            raise faults.ItemNotFound('Container does not exist')
495
        except ValueError:
496
            raise faults.BadRequest('Invalid policy header')
497
    if meta:
498
        try:
499
            request.backend.update_container_meta(request.user_uniq, v_account,
500
                                                  v_container, 'pithos',
501
                                                  meta, replace=False)
502
        except NotAllowedError:
503
            raise faults.Forbidden('Not allowed')
504
        except ItemNotExists:
505
            raise faults.ItemNotFound('Container does not exist')
506

    
507
    return HttpResponse(status=ret)
508

    
509

    
510
@api_method('POST', format_allowed=True, user_required=True, logger=logger)
511
def container_update(request, v_account, v_container):
512
    # Normal Response Codes: 202
513
    # Error Response Codes: internalServerError (500),
514
    #                       itemNotFound (404),
515
    #                       forbidden (403),
516
    #                       badRequest (400)
517

    
518
    meta, policy = get_container_headers(request)
519
    replace = True
520
    if 'update' in request.GET:
521
        replace = False
522
    if policy:
523
        try:
524
            request.backend.update_container_policy(
525
                request.user_uniq, v_account,
526
                v_container, policy, replace)
527
        except NotAllowedError:
528
            raise faults.Forbidden('Not allowed')
529
        except ItemNotExists:
530
            raise faults.ItemNotFound('Container does not exist')
531
        except ValueError:
532
            raise faults.BadRequest('Invalid policy header')
533
    if meta or replace:
534
        try:
535
            request.backend.update_container_meta(request.user_uniq, v_account,
536
                                                  v_container, 'pithos',
537
                                                  meta, replace)
538
        except NotAllowedError:
539
            raise faults.Forbidden('Not allowed')
540
        except ItemNotExists:
541
            raise faults.ItemNotFound('Container does not exist')
542

    
543
    content_length = -1
544
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
545
        content_length = get_int_parameter(
546
            request.META.get('CONTENT_LENGTH', 0))
547
    content_type = request.META.get('CONTENT_TYPE')
548
    hashmap = []
549
    if (content_type
550
            and content_type == 'application/octet-stream'
551
            and content_length != 0):
552
        for data in socket_read_iterator(request, content_length,
553
                                         request.backend.block_size):
554
            # TODO: Raise 408 (Request Timeout) if this takes too long.
555
            # TODO: Raise 499 (Client Disconnect) if a length is defined
556
            #       and we stop before getting this much data.
557
            hashmap.append(request.backend.put_block(data))
558

    
559
    response = HttpResponse(status=202)
560
    if hashmap:
561
        response.content = simple_list_response(request, hashmap)
562
    return response
563

    
564

    
565
@api_method('DELETE', user_required=True, logger=logger)
566
def container_delete(request, v_account, v_container):
567
    # Normal Response Codes: 204
568
    # Error Response Codes: internalServerError (500),
569
    #                       conflict (409),
570
    #                       itemNotFound (404),
571
    #                       forbidden (403),
572
    #                       badRequest (400)
573
    #                       requestentitytoolarge (413)
574

    
575
    until = get_int_parameter(request.GET.get('until'))
576

    
577
    delimiter = request.GET.get('delimiter')
578

    
579
    try:
580
        request.backend.delete_container(
581
            request.user_uniq, v_account, v_container,
582
            until, delimiter=delimiter)
583
    except NotAllowedError:
584
        raise faults.Forbidden('Not allowed')
585
    except ItemNotExists:
586
        raise faults.ItemNotFound('Container does not exist')
587
    except ContainerNotEmpty:
588
        raise faults.Conflict('Container is not empty')
589
    except QuotaError, e:
590
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
591
    return HttpResponse(status=204)
592

    
593

    
594
@api_method('GET', format_allowed=True, user_required=True, logger=logger,
595
            serializations=["text", "xml", "json"])
596
def object_list(request, v_account, v_container):
597
    # Normal Response Codes: 200, 204
598
    # Error Response Codes: internalServerError (500),
599
    #                       itemNotFound (404),
600
    #                       forbidden (403),
601
    #                       badRequest (400)
602

    
603
    until = get_int_parameter(request.GET.get('until'))
604
    try:
605
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
606
                                                  v_container, 'pithos', until)
607
        meta['object_meta'] = \
608
            request.backend.list_container_meta(request.user_uniq,
609
                                                v_account, v_container,
610
                                                'pithos', until)
611
        policy = request.backend.get_container_policy(
612
            request.user_uniq, v_account,
613
            v_container)
614
    except NotAllowedError:
615
        raise faults.Forbidden('Not allowed')
616
    except ItemNotExists:
617
        raise faults.ItemNotFound('Container does not exist')
618

    
619
    validate_modification_preconditions(request, meta)
620

    
621
    response = HttpResponse()
622
    put_container_headers(request, response, meta, policy)
623

    
624
    path = request.GET.get('path')
625
    prefix = request.GET.get('prefix')
626
    delimiter = request.GET.get('delimiter')
627

    
628
    # Path overrides prefix and delimiter.
629
    virtual = True
630
    if path:
631
        prefix = path
632
        delimiter = '/'
633
        virtual = False
634

    
635
    # Naming policy.
636
    if prefix and delimiter and not prefix.endswith(delimiter):
637
        prefix = prefix + delimiter
638
    if not prefix:
639
        prefix = ''
640
    prefix = prefix.lstrip('/')
641

    
642
    marker = request.GET.get('marker')
643
    limit = get_int_parameter(request.GET.get('limit'))
644
    if not limit:
645
        limit = settings.API_LIST_LIMIT
646

    
647
    keys = request.GET.get('meta')
648
    if keys:
649
        keys = [smart_str(x.strip()) for x in keys.split(',')
650
                if x.strip() != '']
651
        included, excluded, opers = parse_filters(keys)
652
        keys = []
653
        keys += [format_header_key('X-Object-Meta-' + x) for x in included]
654
        keys += ['!' + format_header_key('X-Object-Meta-' + x)
655
                 for x in excluded]
656
        keys += ['%s%s%s' % (format_header_key(
657
            'X-Object-Meta-' + k), o, v) for k, o, v in opers]
658
    else:
659
        keys = []
660

    
661
    shared = False
662
    if 'shared' in request.GET:
663
        shared = True
664

    
665
    public_requested = 'public' in request.GET
666
    public_granted = public_requested and request.user_uniq == v_account
667

    
668
    if public_requested and not public_granted:
669
        raise faults.Forbidden(
670
            'PUblic object listing is not allowed to non path owners')
671

    
672
    if request.serialization == 'text':
673
        try:
674
            objects = request.backend.list_objects(
675
                request.user_uniq, v_account,
676
                v_container, prefix, delimiter, marker,
677
                limit, virtual, 'pithos', keys, shared,
678
                until, None, public_granted)
679
        except NotAllowedError:
680
            raise faults.Forbidden('Not allowed')
681
        except ItemNotExists:
682
            raise faults.ItemNotFound('Container does not exist')
683

    
684
        if len(objects) == 0:
685
            # The cloudfiles python bindings expect 200 if json/xml.
686
            response.status_code = 204
687
            return response
688
        response.status_code = 200
689
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
690
        return response
691

    
692
    try:
693
        objects = request.backend.list_object_meta(
694
            request.user_uniq, v_account,
695
            v_container, prefix, delimiter, marker,
696
            limit, virtual, 'pithos', keys, shared, until, None, public_granted)
697
        object_permissions = {}
698
        object_public = {}
699
        if until is None:
700
            name = '/'.join((v_account, v_container, ''))
701
            name_idx = len(name)
702
            for x in request.backend.list_object_permissions(
703
                    request.user_uniq, v_account, v_container, prefix):
704

    
705
                # filter out objects which are not under the container
706
                if name != x[:name_idx]:
707
                    continue
708

    
709
                object = x[name_idx:]
710
                object_permissions[object] = \
711
                    request.backend.get_object_permissions(
712
                        request.user_uniq, v_account, v_container, object)
713

    
714
            if request.user_uniq == v_account:
715
                # Bring public information only if the request user
716
                # is the object owner
717
                for k, v in request.backend.list_object_public(
718
                        request.user_uniq, v_account,
719
                        v_container, prefix).iteritems():
720
                    object_public[k[name_idx:]] = v
721
    except NotAllowedError:
722
        raise faults.Forbidden('Not allowed')
723
    except ItemNotExists:
724
        raise faults.ItemNotFound('Container does not exist')
725

    
726
    object_meta = []
727
    for meta in objects:
728
        if TRANSLATE_UUIDS:
729
            modified_by = meta.get('modified_by')
730
            if modified_by:
731
                l = retrieve_displaynames(
732
                    getattr(request, 'token', None), [meta['modified_by']])
733
                if l is not None and len(l) == 1:
734
                    meta['modified_by'] = l[0]
735

    
736
        if len(meta) == 1:
737
            # Virtual objects/directories.
738
            object_meta.append(meta)
739
        else:
740
            rename_meta_key(
741
                meta, 'hash', 'x_object_hash')  # Will be replaced by checksum.
742
            rename_meta_key(meta, 'checksum', 'hash')
743
            rename_meta_key(meta, 'type', 'content_type')
744
            rename_meta_key(meta, 'uuid', 'x_object_uuid')
745
            if until is not None and 'modified' in meta:
746
                del(meta['modified'])
747
            else:
748
                rename_meta_key(meta, 'modified', 'last_modified')
749
            rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
750
            rename_meta_key(meta, 'version', 'x_object_version')
751
            rename_meta_key(
752
                meta, 'version_timestamp', 'x_object_version_timestamp')
753
            permissions = object_permissions.get(meta['name'], None)
754
            if permissions:
755
                update_sharing_meta(request, permissions, v_account,
756
                                    v_container, meta['name'], meta)
757
            public_url = object_public.get(meta['name'], None)
758
            if request.user_uniq == v_account:
759
                # Return public information only if the request user
760
                # is the object owner
761
                update_public_meta(public_url, meta)
762
            object_meta.append(printable_header_dict(meta))
763

    
764
    if request.serialization == 'xml':
765
        data = render_to_string(
766
            'objects.xml', {'container': v_container, 'objects': object_meta})
767
    elif request.serialization == 'json':
768
        data = json.dumps(object_meta, default=json_encode_decimal)
769
    response.status_code = 200
770
    response.content = data
771
    return response
772

    
773

    
774
@api_method('HEAD', user_required=True, logger=logger)
775
def object_meta(request, v_account, v_container, v_object):
776
    # Normal Response Codes: 204
777
    # Error Response Codes: internalServerError (500),
778
    #                       itemNotFound (404),
779
    #                       forbidden (403),
780
    #                       badRequest (400)
781

    
782
    version = request.GET.get('version')
783
    try:
784
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
785
                                               v_container, v_object,
786
                                               'pithos', version)
787
        if version is None:
788
            permissions = request.backend.get_object_permissions(
789
                request.user_uniq,
790
                v_account, v_container, v_object)
791
            public = request.backend.get_object_public(
792
                request.user_uniq, v_account,
793
                v_container, v_object)
794
        else:
795
            permissions = None
796
            public = None
797
    except NotAllowedError:
798
        raise faults.Forbidden('Not allowed')
799
    except ItemNotExists:
800
        raise faults.ItemNotFound('Object does not exist')
801
    except VersionNotExists:
802
        raise faults.ItemNotFound('Version does not exist')
803

    
804
    update_manifest_meta(request, v_account, meta)
805
    update_sharing_meta(
806
        request, permissions, v_account, v_container, v_object, meta)
807
    if request.user_uniq == v_account:
808
        update_public_meta(public, meta)
809

    
810
    # Evaluate conditions.
811
    validate_modification_preconditions(request, meta)
812
    try:
813
        validate_matching_preconditions(request, meta)
814
    except faults.NotModified:
815
        response = HttpResponse(status=304)
816
        response['ETag'] = meta['hash'] if not UPDATE_MD5 else meta['checksum']
817
        return response
818

    
819
    response = HttpResponse(status=200)
820
    put_object_headers(response, meta, token=getattr(request, 'token', None))
821
    return response
822

    
823

    
824
@api_method('GET', format_allowed=True, user_required=True, logger=logger)
825
def object_read(request, v_account, v_container, v_object):
826
    return _object_read(request, v_account, v_container, v_object)
827

    
828

    
829
def _object_read(request, v_account, v_container, v_object):
830
    # Normal Response Codes: 200, 206
831
    # Error Response Codes: internalServerError (500),
832
    #                       rangeNotSatisfiable (416),
833
    #                       preconditionFailed (412),
834
    #                       itemNotFound (404),
835
    #                       forbidden (403),
836
    #                       badRequest (400),
837
    #                       notModified (304)
838

    
839
    version = request.GET.get('version')
840

    
841
    # Reply with the version list. Do this first, as the object may be deleted.
842
    if version == 'list':
843
        if request.serialization == 'text':
844
            raise faults.BadRequest('No format specified for version list.')
845

    
846
        try:
847
            v = request.backend.list_versions(request.user_uniq, v_account,
848
                                              v_container, v_object)
849
        except NotAllowedError:
850
            raise faults.Forbidden('Not allowed')
851
        except ItemNotExists:
852
            raise faults.ItemNotFound('Object does not exist')
853
        d = {'versions': v}
854
        if request.serialization == 'xml':
855
            d['object'] = v_object
856
            data = render_to_string('versions.xml', d)
857
        elif request.serialization == 'json':
858
            data = json.dumps(d, default=json_encode_decimal)
859

    
860
        response = HttpResponse(data, status=200)
861
        response['Content-Length'] = len(data)
862
        return response
863

    
864
    try:
865
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
866
                                               v_container, v_object,
867
                                               'pithos', version)
868
        if version is None:
869
            permissions = request.backend.get_object_permissions(
870
                request.user_uniq,
871
                v_account, v_container, v_object)
872
            public = request.backend.get_object_public(
873
                request.user_uniq, v_account,
874
                v_container, v_object)
875
        else:
876
            permissions = None
877
            public = None
878
    except NotAllowedError:
879
        raise faults.Forbidden('Not allowed')
880
    except ItemNotExists:
881
        raise faults.ItemNotFound('Object does not exist')
882
    except VersionNotExists:
883
        raise faults.ItemNotFound('Version does not exist')
884

    
885
    update_manifest_meta(request, v_account, meta)
886
    update_sharing_meta(
887
        request, permissions, v_account, v_container, v_object, meta)
888
    if request.user_uniq == v_account:
889
        update_public_meta(public, meta)
890

    
891
    # Evaluate conditions.
892
    validate_modification_preconditions(request, meta)
893
    try:
894
        validate_matching_preconditions(request, meta)
895
    except faults.NotModified:
896
        response = HttpResponse(status=304)
897
        response['ETag'] = meta['hash'] if not UPDATE_MD5 else meta['checksum']
898
        return response
899

    
900
    hashmap_reply = False
901
    if 'hashmap' in request.GET and request.serialization != 'text':
902
        hashmap_reply = True
903

    
904
    sizes = []
905
    hashmaps = []
906
    if 'X-Object-Manifest' in meta and not hashmap_reply:
907
        try:
908
            src_container, src_name = split_container_object_string(
909
                '/' + meta['X-Object-Manifest'])
910
            objects = request.backend.list_objects(
911
                request.user_uniq, v_account,
912
                src_container, prefix=src_name, virtual=False)
913
        except NotAllowedError:
914
            raise faults.Forbidden('Not allowed')
915
        except ValueError:
916
            raise faults.BadRequest('Invalid X-Object-Manifest header')
917
        except ItemNotExists:
918
            raise faults.ItemNotFound('Container does not exist')
919

    
920
        try:
921
            for x in objects:
922
                s, h = \
923
                    request.backend.get_object_hashmap(request.user_uniq,
924
                                                       v_account, src_container,
925
                                                       x[0], x[1])
926
                sizes.append(s)
927
                hashmaps.append(h)
928
        except NotAllowedError:
929
            raise faults.Forbidden('Not allowed')
930
        except ItemNotExists:
931
            raise faults.ItemNotFound('Object does not exist')
932
        except VersionNotExists:
933
            raise faults.ItemNotFound('Version does not exist')
934
    else:
935
        try:
936
            s, h = request.backend.get_object_hashmap(
937
                request.user_uniq, v_account,
938
                v_container, v_object, version)
939
            sizes.append(s)
940
            hashmaps.append(h)
941
        except NotAllowedError:
942
            raise faults.Forbidden('Not allowed')
943
        except ItemNotExists:
944
            raise faults.ItemNotFound('Object does not exist')
945
        except VersionNotExists:
946
            raise faults.ItemNotFound('Version does not exist')
947

    
948
    # Reply with the hashmap.
949
    if hashmap_reply:
950
        size = sum(sizes)
951
        hashmap = sum(hashmaps, [])
952
        d = {
953
            'block_size': request.backend.block_size,
954
            'block_hash': request.backend.hash_algorithm,
955
            'bytes': size,
956
            'hashes': hashmap}
957
        if request.serialization == 'xml':
958
            d['object'] = v_object
959
            data = render_to_string('hashes.xml', d)
960
        elif request.serialization == 'json':
961
            data = json.dumps(d)
962

    
963
        response = HttpResponse(data, status=200)
964
        put_object_headers(
965
            response, meta, token=getattr(request, 'token', None))
966
        response['Content-Length'] = len(data)
967
        return response
968

    
969
    request.serialization = 'text'  # Unset.
970
    return object_data_response(request, sizes, hashmaps, meta)
971

    
972

    
973
@api_method('PUT', format_allowed=True, user_required=True, logger=logger)
974
def object_write(request, v_account, v_container, v_object):
975
    # Normal Response Codes: 201
976
    # Error Response Codes: internalServerError (500),
977
    #                       unprocessableEntity (422),
978
    #                       lengthRequired (411),
979
    #                       conflict (409),
980
    #                       itemNotFound (404),
981
    #                       forbidden (403),
982
    #                       badRequest (400)
983
    #                       requestentitytoolarge (413)
984

    
985
    # Evaluate conditions.
986
    if (request.META.get('HTTP_IF_MATCH')
987
            or request.META.get('HTTP_IF_NONE_MATCH')):
988
        try:
989
            meta = request.backend.get_object_meta(
990
                request.user_uniq, v_account,
991
                v_container, v_object, 'pithos')
992
        except NotAllowedError:
993
            raise faults.Forbidden('Not allowed')
994
        except NameError:
995
            meta = {}
996
        validate_matching_preconditions(request, meta)
997

    
998
    copy_from = request.META.get('HTTP_X_COPY_FROM')
999
    move_from = request.META.get('HTTP_X_MOVE_FROM')
1000
    if copy_from or move_from:
1001
        delimiter = request.GET.get('delimiter')
1002
        content_length = get_content_length(request)  # Required by the API.
1003

    
1004
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1005

    
1006
        if not src_account:
1007
            src_account = request.user_uniq
1008
        else:
1009
            if TRANSLATE_UUIDS:
1010
                try:
1011
                    src_account = retrieve_uuid(getattr(request, 'token', None),
1012
                                                src_account)
1013
                except ItemNotExists:
1014
                    faults.ItemNotFound('Invalid source account')
1015

    
1016
        if move_from:
1017
            try:
1018
                src_container, src_name = split_container_object_string(
1019
                    move_from)
1020
            except ValueError:
1021
                raise faults.BadRequest('Invalid X-Move-From header')
1022
            version_id = copy_or_move_object(
1023
                request, src_account, src_container, src_name,
1024
                v_account, v_container, v_object,
1025
                move=True, delimiter=delimiter)
1026
        else:
1027
            try:
1028
                src_container, src_name = split_container_object_string(
1029
                    copy_from)
1030
            except ValueError:
1031
                raise faults.BadRequest('Invalid X-Copy-From header')
1032
            version_id = copy_or_move_object(
1033
                request, src_account, src_container, src_name,
1034
                v_account, v_container, v_object,
1035
                move=False, delimiter=delimiter)
1036
        response = HttpResponse(status=201)
1037
        response['X-Object-Version'] = version_id
1038
        return response
1039

    
1040
    content_type, meta, permissions, public = get_object_headers(request)
1041
    content_length = -1
1042
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1043
        content_length = get_content_length(request)
1044
    # Should be BadRequest, but API says otherwise.
1045
    if content_type is None:
1046
        raise faults.LengthRequired('Missing Content-Type header')
1047

    
1048
    if 'hashmap' in request.GET:
1049
        if request.serialization not in ('json', 'xml'):
1050
            raise faults.BadRequest('Invalid hashmap format')
1051

    
1052
        data = ''
1053
        for block in socket_read_iterator(request, content_length,
1054
                                          request.backend.block_size):
1055
            data = '%s%s' % (data, block)
1056

    
1057
        if request.serialization == 'json':
1058
            d = json.loads(data)
1059
            if not hasattr(d, '__getitem__'):
1060
                raise faults.BadRequest('Invalid data formating')
1061
            try:
1062
                hashmap = d['hashes']
1063
                size = int(d['bytes'])
1064
            except:
1065
                raise faults.BadRequest('Invalid data formatting')
1066
        elif request.serialization == 'xml':
1067
            try:
1068
                xml = minidom.parseString(data)
1069
                obj = xml.getElementsByTagName('object')[0]
1070
                size = int(obj.attributes['bytes'].value)
1071

    
1072
                hashes = xml.getElementsByTagName('hash')
1073
                hashmap = []
1074
                for hash in hashes:
1075
                    hashmap.append(hash.firstChild.data)
1076
            except:
1077
                raise faults.BadRequest('Invalid data formatting')
1078

    
1079
        checksum = ''  # Do not set to None (will copy previous value).
1080
    else:
1081
        etag = request.META.get('HTTP_ETAG')
1082
        checksum_compute = Checksum() if etag or UPDATE_MD5 else NoChecksum()
1083
        size = 0
1084
        hashmap = []
1085
        for data in socket_read_iterator(request, content_length,
1086
                                         request.backend.block_size):
1087
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1088
            # TODO: Raise 499 (Client Disconnect) if a length is defined
1089
            #       and we stop before getting this much data.
1090
            size += len(data)
1091
            hashmap.append(request.backend.put_block(data))
1092
            checksum_compute.update(data)
1093

    
1094
        checksum = checksum_compute.hexdigest()
1095
        if etag and parse_etags(etag)[0].lower() != checksum:
1096
            raise faults.UnprocessableEntity('Object ETag does not match')
1097

    
1098
    try:
1099
        version_id, merkle = request.backend.update_object_hashmap(
1100
            request.user_uniq, v_account, v_container, v_object, size,
1101
            content_type, hashmap, checksum, 'pithos', meta, True, permissions
1102
        )
1103
    except NotAllowedError:
1104
        raise faults.Forbidden('Not allowed')
1105
    except IndexError, e:
1106
        missing_blocks = e.data
1107
        response = HttpResponse(status=409)
1108
        response.content = simple_list_response(request, missing_blocks)
1109
        return response
1110
    except ItemNotExists:
1111
        raise faults.ItemNotFound('Container does not exist')
1112
    except ValueError:
1113
        raise faults.BadRequest('Invalid sharing header')
1114
    except QuotaError, e:
1115
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1116
    if not checksum and UPDATE_MD5:
1117
        # Update the MD5 after the hashmap, as there may be missing hashes.
1118
        checksum = hashmap_md5(request.backend, hashmap, size)
1119
        try:
1120
            request.backend.update_object_checksum(request.user_uniq,
1121
                                                   v_account, v_container,
1122
                                                   v_object, version_id,
1123
                                                   checksum)
1124
        except NotAllowedError:
1125
            raise faults.Forbidden('Not allowed')
1126
    if public is not None:
1127
        try:
1128
            request.backend.update_object_public(request.user_uniq, v_account,
1129
                                                 v_container, v_object, public)
1130
        except NotAllowedError:
1131
            raise faults.Forbidden('Not allowed')
1132
        except ItemNotExists:
1133
            raise faults.ItemNotFound('Object does not exist')
1134

    
1135
    response = HttpResponse(status=201)
1136
    response['ETag'] = merkle if not UPDATE_MD5 else checksum
1137
    response['X-Object-Version'] = version_id
1138
    return response
1139

    
1140

    
1141
@api_method('POST', user_required=True, logger=logger)
1142
def object_write_form(request, v_account, v_container, v_object):
1143
    # Normal Response Codes: 201
1144
    # Error Response Codes: internalServerError (500),
1145
    #                       itemNotFound (404),
1146
    #                       forbidden (403),
1147
    #                       badRequest (400)
1148
    #                       requestentitytoolarge (413)
1149

    
1150
    request.upload_handlers = [SaveToBackendHandler(request)]
1151
    if 'X-Object-Data' not in request.FILES:
1152
        raise faults.BadRequest('Missing X-Object-Data field')
1153
    file = request.FILES['X-Object-Data']
1154

    
1155
    checksum = file.etag
1156
    try:
1157
        version_id, merkle = request.backend.update_object_hashmap(
1158
            request.user_uniq, v_account, v_container, v_object, file.size,
1159
            file.content_type, file.hashmap, checksum, 'pithos', {}, True
1160
        )
1161
    except NotAllowedError:
1162
        raise faults.Forbidden('Not allowed')
1163
    except ItemNotExists:
1164
        raise faults.ItemNotFound('Container does not exist')
1165
    except QuotaError, e:
1166
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1167

    
1168
    response = HttpResponse(status=201)
1169
    response['ETag'] = merkle if not UPDATE_MD5 else checksum
1170
    response['X-Object-Version'] = version_id
1171
    response.content = checksum
1172
    return response
1173

    
1174

    
1175
@api_method('COPY', format_allowed=True, user_required=True, logger=logger)
1176
def object_copy(request, v_account, v_container, v_object):
1177
    # Normal Response Codes: 201
1178
    # Error Response Codes: internalServerError (500),
1179
    #                       itemNotFound (404),
1180
    #                       forbidden (403),
1181
    #                       badRequest (400)
1182
    #                       requestentitytoolarge (413)
1183

    
1184
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
1185
    if not dest_account:
1186
        dest_account = request.user_uniq
1187
    dest_path = request.META.get('HTTP_DESTINATION')
1188
    if not dest_path:
1189
        raise faults.BadRequest('Missing Destination header')
1190
    try:
1191
        dest_container, dest_name = split_container_object_string(dest_path)
1192
    except ValueError:
1193
        raise faults.BadRequest('Invalid Destination header')
1194

    
1195
    # Evaluate conditions.
1196
    if (request.META.get('HTTP_IF_MATCH')
1197
            or request.META.get('HTTP_IF_NONE_MATCH')):
1198
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1199
        try:
1200
            meta = request.backend.get_object_meta(
1201
                request.user_uniq, v_account,
1202
                v_container, v_object, 'pithos', src_version)
1203
        except NotAllowedError:
1204
            raise faults.Forbidden('Not allowed')
1205
        except (ItemNotExists, VersionNotExists):
1206
            raise faults.ItemNotFound('Container or object does not exist')
1207
        validate_matching_preconditions(request, meta)
1208

    
1209
    delimiter = request.GET.get('delimiter')
1210

    
1211
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1212
                                     dest_account, dest_container, dest_name,
1213
                                     move=False, delimiter=delimiter)
1214
    response = HttpResponse(status=201)
1215
    response['X-Object-Version'] = version_id
1216
    return response
1217

    
1218

    
1219
@api_method('MOVE', format_allowed=True, user_required=True, logger=logger)
1220
def object_move(request, v_account, v_container, v_object):
1221
    # Normal Response Codes: 201
1222
    # Error Response Codes: internalServerError (500),
1223
    #                       itemNotFound (404),
1224
    #                       forbidden (403),
1225
    #                       badRequest (400)
1226
    #                       requestentitytoolarge (413)
1227

    
1228
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
1229
    if not dest_account:
1230
        dest_account = request.user_uniq
1231
    dest_path = request.META.get('HTTP_DESTINATION')
1232
    if not dest_path:
1233
        raise faults.BadRequest('Missing Destination header')
1234
    try:
1235
        dest_container, dest_name = split_container_object_string(dest_path)
1236
    except ValueError:
1237
        raise faults.BadRequest('Invalid Destination header')
1238

    
1239
    # Evaluate conditions.
1240
    if (request.META.get('HTTP_IF_MATCH')
1241
            or request.META.get('HTTP_IF_NONE_MATCH')):
1242
        try:
1243
            meta = request.backend.get_object_meta(
1244
                request.user_uniq, v_account,
1245
                v_container, v_object, 'pithos')
1246
        except NotAllowedError:
1247
            raise faults.Forbidden('Not allowed')
1248
        except ItemNotExists:
1249
            raise faults.ItemNotFound('Container or object does not exist')
1250
        validate_matching_preconditions(request, meta)
1251

    
1252
    delimiter = request.GET.get('delimiter')
1253

    
1254
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1255
                                     dest_account, dest_container, dest_name,
1256
                                     move=True, delimiter=delimiter)
1257
    response = HttpResponse(status=201)
1258
    response['X-Object-Version'] = version_id
1259
    return response
1260

    
1261

    
1262
@api_method('POST', format_allowed=True, user_required=True, logger=logger)
1263
def object_update(request, v_account, v_container, v_object):
1264
    # Normal Response Codes: 202, 204
1265
    # Error Response Codes: internalServerError (500),
1266
    #                       conflict (409),
1267
    #                       itemNotFound (404),
1268
    #                       forbidden (403),
1269
    #                       badRequest (400)
1270

    
1271
    content_type, meta, permissions, public = get_object_headers(request)
1272

    
1273
    try:
1274
        prev_meta = request.backend.get_object_meta(
1275
            request.user_uniq, v_account,
1276
            v_container, v_object, 'pithos')
1277
    except NotAllowedError:
1278
        raise faults.Forbidden('Not allowed')
1279
    except ItemNotExists:
1280
        raise faults.ItemNotFound('Object does not exist')
1281

    
1282
    # Evaluate conditions.
1283
    if (request.META.get('HTTP_IF_MATCH')
1284
            or request.META.get('HTTP_IF_NONE_MATCH')):
1285
        validate_matching_preconditions(request, prev_meta)
1286

    
1287
    replace = True
1288
    if 'update' in request.GET:
1289
        replace = False
1290

    
1291
    # A Content-Type or X-Source-Object header indicates data updates.
1292
    src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1293
    if ((not content_type or content_type != 'application/octet-stream')
1294
            and not src_object):
1295
        response = HttpResponse(status=202)
1296

    
1297
        # Do permissions first, as it may fail easier.
1298
        if permissions is not None:
1299
            try:
1300
                request.backend.update_object_permissions(request.user_uniq,
1301
                                                          v_account,
1302
                                                          v_container, v_object,
1303
                                                          permissions)
1304
            except NotAllowedError:
1305
                raise faults.Forbidden('Not allowed')
1306
            except ItemNotExists:
1307
                raise faults.ItemNotFound('Object does not exist')
1308
            except ValueError:
1309
                raise faults.BadRequest('Invalid sharing header')
1310
        if public is not None:
1311
            try:
1312
                request.backend.update_object_public(
1313
                    request.user_uniq, v_account,
1314
                    v_container, v_object, public)
1315
            except NotAllowedError:
1316
                raise faults.Forbidden('Not allowed')
1317
            except ItemNotExists:
1318
                raise faults.ItemNotFound('Object does not exist')
1319
        if meta or replace:
1320
            try:
1321
                version_id = request.backend.update_object_meta(
1322
                    request.user_uniq,
1323
                    v_account, v_container, v_object, 'pithos', meta, replace)
1324
            except NotAllowedError:
1325
                raise faults.Forbidden('Not allowed')
1326
            except ItemNotExists:
1327
                raise faults.ItemNotFound('Object does not exist')
1328
            response['X-Object-Version'] = version_id
1329

    
1330
        return response
1331

    
1332
    # Single range update. Range must be in Content-Range.
1333
    # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1334
    # (with the addition that '*' is allowed for the range - will append).
1335
    content_range = request.META.get('HTTP_CONTENT_RANGE')
1336
    if not content_range:
1337
        raise faults.BadRequest('Missing Content-Range header')
1338
    ranges = get_content_range(request)
1339
    if not ranges:
1340
        raise faults.RangeNotSatisfiable('Invalid Content-Range header')
1341

    
1342
    try:
1343
        size, hashmap = \
1344
            request.backend.get_object_hashmap(request.user_uniq,
1345
                                               v_account, v_container, v_object)
1346
    except NotAllowedError:
1347
        raise faults.Forbidden('Not allowed')
1348
    except ItemNotExists:
1349
        raise faults.ItemNotFound('Object does not exist')
1350

    
1351
    offset, length, total = ranges
1352
    if offset is None:
1353
        offset = size
1354
    elif offset > size:
1355
        raise faults.RangeNotSatisfiable(
1356
            'Supplied offset is beyond object limits')
1357
    if src_object:
1358
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1359
        if not src_account:
1360
            src_account = request.user_uniq
1361
        src_container, src_name = split_container_object_string(src_object)
1362
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1363
        try:
1364
            src_size, src_hashmap = request.backend.get_object_hashmap(
1365
                request.user_uniq,
1366
                src_account, src_container, src_name, src_version)
1367
        except NotAllowedError:
1368
            raise faults.Forbidden('Not allowed')
1369
        except ItemNotExists:
1370
            raise faults.ItemNotFound('Source object does not exist')
1371

    
1372
        if length is None:
1373
            length = src_size
1374
        elif length > src_size:
1375
            raise faults.BadRequest(
1376
                'Object length is smaller than range length')
1377
    else:
1378
        # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1379
        content_length = -1
1380
        if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1381
            content_length = get_content_length(request)
1382

    
1383
        if length is None:
1384
            length = content_length
1385
        else:
1386
            if content_length == -1:
1387
                # TODO: Get up to length bytes in chunks.
1388
                length = content_length
1389
            elif length != content_length:
1390
                raise faults.BadRequest(
1391
                    'Content length does not match range length')
1392
    if (total is not None
1393
            and (total != size or offset >= size
1394
                 or (length > 0 and offset + length > size))):
1395
        raise faults.RangeNotSatisfiable(
1396
            'Supplied range will change provided object limits')
1397

    
1398
    dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1399
    if dest_bytes is not None:
1400
        dest_bytes = get_int_parameter(dest_bytes)
1401
        if dest_bytes is None:
1402
            raise faults.BadRequest('Invalid X-Object-Bytes header')
1403

    
1404
    if src_object:
1405
        if offset % request.backend.block_size == 0:
1406
            # Update the hashes only.
1407
            sbi = 0
1408
            while length > 0:
1409
                bi = int(offset / request.backend.block_size)
1410
                bl = min(length, request.backend.block_size)
1411
                if bi < len(hashmap):
1412
                    if bl == request.backend.block_size:
1413
                        hashmap[bi] = src_hashmap[sbi]
1414
                    else:
1415
                        data = request.backend.get_block(src_hashmap[sbi])
1416
                        hashmap[bi] = request.backend.update_block(hashmap[bi],
1417
                                                                   data[:bl], 0)
1418
                else:
1419
                    hashmap.append(src_hashmap[sbi])
1420
                offset += bl
1421
                length -= bl
1422
                sbi += 1
1423
        else:
1424
            data = ''
1425
            sbi = 0
1426
            while length > 0:
1427
                data += request.backend.get_block(src_hashmap[sbi])
1428
                if length < request.backend.block_size:
1429
                    data = data[:length]
1430
                bytes = put_object_block(request, hashmap, data, offset)
1431
                offset += bytes
1432
                data = data[bytes:]
1433
                length -= bytes
1434
                sbi += 1
1435
    else:
1436
        data = ''
1437
        for d in socket_read_iterator(request, length,
1438
                                      request.backend.block_size):
1439
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1440
            # TODO: Raise 499 (Client Disconnect) if a length is defined
1441
            #       and we stop before getting this much data.
1442
            data += d
1443
            bytes = put_object_block(request, hashmap, data, offset)
1444
            offset += bytes
1445
            data = data[bytes:]
1446
        if len(data) > 0:
1447
            put_object_block(request, hashmap, data, offset)
1448

    
1449
    if offset > size:
1450
        size = offset
1451
    if dest_bytes is not None and dest_bytes < size:
1452
        size = dest_bytes
1453
        hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1454
    checksum = hashmap_md5(
1455
        request.backend, hashmap, size) if UPDATE_MD5 else ''
1456
    try:
1457
        version_id, merkle = request.backend.update_object_hashmap(
1458
            request.user_uniq, v_account, v_container, v_object, size,
1459
            prev_meta['type'], hashmap, checksum, 'pithos', meta, replace,
1460
            permissions
1461
        )
1462
    except NotAllowedError:
1463
        raise faults.Forbidden('Not allowed')
1464
    except ItemNotExists:
1465
        raise faults.ItemNotFound('Container does not exist')
1466
    except ValueError:
1467
        raise faults.BadRequest('Invalid sharing header')
1468
    except QuotaError, e:
1469
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1470
    if public is not None:
1471
        try:
1472
            request.backend.update_object_public(request.user_uniq, v_account,
1473
                                                 v_container, v_object, public)
1474
        except NotAllowedError:
1475
            raise faults.Forbidden('Not allowed')
1476
        except ItemNotExists:
1477
            raise faults.ItemNotFound('Object does not exist')
1478

    
1479
    response = HttpResponse(status=204)
1480
    response['ETag'] = merkle if not UPDATE_MD5 else checksum
1481
    response['X-Object-Version'] = version_id
1482
    return response
1483

    
1484

    
1485
@api_method('DELETE', user_required=True, logger=logger)
1486
def object_delete(request, v_account, v_container, v_object):
1487
    # Normal Response Codes: 204
1488
    # Error Response Codes: internalServerError (500),
1489
    #                       itemNotFound (404),
1490
    #                       forbidden (403),
1491
    #                       badRequest (400)
1492
    #                       requestentitytoolarge (413)
1493

    
1494
    until = get_int_parameter(request.GET.get('until'))
1495
    delimiter = request.GET.get('delimiter')
1496

    
1497
    try:
1498
        request.backend.delete_object(
1499
            request.user_uniq, v_account, v_container,
1500
            v_object, until, delimiter=delimiter)
1501
    except NotAllowedError:
1502
        raise faults.Forbidden('Not allowed')
1503
    except ItemNotExists:
1504
        raise faults.ItemNotFound('Object does not exist')
1505
    except QuotaError, e:
1506
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1507
    return HttpResponse(status=204)