Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / functions.py @ 65bbcd43

History | View | Annotate | Download (56.7 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 synnefo.lib.astakos import get_user, get_uuids as _get_uuids
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,
59
    retrieve_uuid, retrieve_displayname, retrieve_uuids, retrieve_displaynames,
60
    get_pithos_usage
61
)
62

    
63
from pithos.api.settings import (UPDATE_MD5, TRANSLATE_UUIDS,
64
                                 SERVICE_TOKEN, AUTHENTICATION_URL,
65
                                 AUTHENTICATION_USERS)
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 hashlib
74

    
75
import logging
76
logger = logging.getLogger(__name__)
77

    
78

    
79
def get_uuids(names):
80
    try:
81
        uuids = _get_uuids(SERVICE_TOKEN, names,
82
                           url=AUTHENTICATION_URL.replace(
83
                                            'im/authenticate',
84
                                            'service/api/user_catalogs'),
85
                           override_users=AUTHENTICATION_USERS)
86
    except Exception, e:
87
        logger.exception(e)
88
        return {}
89

    
90
    return uuids
91

    
92

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

    
107

    
108
@csrf_exempt
109
def account_demux(request, v_account):
110
    if TRANSLATE_UUIDS:
111
        if not is_uuid(v_account):
112
            uuids = get_uuids([v_account])
113
            if not uuids or not v_account in uuids:
114
                return HttpResponse(status=404)
115
            v_account = uuids[v_account]
116

    
117
    if request.method == 'HEAD':
118
        return account_meta(request, v_account)
119
    elif request.method == 'POST':
120
        return account_update(request, v_account)
121
    elif request.method == 'GET':
122
        return container_list(request, v_account)
123
    else:
124
        return api.method_not_allowed(request)
125

    
126

    
127
@csrf_exempt
128
def container_demux(request, v_account, v_container):
129
    if TRANSLATE_UUIDS:
130
        if not is_uuid(v_account):
131
            uuids = get_uuids([v_account])
132
            if not uuids or not v_account in uuids:
133
                return HttpResponse(status=404)
134
            v_account = uuids[v_account]
135

    
136
    if request.method == 'HEAD':
137
        return container_meta(request, v_account, v_container)
138
    elif request.method == 'PUT':
139
        return container_create(request, v_account, v_container)
140
    elif request.method == 'POST':
141
        return container_update(request, v_account, v_container)
142
    elif request.method == 'DELETE':
143
        return container_delete(request, v_account, v_container)
144
    elif request.method == 'GET':
145
        return object_list(request, v_account, v_container)
146
    else:
147
        return api.method_not_allowed(request)
148

    
149

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

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

    
179

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

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

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

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

    
202

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

    
210
    marker = request.GET.get('marker')
211
    limit = get_int_parameter(request.GET.get('limit'))
212
    if not limit:
213
        limit = 10000
214

    
215
    accounts = request.backend.list_accounts(request.user_uniq, marker, limit)
216

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

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

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

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

    
265

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

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

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

    
291
    validate_modification_preconditions(request, meta)
292

    
293
    response = HttpResponse(status=204)
294
    put_account_headers(response, meta, groups, policy)
295
    return response
296

    
297

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

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

    
344

    
345
@api_method('GET', format_allowed=True, user_required=True, logger=logger)
346
def container_list(request, v_account):
347
    # Normal Response Codes: 200, 204
348
    # Error Response Codes: internalServerError (500),
349
    #                       itemNotFound (404),
350
    #                       forbidden (403),
351
    #                       badRequest (400)
352

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

    
366
    validate_modification_preconditions(request, meta)
367

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

    
371
    marker = request.GET.get('marker')
372
    limit = get_int_parameter(request.GET.get('limit'))
373
    if not limit:
374
        limit = 10000
375

    
376
    shared = False
377
    if 'shared' in request.GET:
378
        shared = True
379
    public = False
380
    if request.user_uniq == v_account and 'public' in request.GET:
381
        public = True
382

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

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

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

    
430

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

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

    
453
    validate_modification_preconditions(request, meta)
454

    
455
    response = HttpResponse(status=204)
456
    put_container_headers(request, response, meta, policy)
457
    return response
458

    
459

    
460
@api_method('PUT', user_required=True, logger=logger)
461
def container_create(request, v_account, v_container):
462
    # Normal Response Codes: 201, 202
463
    # Error Response Codes: internalServerError (500),
464
    #                       itemNotFound (404),
465
    #                       forbidden (403),
466
    #                       badRequest (400)
467

    
468
    meta, policy = get_container_headers(request)
469

    
470
    try:
471
        request.backend.put_container(
472
            request.user_uniq, v_account, v_container, policy)
473
        ret = 201
474
    except NotAllowedError:
475
        raise faults.Forbidden('Not allowed')
476
    except ValueError:
477
        raise faults.BadRequest('Invalid policy header')
478
    except ContainerExists:
479
        ret = 202
480

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

    
501
    return HttpResponse(status=ret)
502

    
503

    
504
@api_method('POST', format_allowed=True, user_required=True, logger=logger)
505
def container_update(request, v_account, v_container):
506
    # Normal Response Codes: 202
507
    # Error Response Codes: internalServerError (500),
508
    #                       itemNotFound (404),
509
    #                       forbidden (403),
510
    #                       badRequest (400)
511

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

    
536
    content_length = -1
537
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
538
        content_length = get_int_parameter(
539
            request.META.get('CONTENT_LENGTH', 0))
540
    content_type = request.META.get('CONTENT_TYPE')
541
    hashmap = []
542
    if content_type and content_type == 'application/octet-stream' and content_length != 0:
543
        for data in socket_read_iterator(request, content_length,
544
                                         request.backend.block_size):
545
            # TODO: Raise 408 (Request Timeout) if this takes too long.
546
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
547
            hashmap.append(request.backend.put_block(data))
548

    
549
    response = HttpResponse(status=202)
550
    if hashmap:
551
        response.content = simple_list_response(request, hashmap)
552
    return response
553

    
554

    
555
@api_method('DELETE', user_required=True, logger=logger)
556
def container_delete(request, v_account, v_container):
557
    # Normal Response Codes: 204
558
    # Error Response Codes: internalServerError (500),
559
    #                       conflict (409),
560
    #                       itemNotFound (404),
561
    #                       forbidden (403),
562
    #                       badRequest (400)
563
    #                       requestentitytoolarge (413)
564

    
565
    until = get_int_parameter(request.GET.get('until'))
566

    
567
    delimiter = request.GET.get('delimiter')
568

    
569
    try:
570
        request.backend.delete_container(
571
            request.user_uniq, v_account, v_container,
572
            until, delimiter=delimiter)
573
    except NotAllowedError:
574
        raise faults.Forbidden('Not allowed')
575
    except ItemNotExists:
576
        raise faults.ItemNotFound('Container does not exist')
577
    except ContainerNotEmpty:
578
        raise faults.Conflict('Container is not empty')
579
    except QuotaError, e:
580
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
581
    return HttpResponse(status=204)
582

    
583

    
584
@api_method('GET', format_allowed=True, user_required=True, logger=logger)
585
def object_list(request, v_account, v_container):
586
    # Normal Response Codes: 200, 204
587
    # Error Response Codes: internalServerError (500),
588
    #                       itemNotFound (404),
589
    #                       forbidden (403),
590
    #                       badRequest (400)
591

    
592
    until = get_int_parameter(request.GET.get('until'))
593
    try:
594
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
595
                                                  v_container, 'pithos', until)
596
        meta['object_meta'] = request.backend.list_container_meta(request.user_uniq,
597
                                                                  v_account, v_container, 'pithos', until)
598
        policy = request.backend.get_container_policy(
599
            request.user_uniq, v_account,
600
            v_container)
601
    except NotAllowedError:
602
        raise faults.Forbidden('Not allowed')
603
    except ItemNotExists:
604
        raise faults.ItemNotFound('Container does not exist')
605

    
606
    validate_modification_preconditions(request, meta)
607

    
608
    response = HttpResponse()
609
    put_container_headers(request, response, meta, policy)
610

    
611
    path = request.GET.get('path')
612
    prefix = request.GET.get('prefix')
613
    delimiter = request.GET.get('delimiter')
614

    
615
    # Path overrides prefix and delimiter.
616
    virtual = True
617
    if path:
618
        prefix = path
619
        delimiter = '/'
620
        virtual = False
621

    
622
    # Naming policy.
623
    if prefix and delimiter and not prefix.endswith(delimiter):
624
        prefix = prefix + delimiter
625
    if not prefix:
626
        prefix = ''
627
    prefix = prefix.lstrip('/')
628

    
629
    marker = request.GET.get('marker')
630
    limit = get_int_parameter(request.GET.get('limit'))
631
    if not limit:
632
        limit = 10000
633

    
634
    keys = request.GET.get('meta')
635
    if keys:
636
        keys = [smart_str(x.strip()) for x in keys.split(',')
637
                if x.strip() != '']
638
        included, excluded, opers = parse_filters(keys)
639
        keys = []
640
        keys += [format_header_key('X-Object-Meta-' + x) for x in included]
641
        keys += ['!' + format_header_key('X-Object-Meta-' + x)
642
                 for x in excluded]
643
        keys += ['%s%s%s' % (format_header_key(
644
            'X-Object-Meta-' + k), o, v) for k, o, v in opers]
645
    else:
646
        keys = []
647

    
648
    shared = False
649
    if 'shared' in request.GET:
650
        shared = True
651

    
652
    public_requested = 'public' in request.GET
653
    public_granted = public_requested and request.user_uniq == v_account
654

    
655
    if request.serialization == 'text':
656
        try:
657
            objects = request.backend.list_objects(
658
                request.user_uniq, v_account,
659
                v_container, prefix, delimiter, marker,
660
                limit, virtual, 'pithos', keys, shared,
661
                until, None, public_granted)
662
        except NotAllowedError:
663
            raise faults.Forbidden('Not allowed')
664
        except ItemNotExists:
665
            raise faults.ItemNotFound('Container does not exist')
666

    
667
        if len(objects) == 0:
668
            # The cloudfiles python bindings expect 200 if json/xml.
669
            response.status_code = 204
670
            return response
671
        response.status_code = 200
672
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
673
        return response
674

    
675
    try:
676
        objects = request.backend.list_object_meta(
677
            request.user_uniq, v_account,
678
            v_container, prefix, delimiter, marker,
679
            limit, virtual, 'pithos', keys, shared, until, None, public_granted)
680
        object_permissions = {}
681
        object_public = {}
682
        if until is None:
683
            name = '/'.join((v_account, v_container, ''))
684
            name_idx = len(name)
685
            for x in request.backend.list_object_permissions(request.user_uniq,
686
                                                             v_account, v_container, prefix):
687

    
688
                # filter out objects which are not under the container
689
                if name != x[:name_idx]:
690
                    continue
691

    
692
                object = x[name_idx:]
693
                object_permissions[object] = request.backend.get_object_permissions(
694
                    request.user_uniq, v_account, v_container, object)
695

    
696
            if public_granted:
697
                for k, v in request.backend.list_object_public(
698
                        request.user_uniq, v_account,
699
                        v_container, prefix).iteritems():
700
                    object_public[k[name_idx:]] = v
701
    except NotAllowedError:
702
        raise faults.Forbidden('Not allowed')
703
    except ItemNotExists:
704
        raise faults.ItemNotFound('Container does not exist')
705

    
706
    object_meta = []
707
    for meta in objects:
708
        if TRANSLATE_UUIDS:
709
            modified_by = meta.get('modified_by')
710
            if modified_by:
711
                l = retrieve_displaynames(
712
                        getattr(request, 'token', None), [meta['modified_by']])
713
                if l is not None and len(l) == 1:
714
                    meta['modified_by'] = l[0]
715

    
716
        if len(meta) == 1:
717
            # Virtual objects/directories.
718
            object_meta.append(meta)
719
        else:
720
            rename_meta_key(
721
                meta, 'hash', 'x_object_hash')  # Will be replaced by checksum.
722
            rename_meta_key(meta, 'checksum', 'hash')
723
            rename_meta_key(meta, 'type', 'content_type')
724
            rename_meta_key(meta, 'uuid', 'x_object_uuid')
725
            if until is not None and 'modified' in meta:
726
                del(meta['modified'])
727
            else:
728
                rename_meta_key(meta, 'modified', 'last_modified')
729
            rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
730
            rename_meta_key(meta, 'version', 'x_object_version')
731
            rename_meta_key(
732
                meta, 'version_timestamp', 'x_object_version_timestamp')
733
            permissions = object_permissions.get(meta['name'], None)
734
            if permissions:
735
                update_sharing_meta(request, permissions, v_account,
736
                                    v_container, meta['name'], meta)
737
            public_url = object_public.get(meta['name'], None)
738
            if public_granted:
739
                update_public_meta(public_url, meta)
740
            object_meta.append(printable_header_dict(meta))
741

    
742
    if request.serialization == 'xml':
743
        data = render_to_string(
744
            'objects.xml', {'container': v_container, 'objects': object_meta})
745
    elif request.serialization == 'json':
746
        data = json.dumps(object_meta, default=json_encode_decimal)
747
    response.status_code = 200
748
    response.content = data
749
    return response
750

    
751

    
752
@api_method('HEAD', user_required=True, logger=logger)
753
def object_meta(request, v_account, v_container, v_object):
754
    # Normal Response Codes: 204
755
    # Error Response Codes: internalServerError (500),
756
    #                       itemNotFound (404),
757
    #                       forbidden (403),
758
    #                       badRequest (400)
759

    
760
    version = request.GET.get('version')
761
    try:
762
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
763
                                               v_container, v_object, 'pithos', version)
764
        if version is None:
765
            permissions = request.backend.get_object_permissions(
766
                request.user_uniq,
767
                v_account, v_container, v_object)
768
            public = request.backend.get_object_public(
769
                request.user_uniq, v_account,
770
                v_container, v_object)
771
        else:
772
            permissions = None
773
            public = None
774
    except NotAllowedError:
775
        raise faults.Forbidden('Not allowed')
776
    except ItemNotExists:
777
        raise faults.ItemNotFound('Object does not exist')
778
    except VersionNotExists:
779
        raise faults.ItemNotFound('Version does not exist')
780

    
781
    update_manifest_meta(request, v_account, meta)
782
    update_sharing_meta(
783
        request, permissions, v_account, v_container, v_object, meta)
784
    if request.user_uniq == v_account:
785
        update_public_meta(public, meta)
786

    
787
    # Evaluate conditions.
788
    validate_modification_preconditions(request, meta)
789
    try:
790
        validate_matching_preconditions(request, meta)
791
    except faults.NotModified:
792
        response = HttpResponse(status=304)
793
        response['ETag'] = meta['checksum']
794
        return response
795

    
796
    response = HttpResponse(status=200)
797
    put_object_headers(response, meta, token=getattr(request, 'token', None))
798
    return response
799

    
800

    
801
@api_method('GET', format_allowed=True, user_required=True, logger=logger)
802
def object_read(request, v_account, v_container, v_object):
803
    # Normal Response Codes: 200, 206
804
    # Error Response Codes: internalServerError (500),
805
    #                       rangeNotSatisfiable (416),
806
    #                       preconditionFailed (412),
807
    #                       itemNotFound (404),
808
    #                       forbidden (403),
809
    #                       badRequest (400),
810
    #                       notModified (304)
811

    
812
    version = request.GET.get('version')
813

    
814
    # Reply with the version list. Do this first, as the object may be deleted.
815
    if version == 'list':
816
        if request.serialization == 'text':
817
            raise faults.BadRequest('No format specified for version list.')
818

    
819
        try:
820
            v = request.backend.list_versions(request.user_uniq, v_account,
821
                                              v_container, v_object)
822
        except NotAllowedError:
823
            raise faults.Forbidden('Not allowed')
824
        except ItemNotExists:
825
            raise faults.ItemNotFound('Object does not exist')
826
        d = {'versions': v}
827
        if request.serialization == 'xml':
828
            d['object'] = v_object
829
            data = render_to_string('versions.xml', d)
830
        elif request.serialization == 'json':
831
            data = json.dumps(d, default=json_encode_decimal)
832

    
833
        response = HttpResponse(data, status=200)
834
        response['Content-Length'] = len(data)
835
        return response
836

    
837
    try:
838
        meta = request.backend.get_object_meta(request.user_uniq, v_account,
839
                                               v_container, v_object, 'pithos', version)
840
        if version is None:
841
            permissions = request.backend.get_object_permissions(
842
                request.user_uniq,
843
                v_account, v_container, v_object)
844
            public = request.backend.get_object_public(
845
                request.user_uniq, v_account,
846
                v_container, v_object)
847
        else:
848
            permissions = None
849
            public = None
850
    except NotAllowedError:
851
        raise faults.Forbidden('Not allowed')
852
    except ItemNotExists:
853
        raise faults.ItemNotFound('Object does not exist')
854
    except VersionNotExists:
855
        raise faults.ItemNotFound('Version does not exist')
856

    
857
    update_manifest_meta(request, v_account, meta)
858
    update_sharing_meta(
859
        request, permissions, v_account, v_container, v_object, meta)
860
    if request.user_uniq == v_account:
861
        update_public_meta(public, meta)
862

    
863
    # Evaluate conditions.
864
    validate_modification_preconditions(request, meta)
865
    try:
866
        validate_matching_preconditions(request, meta)
867
    except faults.NotModified:
868
        response = HttpResponse(status=304)
869
        response['ETag'] = meta['checksum']
870
        return response
871

    
872
    hashmap_reply = False
873
    if 'hashmap' in request.GET and request.serialization != 'text':
874
        hashmap_reply = True
875

    
876
    sizes = []
877
    hashmaps = []
878
    if 'X-Object-Manifest' in meta and not hashmap_reply:
879
        try:
880
            src_container, src_name = split_container_object_string(
881
                '/' + meta['X-Object-Manifest'])
882
            objects = request.backend.list_objects(
883
                request.user_uniq, v_account,
884
                src_container, prefix=src_name, virtual=False)
885
        except NotAllowedError:
886
            raise faults.Forbidden('Not allowed')
887
        except ValueError:
888
            raise faults.BadRequest('Invalid X-Object-Manifest header')
889
        except ItemNotExists:
890
            raise faults.ItemNotFound('Container does not exist')
891

    
892
        try:
893
            for x in objects:
894
                s, h = request.backend.get_object_hashmap(request.user_uniq,
895
                                                          v_account, src_container, x[0], x[1])
896
                sizes.append(s)
897
                hashmaps.append(h)
898
        except NotAllowedError:
899
            raise faults.Forbidden('Not allowed')
900
        except ItemNotExists:
901
            raise faults.ItemNotFound('Object does not exist')
902
        except VersionNotExists:
903
            raise faults.ItemNotFound('Version does not exist')
904
    else:
905
        try:
906
            s, h = request.backend.get_object_hashmap(
907
                request.user_uniq, v_account,
908
                v_container, v_object, version)
909
            sizes.append(s)
910
            hashmaps.append(h)
911
        except NotAllowedError:
912
            raise faults.Forbidden('Not allowed')
913
        except ItemNotExists:
914
            raise faults.ItemNotFound('Object does not exist')
915
        except VersionNotExists:
916
            raise faults.ItemNotFound('Version does not exist')
917

    
918
    # Reply with the hashmap.
919
    if hashmap_reply:
920
        size = sum(sizes)
921
        hashmap = sum(hashmaps, [])
922
        d = {
923
            'block_size': request.backend.block_size,
924
            'block_hash': request.backend.hash_algorithm,
925
            'bytes': size,
926
            'hashes': hashmap}
927
        if request.serialization == 'xml':
928
            d['object'] = v_object
929
            data = render_to_string('hashes.xml', d)
930
        elif request.serialization == 'json':
931
            data = json.dumps(d)
932

    
933
        response = HttpResponse(data, status=200)
934
        put_object_headers(
935
                response, meta, token=getattr(request, 'token', None))
936
        response['Content-Length'] = len(data)
937
        return response
938

    
939
    request.serialization = 'text'  # Unset.
940
    return object_data_response(request, sizes, hashmaps, meta)
941

    
942

    
943
@api_method('PUT', format_allowed=True, user_required=True, logger=logger)
944
def object_write(request, v_account, v_container, v_object):
945
    # Normal Response Codes: 201
946
    # Error Response Codes: internalServerError (500),
947
    #                       unprocessableEntity (422),
948
    #                       lengthRequired (411),
949
    #                       conflict (409),
950
    #                       itemNotFound (404),
951
    #                       forbidden (403),
952
    #                       badRequest (400)
953
    #                       requestentitytoolarge (413)
954

    
955
    # Evaluate conditions.
956
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
957
        try:
958
            meta = request.backend.get_object_meta(
959
                request.user_uniq, v_account,
960
                v_container, v_object, 'pithos')
961
        except NotAllowedError:
962
            raise faults.Forbidden('Not allowed')
963
        except NameError:
964
            meta = {}
965
        validate_matching_preconditions(request, meta)
966

    
967
    copy_from = request.META.get('HTTP_X_COPY_FROM')
968
    move_from = request.META.get('HTTP_X_MOVE_FROM')
969
    if copy_from or move_from:
970
        delimiter = request.GET.get('delimiter')
971
        content_length = get_content_length(request)  # Required by the API.
972

    
973
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
974

    
975
        if not src_account:
976
            src_account = request.user_uniq
977
        else:
978
            if TRANSLATE_UUIDS:
979
                try:
980
                    src_account = retrieve_uuid(getattr(request, 'token', None),
981
                                                src_account)
982
                except ItemNotExists:
983
                    faults.ItemNotFound('Invalid source account')
984

    
985
        if move_from:
986
            try:
987
                src_container, src_name = split_container_object_string(
988
                    move_from)
989
            except ValueError:
990
                raise faults.BadRequest('Invalid X-Move-From header')
991
            version_id = copy_or_move_object(
992
                request, src_account, src_container, src_name,
993
                v_account, v_container, v_object, move=True, delimiter=delimiter)
994
        else:
995
            try:
996
                src_container, src_name = split_container_object_string(
997
                    copy_from)
998
            except ValueError:
999
                raise faults.BadRequest('Invalid X-Copy-From header')
1000
            version_id = copy_or_move_object(
1001
                request, src_account, src_container, src_name,
1002
                v_account, v_container, v_object, move=False, delimiter=delimiter)
1003
        response = HttpResponse(status=201)
1004
        response['X-Object-Version'] = version_id
1005
        return response
1006

    
1007
    content_type, meta, permissions, public = get_object_headers(request)
1008
    content_length = -1
1009
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1010
        content_length = get_content_length(request)
1011
    # Should be BadRequest, but API says otherwise.
1012
    if content_type is None:
1013
        raise faults.LengthRequired('Missing Content-Type header')
1014

    
1015
    if 'hashmap' in request.GET:
1016
        if request.serialization not in ('json', 'xml'):
1017
            raise faults.BadRequest('Invalid hashmap format')
1018

    
1019
        data = ''
1020
        for block in socket_read_iterator(request, content_length,
1021
                                          request.backend.block_size):
1022
            data = '%s%s' % (data, block)
1023

    
1024
        if request.serialization == 'json':
1025
            d = json.loads(data)
1026
            if not hasattr(d, '__getitem__'):
1027
                raise faults.BadRequest('Invalid data formating')
1028
            try:
1029
                hashmap = d['hashes']
1030
                size = int(d['bytes'])
1031
            except:
1032
                raise faults.BadRequest('Invalid data formatting')
1033
        elif request.serialization == 'xml':
1034
            try:
1035
                xml = minidom.parseString(data)
1036
                obj = xml.getElementsByTagName('object')[0]
1037
                size = int(obj.attributes['bytes'].value)
1038

    
1039
                hashes = xml.getElementsByTagName('hash')
1040
                hashmap = []
1041
                for hash in hashes:
1042
                    hashmap.append(hash.firstChild.data)
1043
            except:
1044
                raise faults.BadRequest('Invalid data formatting')
1045

    
1046
        checksum = ''  # Do not set to None (will copy previous value).
1047
    else:
1048
        md5 = hashlib.md5()
1049
        size = 0
1050
        hashmap = []
1051
        for data in socket_read_iterator(request, content_length,
1052
                                         request.backend.block_size):
1053
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1054
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1055
            size += len(data)
1056
            hashmap.append(request.backend.put_block(data))
1057
            md5.update(data)
1058

    
1059
        checksum = md5.hexdigest().lower()
1060
        etag = request.META.get('HTTP_ETAG')
1061
        if etag and parse_etags(etag)[0].lower() != checksum:
1062
            raise faults.UnprocessableEntity('Object ETag does not match')
1063

    
1064
    try:
1065
        version_id = request.backend.update_object_hashmap(request.user_uniq,
1066
                                                           v_account, v_container, v_object, size, content_type,
1067
                                                           hashmap, checksum, 'pithos', meta, True, permissions)
1068
    except NotAllowedError:
1069
        raise faults.Forbidden('Not allowed')
1070
    except IndexError, e:
1071
        raise faults.Conflict(simple_list_response(request, e.data))
1072
    except ItemNotExists:
1073
        raise faults.ItemNotFound('Container does not exist')
1074
    except ValueError:
1075
        raise faults.BadRequest('Invalid sharing header')
1076
    except QuotaError, e:
1077
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1078
    if not checksum and UPDATE_MD5:
1079
        # Update the MD5 after the hashmap, as there may be missing hashes.
1080
        checksum = hashmap_md5(request.backend, hashmap, size)
1081
        try:
1082
            request.backend.update_object_checksum(request.user_uniq,
1083
                                                   v_account, v_container, v_object, version_id, checksum)
1084
        except NotAllowedError:
1085
            raise faults.Forbidden('Not allowed')
1086
    if public is not None:
1087
        try:
1088
            request.backend.update_object_public(request.user_uniq, v_account,
1089
                                                 v_container, v_object, public)
1090
        except NotAllowedError:
1091
            raise faults.Forbidden('Not allowed')
1092
        except ItemNotExists:
1093
            raise faults.ItemNotFound('Object does not exist')
1094

    
1095
    response = HttpResponse(status=201)
1096
    if checksum:
1097
        response['ETag'] = checksum
1098
    response['X-Object-Version'] = version_id
1099
    return response
1100

    
1101

    
1102
@api_method('POST', user_required=True, logger=logger)
1103
def object_write_form(request, v_account, v_container, v_object):
1104
    # Normal Response Codes: 201
1105
    # Error Response Codes: internalServerError (500),
1106
    #                       itemNotFound (404),
1107
    #                       forbidden (403),
1108
    #                       badRequest (400)
1109
    #                       requestentitytoolarge (413)
1110

    
1111
    request.upload_handlers = [SaveToBackendHandler(request)]
1112
    if 'X-Object-Data' not in request.FILES:
1113
        raise faults.BadRequest('Missing X-Object-Data field')
1114
    file = request.FILES['X-Object-Data']
1115

    
1116
    checksum = file.etag
1117
    try:
1118
        version_id = request.backend.update_object_hashmap(request.user_uniq,
1119
                                                           v_account, v_container, v_object, file.size, file.content_type,
1120
                                                           file.hashmap, checksum, 'pithos', {}, True)
1121
    except NotAllowedError:
1122
        raise faults.Forbidden('Not allowed')
1123
    except ItemNotExists:
1124
        raise faults.ItemNotFound('Container does not exist')
1125
    except QuotaError, e:
1126
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1127

    
1128
    response = HttpResponse(status=201)
1129
    response['ETag'] = checksum
1130
    response['X-Object-Version'] = version_id
1131
    response.content = checksum
1132
    return response
1133

    
1134

    
1135
@api_method('COPY', format_allowed=True, user_required=True, logger=logger)
1136
def object_copy(request, v_account, v_container, v_object):
1137
    # Normal Response Codes: 201
1138
    # Error Response Codes: internalServerError (500),
1139
    #                       itemNotFound (404),
1140
    #                       forbidden (403),
1141
    #                       badRequest (400)
1142
    #                       requestentitytoolarge (413)
1143

    
1144
    dest_account = request.META.get('HTTP_DESTINATION_ACCOUNT')
1145
    if not dest_account:
1146
        dest_account = request.user_uniq
1147
    dest_path = request.META.get('HTTP_DESTINATION')
1148
    if not dest_path:
1149
        raise faults.BadRequest('Missing Destination header')
1150
    try:
1151
        dest_container, dest_name = split_container_object_string(dest_path)
1152
    except ValueError:
1153
        raise faults.BadRequest('Invalid Destination header')
1154

    
1155
    # Evaluate conditions.
1156
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1157
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1158
        try:
1159
            meta = request.backend.get_object_meta(
1160
                request.user_uniq, v_account,
1161
                v_container, v_object, 'pithos', src_version)
1162
        except NotAllowedError:
1163
            raise faults.Forbidden('Not allowed')
1164
        except (ItemNotExists, VersionNotExists):
1165
            raise faults.ItemNotFound('Container or object does not exist')
1166
        validate_matching_preconditions(request, meta)
1167

    
1168
    delimiter = request.GET.get('delimiter')
1169

    
1170
    version_id = copy_or_move_object(request, v_account, v_container, v_object,
1171
                                     dest_account, dest_container, dest_name, move=False, delimiter=delimiter)
1172
    response = HttpResponse(status=201)
1173
    response['X-Object-Version'] = version_id
1174
    return response
1175

    
1176

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

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

    
1197
    # Evaluate conditions.
1198
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1199
        try:
1200
            meta = request.backend.get_object_meta(
1201
                request.user_uniq, v_account,
1202
                v_container, v_object, 'pithos')
1203
        except NotAllowedError:
1204
            raise faults.Forbidden('Not allowed')
1205
        except ItemNotExists:
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, move=True, delimiter=delimiter)
1213
    response = HttpResponse(status=201)
1214
    response['X-Object-Version'] = version_id
1215
    return response
1216

    
1217

    
1218
@api_method('POST', format_allowed=True, user_required=True, logger=logger)
1219
def object_update(request, v_account, v_container, v_object):
1220
    # Normal Response Codes: 202, 204
1221
    # Error Response Codes: internalServerError (500),
1222
    #                       conflict (409),
1223
    #                       itemNotFound (404),
1224
    #                       forbidden (403),
1225
    #                       badRequest (400)
1226

    
1227
    content_type, meta, permissions, public = get_object_headers(request)
1228

    
1229
    try:
1230
        prev_meta = request.backend.get_object_meta(
1231
            request.user_uniq, v_account,
1232
            v_container, v_object, 'pithos')
1233
    except NotAllowedError:
1234
        raise faults.Forbidden('Not allowed')
1235
    except ItemNotExists:
1236
        raise faults.ItemNotFound('Object does not exist')
1237

    
1238
    # Evaluate conditions.
1239
    if request.META.get('HTTP_IF_MATCH') or request.META.get('HTTP_IF_NONE_MATCH'):
1240
        validate_matching_preconditions(request, prev_meta)
1241

    
1242
    replace = True
1243
    if 'update' in request.GET:
1244
        replace = False
1245

    
1246
    # A Content-Type or X-Source-Object header indicates data updates.
1247
    src_object = request.META.get('HTTP_X_SOURCE_OBJECT')
1248
    if (not content_type or content_type != 'application/octet-stream') and not src_object:
1249
        response = HttpResponse(status=202)
1250

    
1251
        # Do permissions first, as it may fail easier.
1252
        if permissions is not None:
1253
            try:
1254
                request.backend.update_object_permissions(request.user_uniq,
1255
                                                          v_account, v_container, v_object, permissions)
1256
            except NotAllowedError:
1257
                raise faults.Forbidden('Not allowed')
1258
            except ItemNotExists:
1259
                raise faults.ItemNotFound('Object does not exist')
1260
            except ValueError:
1261
                raise faults.BadRequest('Invalid sharing header')
1262
        if public is not None:
1263
            try:
1264
                request.backend.update_object_public(
1265
                    request.user_uniq, v_account,
1266
                    v_container, v_object, public)
1267
            except NotAllowedError:
1268
                raise faults.Forbidden('Not allowed')
1269
            except ItemNotExists:
1270
                raise faults.ItemNotFound('Object does not exist')
1271
        if meta or replace:
1272
            try:
1273
                version_id = request.backend.update_object_meta(
1274
                    request.user_uniq,
1275
                    v_account, v_container, v_object, 'pithos', meta, replace)
1276
            except NotAllowedError:
1277
                raise faults.Forbidden('Not allowed')
1278
            except ItemNotExists:
1279
                raise faults.ItemNotFound('Object does not exist')
1280
            response['X-Object-Version'] = version_id
1281

    
1282
        return response
1283

    
1284
    # Single range update. Range must be in Content-Range.
1285
    # Based on: http://code.google.com/p/gears/wiki/ContentRangePostProposal
1286
    # (with the addition that '*' is allowed for the range - will append).
1287
    content_range = request.META.get('HTTP_CONTENT_RANGE')
1288
    if not content_range:
1289
        raise faults.BadRequest('Missing Content-Range header')
1290
    ranges = get_content_range(request)
1291
    if not ranges:
1292
        raise faults.RangeNotSatisfiable('Invalid Content-Range header')
1293

    
1294
    try:
1295
        size, hashmap = request.backend.get_object_hashmap(request.user_uniq,
1296
                                                           v_account, v_container, v_object)
1297
    except NotAllowedError:
1298
        raise faults.Forbidden('Not allowed')
1299
    except ItemNotExists:
1300
        raise faults.ItemNotFound('Object does not exist')
1301

    
1302
    offset, length, total = ranges
1303
    if offset is None:
1304
        offset = size
1305
    elif offset > size:
1306
        raise faults.RangeNotSatisfiable('Supplied offset is beyond object limits')
1307
    if src_object:
1308
        src_account = request.META.get('HTTP_X_SOURCE_ACCOUNT')
1309
        if not src_account:
1310
            src_account = request.user_uniq
1311
        src_container, src_name = split_container_object_string(src_object)
1312
        src_version = request.META.get('HTTP_X_SOURCE_VERSION')
1313
        try:
1314
            src_size, src_hashmap = request.backend.get_object_hashmap(
1315
                request.user_uniq,
1316
                src_account, src_container, src_name, src_version)
1317
        except NotAllowedError:
1318
            raise faults.Forbidden('Not allowed')
1319
        except ItemNotExists:
1320
            raise faults.ItemNotFound('Source object does not exist')
1321

    
1322
        if length is None:
1323
            length = src_size
1324
        elif length > src_size:
1325
            raise faults.BadRequest('Object length is smaller than range length')
1326
    else:
1327
        # Require either a Content-Length, or 'chunked' Transfer-Encoding.
1328
        content_length = -1
1329
        if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
1330
            content_length = get_content_length(request)
1331

    
1332
        if length is None:
1333
            length = content_length
1334
        else:
1335
            if content_length == -1:
1336
                # TODO: Get up to length bytes in chunks.
1337
                length = content_length
1338
            elif length != content_length:
1339
                raise faults.BadRequest('Content length does not match range length')
1340
    if total is not None and (total != size or offset >= size or (length > 0 and offset + length >= size)):
1341
        raise faults.RangeNotSatisfiable(
1342
            'Supplied range will change provided object limits')
1343

    
1344
    dest_bytes = request.META.get('HTTP_X_OBJECT_BYTES')
1345
    if dest_bytes is not None:
1346
        dest_bytes = get_int_parameter(dest_bytes)
1347
        if dest_bytes is None:
1348
            raise faults.BadRequest('Invalid X-Object-Bytes header')
1349

    
1350
    if src_object:
1351
        if offset % request.backend.block_size == 0:
1352
            # Update the hashes only.
1353
            sbi = 0
1354
            while length > 0:
1355
                bi = int(offset / request.backend.block_size)
1356
                bl = min(length, request.backend.block_size)
1357
                if bi < len(hashmap):
1358
                    if bl == request.backend.block_size:
1359
                        hashmap[bi] = src_hashmap[sbi]
1360
                    else:
1361
                        data = request.backend.get_block(src_hashmap[sbi])
1362
                        hashmap[bi] = request.backend.update_block(hashmap[bi],
1363
                                                                   data[:bl], 0)
1364
                else:
1365
                    hashmap.append(src_hashmap[sbi])
1366
                offset += bl
1367
                length -= bl
1368
                sbi += 1
1369
        else:
1370
            data = ''
1371
            sbi = 0
1372
            while length > 0:
1373
                data += request.backend.get_block(src_hashmap[sbi])
1374
                if length < request.backend.block_size:
1375
                    data = data[:length]
1376
                bytes = put_object_block(request, hashmap, data, offset)
1377
                offset += bytes
1378
                data = data[bytes:]
1379
                length -= bytes
1380
                sbi += 1
1381
    else:
1382
        data = ''
1383
        for d in socket_read_iterator(request, length,
1384
                                      request.backend.block_size):
1385
            # TODO: Raise 408 (Request Timeout) if this takes too long.
1386
            # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
1387
            data += d
1388
            bytes = put_object_block(request, hashmap, data, offset)
1389
            offset += bytes
1390
            data = data[bytes:]
1391
        if len(data) > 0:
1392
            put_object_block(request, hashmap, data, offset)
1393

    
1394
    if offset > size:
1395
        size = offset
1396
    if dest_bytes is not None and dest_bytes < size:
1397
        size = dest_bytes
1398
        hashmap = hashmap[:(int((size - 1) / request.backend.block_size) + 1)]
1399
    checksum = hashmap_md5(
1400
        request.backend, hashmap, size) if UPDATE_MD5 else ''
1401
    try:
1402
        version_id = request.backend.update_object_hashmap(request.user_uniq,
1403
                                                           v_account, v_container, v_object, size, prev_meta[
1404
                                                           'type'],
1405
                                                           hashmap, checksum, 'pithos', meta, replace, permissions)
1406
    except NotAllowedError:
1407
        raise faults.Forbidden('Not allowed')
1408
    except ItemNotExists:
1409
        raise faults.ItemNotFound('Container does not exist')
1410
    except ValueError:
1411
        raise faults.BadRequest('Invalid sharing header')
1412
    except QuotaError, e:
1413
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1414
    if public is not None:
1415
        try:
1416
            request.backend.update_object_public(request.user_uniq, v_account,
1417
                                                 v_container, v_object, public)
1418
        except NotAllowedError:
1419
            raise faults.Forbidden('Not allowed')
1420
        except ItemNotExists:
1421
            raise faults.ItemNotFound('Object does not exist')
1422

    
1423
    response = HttpResponse(status=204)
1424
    response['ETag'] = checksum
1425
    response['X-Object-Version'] = version_id
1426
    return response
1427

    
1428

    
1429
@api_method('DELETE', user_required=True, logger=logger)
1430
def object_delete(request, v_account, v_container, v_object):
1431
    # Normal Response Codes: 204
1432
    # Error Response Codes: internalServerError (500),
1433
    #                       itemNotFound (404),
1434
    #                       forbidden (403),
1435
    #                       badRequest (400)
1436
    #                       requestentitytoolarge (413)
1437

    
1438
    until = get_int_parameter(request.GET.get('until'))
1439
    delimiter = request.GET.get('delimiter')
1440

    
1441
    try:
1442
        request.backend.delete_object(
1443
            request.user_uniq, v_account, v_container,
1444
            v_object, until, delimiter=delimiter)
1445
    except NotAllowedError:
1446
        raise faults.Forbidden('Not allowed')
1447
    except ItemNotExists:
1448
        raise faults.ItemNotFound('Object does not exist')
1449
    except QuotaError, e:
1450
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
1451
    return HttpResponse(status=204)