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

Antony Chazapis's avatar
Antony Chazapis committed
34
from xml.dom import minidom
35
36
37
38
39

from django.http import HttpResponse
from django.template.loader import render_to_string
from django.utils import simplejson as json
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 snf_django.lib.astakos import get_uuids as _get_uuids
44

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

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

63
from pithos.api.settings import (UPDATE_MD5, TRANSLATE_UUIDS,
64
                                 SERVICE_TOKEN, ASTAKOS_URL)
65

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
66
67
68
from pithos.backends.base import (
    NotAllowedError, QuotaError, ContainerNotEmpty, ItemNotExists,
    VersionNotExists, ContainerExists)
69

Antony Chazapis's avatar
Antony Chazapis committed
70
71
from pithos.backends.filter import parse_filters

Antony Chazapis's avatar
Antony Chazapis committed
72
73
import hashlib

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

77

78
79
def get_uuids(names):
    try:
80
        url = ASTAKOS_URL + "/service/api/user_catalogs"
81
        uuids = _get_uuids(SERVICE_TOKEN, names, url=url)
82
83
84
85
86
87
    except Exception, e:
        logger.exception(e)
        return {}

    return uuids

88

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

103

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

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

122

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

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

145

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

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

177

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

185
186
187
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
    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
192
193
    uri = request.build_absolute_uri()
    if '?' in uri:
        uri = uri[:uri.find('?')]
194

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

200

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

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

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

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

227
228
    account_meta = []
    for x in accounts:
229
230
        if x == request.user_uniq:
            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
242
            rename_meta_key(
                meta, 'until_timestamp', 'x_account_until_timestamp')
243
            if groups:
244
245
                meta['X-Account-Group'] = printable_header_dict(
                    dict([(k, ','.join(v)) for k, v in groups.iteritems()]))
246
            account_meta.append(printable_header_dict(meta))
247
248
249
250

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

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

263

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

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

        if TRANSLATE_UUIDS:
            for k in groups:
                groups[k] = retrieve_displaynames(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
283
                    getattr(request, 'token', None), groups[k])
284
        policy = request.backend.get_account_policy(
285
            request.user_uniq, v_account, external_quota=usage)
Antony Chazapis's avatar
Antony Chazapis committed
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
294
    return response

295

296
@api_method('POST', user_required=True, logger=logger)
297
298
def account_update(request, v_account):
    # 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
305
306
307
    for k in groups:
        if TRANSLATE_UUIDS:
            try:
                groups[k] = retrieve_uuids(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
308
309
310
                    getattr(request, 'token', None),
                    groups[k],
                    fail_silently=False)
311
            except ItemNotExists, e:
312
                raise faults.BadRequest(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
313
                    'Bad X-Account-Group header value: %s' % e)
314
315
316
317
318
319
320
        else:
            try:
                retrieve_displaynames(
                    getattr(request, 'token', None),
                    groups[k],
                    fail_silently=False)
            except ItemNotExists, e:
321
                raise faults.BadRequest(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
322
                    'Bad X-Account-Group header value: %s' % e)
323
324
    replace = True
    if 'update' in request.GET:
325
        replace = False
326
327
    if groups:
        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
335
    if meta or replace:
        try:
336
337
            request.backend.update_account_meta(request.user_uniq, v_account,
                                                'pithos', meta, replace)
338
        except NotAllowedError:
339
            raise faults.Forbidden('Not allowed')
340
341
    return HttpResponse(status=202)

342

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

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

364
    validate_modification_preconditions(request, meta)
365

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

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

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

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

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

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

428

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

Antony Chazapis's avatar
Antony Chazapis committed
437
    until = get_int_parameter(request.GET.get('until'))
438
    try:
439
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
440
                                                  v_container, 'pithos', until)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
441
442
443
444
        meta['object_meta'] = \
            request.backend.list_container_meta(request.user_uniq,
                                                v_account, v_container,
                                                'pithos', until)
445
446
447
        policy = request.backend.get_container_policy(
            request.user_uniq, v_account,
            v_container)
Antony Chazapis's avatar
Antony Chazapis committed
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
458
    return response

459

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

Antony Chazapis's avatar
Antony Chazapis committed
468
    meta, policy = get_container_headers(request)
469

470
    try:
471
472
        request.backend.put_container(
            request.user_uniq, v_account, v_container, policy)
473
        ret = 201
Antony Chazapis's avatar
Antony Chazapis committed
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:
Antony Chazapis's avatar
Antony Chazapis committed
482
        try:
483
484
485
            request.backend.update_container_policy(
                request.user_uniq, v_account,
                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
493
    if meta:
        try:
494
            request.backend.update_container_meta(request.user_uniq, v_account,
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
495
496
                                                  v_container, 'pithos',
                                                  meta, replace=False)
Antony Chazapis's avatar
Antony Chazapis committed
497
        except NotAllowedError:
498
            raise faults.Forbidden('Not allowed')
499
        except ItemNotExists:
500
            raise faults.ItemNotFound('Container does not exist')
501

502
503
    return HttpResponse(status=ret)

504

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

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

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

554
555
    response = HttpResponse(status=202)
    if hashmap:
556
        response.content = simple_list_response(request, hashmap)
557
    return response
558

559

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

570
    until = get_int_parameter(request.GET.get('until'))
571

572
    delimiter = request.GET.get('delimiter')
573

574
    try:
575
576
577
        request.backend.delete_container(
            request.user_uniq, v_account, v_container,
            until, delimiter=delimiter)
Antony Chazapis's avatar
Antony Chazapis committed
578
    except NotAllowedError:
579
        raise faults.Forbidden('Not allowed')
580
    except ItemNotExists:
581
        raise faults.ItemNotFound('Container does not exist')
582
    except ContainerNotEmpty:
583
        raise faults.Conflict('Container is not empty')
584
    except QuotaError, e:
585
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
586
587
    return HttpResponse(status=204)

588

589
@api_method('GET', format_allowed=True, user_required=True, logger=logger)
590
591
def object_list(request, v_account, v_container):
    # Normal Response Codes: 200, 204
592
    # Error Response Codes: internalServerError (500),
593
    #                       itemNotFound (404),
594
    #                       forbidden (403),
595
    #                       badRequest (400)
596

Antony Chazapis's avatar
Antony Chazapis committed
597
    until = get_int_parameter(request.GET.get('until'))
598
    try:
599
        meta = request.backend.get_container_meta(request.user_uniq, v_account,
600
                                                  v_container, 'pithos', until)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
601
602
603
604
        meta['object_meta'] = \
            request.backend.list_container_meta(request.user_uniq,
                                                v_account, v_container,
                                                'pithos', until)
605
606
607
        policy = request.backend.get_container_policy(
            request.user_uniq, v_account,
            v_container)
Antony Chazapis's avatar
Antony Chazapis committed
608
    except NotAllowedError:
609
        raise faults.Forbidden('Not allowed')
610
    except ItemNotExists:
611
        raise faults.ItemNotFound('Container does not exist')
612

613
    validate_modification_preconditions(request, meta)
614

615
    response = HttpResponse()
616
    put_container_headers(request, response, meta, policy)
617

618
619
620
    path = request.GET.get('path')
    prefix = request.GET.get('prefix')
    delimiter = request.GET.get('delimiter')
621

622
623
624
625
626
627
    # Path overrides prefix and delimiter.
    virtual = True
    if path:
        prefix = path
        delimiter = '/'
        virtual = False
628

629
    # Naming policy.
630
    if prefix and delimiter and not prefix.endswith(delimiter):
631
632
633
634
        prefix = prefix + delimiter
    if not prefix:
        prefix = ''
    prefix = prefix.lstrip('/')
635

636
    marker = request.GET.get('marker')
637
638
639
    limit = get_int_parameter(request.GET.get('limit'))
    if not limit:
        limit = 10000
640

641
642
    keys = request.GET.get('meta')
    if keys:
643
644
        keys = [smart_str(x.strip()) for x in keys.split(',')
                if x.strip() != '']
645
646
647
        included, excluded, opers = parse_filters(keys)
        keys = []
        keys += [format_header_key('X-Object-Meta-' + x) for x in included]
648
649
650
651
        keys += ['!' + format_header_key('X-Object-Meta-' + x)
                 for x in excluded]
        keys += ['%s%s%s' % (format_header_key(
            'X-Object-Meta-' + k), o, v) for k, o, v in opers]
652
653
    else:
        keys = []
654

655
656
657
    shared = False
    if 'shared' in request.GET:
        shared = True
658
659
660

    public_requested = 'public' in request.GET
    public_granted = public_requested and request.user_uniq == v_account
661

662
    if request.serialization == 'text':
663
        try:
664
665
666
667
            objects = request.backend.list_objects(
                request.user_uniq, v_account,
                v_container, prefix, delimiter, marker,
                limit, virtual, 'pithos', keys, shared,
668
                until, None, public_granted)
669
        except NotAllowedError:
670
            raise faults.Forbidden('Not allowed')
671
        except ItemNotExists:
672
            raise faults.ItemNotFound('Container does not exist')
673

674
675
676
677
678
        if len(objects) == 0:
            # The cloudfiles python bindings expect 200 if json/xml.
            response.status_code = 204
            return response
        response.status_code = 200
679
        response.content = '\n'.join([x[0] for x in objects]) + '\n'
680
        return response
681

682
    try:
683
684
685
        objects = request.backend.list_object_meta(
            request.user_uniq, v_account,
            v_container, prefix, delimiter, marker,
686
            limit, virtual, 'pithos', keys, shared, until, None, public_granted)
687
688
689
        object_permissions = {}
        object_public = {}
        if until is None:
690
691
            name = '/'.join((v_account, v_container, ''))
            name_idx = len(name)
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
692
693
            for x in request.backend.list_object_permissions(
                    request.user_uniq, v_account, v_container, prefix):
694
695
696
697
698

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

699
                object = x[name_idx:]
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
700
701
702
                object_permissions[object] = \
                    request.backend.get_object_permissions(
                        request.user_uniq, v_account, v_container, object)
703
704
705
706
707
708

            if public_granted:
                for k, v in request.backend.list_object_public(
                        request.user_uniq, v_account,
                        v_container, prefix).iteritems():
                    object_public[k[name_idx:]] = v
709
    except NotAllowedError:
710
        raise faults.Forbidden('Not allowed')
711
    except ItemNotExists:
712
        raise faults.ItemNotFound('Container does not exist')
713

714
    object_meta = []
715
    for meta in objects:
716
717
718
719
        if TRANSLATE_UUIDS:
            modified_by = meta.get('modified_by')
            if modified_by:
                l = retrieve_displaynames(
Ilias Tsitsimpis's avatar
Ilias Tsitsimpis committed
720
                    getattr(request, 'token', None), [meta['modified_by']])
721
722
723
                if l is not None and len(l) == 1:
                    meta['modified_by'] = l[0]

724
        if len(meta) == 1:
725
            # Virtual objects/directories.
726
            object_meta.append(meta)
727
        else:
728
729
            rename_meta_key(
                meta, 'hash', 'x_object_hash')  # Will be replaced by checksum.
730
731
732
733
734
            rename_meta_key(meta, 'checksum', 'hash')
            rename_meta_key(meta, 'type', 'content_type')
            rename_meta_key(meta, 'uuid', 'x_object_uuid')
            if until is not None and 'modified' in meta:
                del(meta['modified'])
Antony Chazapis's avatar
Antony Chazapis committed
735
            else:
736
                rename_meta_key(meta, 'modified', 'last_modified')
737
738
            rename_meta_key(meta, 'modified_by', 'x_object_modified_by')
            rename_meta_key(meta, 'version', 'x_object_version')
739
740
            rename_meta_key(
                meta, 'version_timestamp', 'x_object_version_timestamp')
741
742
            permissions = object_permissions.get(meta['name'], None)
            if permissions:
743
744
                update_sharing_meta(request, permissions, v_account,
                                    v_container, meta['name'], meta)
745
746
747
            public_url = object_public.get(meta['name'], None)
            if public_granted:
                update_public_meta(public_url, meta)
748
            object_meta.append(printable_header_dict(meta))
749

750
    if request.serialization == 'xml':