Revision b956618e pithos/api/functions.py

b/pithos/api/functions.py
1
#
2
# Copyright (c) 2011 Greek Research and Technology Network
3
#
4

  
5
from django.http import HttpResponse
6
from django.template.loader import render_to_string
7
from django.utils import simplejson as json
8
from django.utils.http import http_date, parse_etags
9

  
10
from pithos.api.compat import parse_http_date_safe
11

  
12
from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity
13
from pithos.api.util import get_meta, get_range, api_method
14
from pithos.backends.dummy import BackEnd
15

  
16
import os
17
import datetime
18
import logging
19

  
20
from settings import PROJECT_PATH
21
STORAGE_PATH = os.path.join(PROJECT_PATH, 'data')
22

  
23
logger = logging.getLogger(__name__)
24

  
25
@api_method('GET')
26
def authenticate(request):
27
    # Normal Response Codes: 204
28
    # Error Response Codes: serviceUnavailable (503),
29
    #                       unauthorized (401),
30
    #                       badRequest (400)
31
    
32
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
33
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
34
    
35
    if not x_auth_user or not x_auth_key:
36
        raise BadRequest('Missing auth user or key.')
37
    
38
    response = HttpResponse(status = 204)
39
    response['X-Auth-Token'] = '0000'
40
    response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
41
    return response
42

  
43
def account_demux(request, v_account):
44
    if request.method == 'HEAD':
45
        return account_meta(request, v_account)
46
    elif request.method == 'GET':
47
        return container_list(request, v_account)
48
    elif request.method == 'POST':
49
        return account_update(request, v_account)
50
    else:
51
        return method_not_allowed(request)
52

  
53
def container_demux(request, v_account, v_container):
54
    if request.method == 'HEAD':
55
        return container_meta(request, v_account, v_container)
56
    elif request.method == 'GET':
57
        return object_list(request, v_account, v_container)
58
    elif request.method == 'PUT':
59
        return container_create(request, v_account, v_container)
60
    elif request.method == 'POST':
61
        return container_update(request, v_account, v_container)
62
    elif request.method == 'DELETE':
63
        return container_delete(request, v_account, v_container)
64
    else:
65
        return method_not_allowed(request)
66

  
67
def object_demux(request, v_account, v_container, v_object):
68
    if request.method == 'HEAD':
69
        return object_meta(request, v_account, v_container, v_object)
70
    elif request.method == 'GET':
71
        return object_read(request, v_account, v_container, v_object)
72
    elif request.method == 'PUT':
73
        return object_write(request, v_account, v_container, v_object)
74
    elif request.method == 'COPY':
75
        return object_copy(request, v_account, v_container, v_object)
76
    elif request.method == 'POST':
77
        return object_update(request, v_account, v_container, v_object)
78
    elif request.method == 'DELETE':
79
        return object_delete(request, v_account, v_container, v_object)
80
    else:
81
        return method_not_allowed(request)
82

  
83
@api_method('HEAD')
84
def account_meta(request, v_account):
85
    # Normal Response Codes: 204
86
    # Error Response Codes: serviceUnavailable (503),
87
    #                       unauthorized (401),
88
    #                       badRequest (400)
89
    
90
    be = BackEnd(STORAGE_PATH)
91
    try:
92
        info = be.get_account_meta(request.user)
93
    except NameError:
94
        info = {'count': 0, 'bytes': 0}
95
    
96
    response = HttpResponse(status = 204)
97
    response['X-Account-Container-Count'] = info['count']
98
    response['X-Account-Bytes-Used'] = info['bytes']
99
    for k in [x for x in info.keys() if x.startswith('X-Account-Meta-')]:
100
        response[k.encode('utf-8')] = info[k].encode('utf-8')
101
    
102
    return response
103

  
104
@api_method('POST')
105
def account_update(request, v_account):
106
    # Normal Response Codes: 202
107
    # Error Response Codes: serviceUnavailable (503),
108
    #                       itemNotFound (404),
109
    #                       unauthorized (401),
110
    #                       badRequest (400)
111
    
112
    meta = get_meta(request, 'X-Account-Meta-')
113
    
114
    be = BackEnd(STORAGE_PATH)
115
    be.update_account_meta(request.user, meta)
116
    
117
    return HttpResponse(status = 202)
118

  
119
@api_method('GET', format_allowed = True)
120
def container_list(request, v_account):
121
    # Normal Response Codes: 200, 204
122
    # Error Response Codes: serviceUnavailable (503),
123
    #                       itemNotFound (404),
124
    #                       unauthorized (401),
125
    #                       badRequest (400)
126
    
127
    marker = request.GET.get('marker')
128
    limit = request.GET.get('limit')
129
    if limit:
130
        try:
131
            limit = int(limit)
132
        except ValueError:
133
            limit = 10000
134
    
135
    be = BackEnd(STORAGE_PATH)
136
    try:
137
        containers = be.list_containers(request.user, marker, limit)
138
    except NameError:
139
        containers = []
140
    
141
    if request.serialization == 'text':
142
        if len(containers) == 0:
143
            # The cloudfiles python bindings expect 200 if json/xml.
144
            return HttpResponse(status = 204)
145
        return HttpResponse('\n'.join(containers), status = 200)
146
    
147
    # TODO: Do this with a backend parameter?
148
    try:
149
        containers = [be.get_container_meta(request.user, x) for x in containers]
150
    except NameError:
151
        raise ItemNotFound()
152
    if request.serialization == 'xml':
153
        data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})
154
    elif request.serialization  == 'json':
155
        data = json.dumps(containers)
156
    return HttpResponse(data, status = 200)
157

  
158
@api_method('HEAD')
159
def container_meta(request, v_account, v_container):
160
    # Normal Response Codes: 204
161
    # Error Response Codes: serviceUnavailable (503),
162
    #                       itemNotFound (404),
163
    #                       unauthorized (401),
164
    #                       badRequest (400)
165
    
166
    be = BackEnd(STORAGE_PATH)
167
    try:
168
        info = be.get_container_meta(request.user, v_container)
169
    except NameError:
170
        raise ItemNotFound()
171
    
172
    response = HttpResponse(status = 204)
173
    response['X-Container-Object-Count'] = info['count']
174
    response['X-Container-Bytes-Used'] = info['bytes']
175
    for k in [x for x in info.keys() if x.startswith('X-Container-Meta-')]:
176
        response[k.encode('utf-8')] = info[k].encode('utf-8')
177
    
178
    return response
179

  
180
@api_method('PUT')
181
def container_create(request, v_account, v_container):
182
    # Normal Response Codes: 201, 202
183
    # Error Response Codes: serviceUnavailable (503),
184
    #                       itemNotFound (404),
185
    #                       unauthorized (401),
186
    #                       badRequest (400)
187
    
188
    meta = get_meta(request, 'X-Container-Meta-')
189
    
190
    be = BackEnd(STORAGE_PATH)
191
    try:
192
        be.create_container(request.user, v_container)
193
        ret = 201
194
    except NameError:
195
        ret = 202
196
    
197
    if len(meta) > 0:
198
        be.update_container_meta(request.user, v_container, meta)
199
    
200
    return HttpResponse(status = ret)
201

  
202
@api_method('POST')
203
def container_update(request, v_account, v_container):
204
    # Normal Response Codes: 202
205
    # Error Response Codes: serviceUnavailable (503),
206
    #                       itemNotFound (404),
207
    #                       unauthorized (401),
208
    #                       badRequest (400)
209
    
210
    meta = get_meta(request, 'X-Container-Meta-')
211
    
212
    be = BackEnd(STORAGE_PATH)
213
    try:
214
        be.update_container_meta(request.user, v_container, meta)
215
    except NameError:
216
        raise ItemNotFound()
217
    
218
    return HttpResponse(status = 202)
219

  
220
@api_method('DELETE')
221
def container_delete(request, v_account, v_container):
222
    # Normal Response Codes: 204
223
    # Error Response Codes: serviceUnavailable (503),
224
    #                       conflict (409),
225
    #                       itemNotFound (404),
226
    #                       unauthorized (401),
227
    #                       badRequest (400)
228
    
229
    be = BackEnd(STORAGE_PATH)
230
    try:
231
        be.delete_container(request.user, v_container)
232
    except NameError:
233
        raise ItemNotFound()
234
    except IndexError:
235
        raise Conflict()
236
    return HttpResponse(status = 204)
237

  
238
@api_method('GET', format_allowed = True)
239
def object_list(request, v_account, v_container):
240
    # Normal Response Codes: 200, 204
241
    # Error Response Codes: serviceUnavailable (503),
242
    #                       itemNotFound (404),
243
    #                       unauthorized (401),
244
    #                       badRequest (400)
245
    
246
    path = request.GET.get('path')
247
    prefix = request.GET.get('prefix')
248
    delimiter = request.GET.get('delimiter')
249
    
250
    # TODO: Check if the cloudfiles python bindings expect the results with the prefix.
251
    # Path overrides prefix and delimiter.
252
    if path:
253
        prefix = path
254
        delimiter = '/'
255
    # Naming policy.
256
    if prefix and delimiter:
257
        prefix = prefix + delimiter
258
    if not prefix:
259
        prefix = ''
260
    
261
    marker = request.GET.get('marker')
262
    limit = request.GET.get('limit')
263
    if limit:
264
        try:
265
            limit = int(limit)
266
        except ValueError:
267
            limit = 10000
268
    
269
    be = BackEnd(STORAGE_PATH)
270
    try:
271
        objects = be.list_objects(request.user, v_container, prefix, delimiter, marker, limit)
272
    except NameError:
273
        raise ItemNotFound()
274
    
275
    if request.serialization == 'text':
276
        if len(objects) == 0:
277
            # The cloudfiles python bindings expect 200 if json/xml.
278
            return HttpResponse(status = 204)
279
        return HttpResponse('\n'.join(objects), status = 200)
280
    
281
    # TODO: Do this with a backend parameter?
282
    try:
283
        objects = [be.get_object_meta(request.user, v_container, x) for x in objects]
284
    except NameError:
285
        raise ItemNotFound()
286
    # Format dates.
287
    for x in objects:
288
        if x.has_key('last_modified'):
289
            x['last_modified'] = datetime.datetime.fromtimestamp(x['last_modified']).isoformat()
290
    if request.serialization == 'xml':
291
        data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})
292
    elif request.serialization  == 'json':
293
        data = json.dumps(objects)
294
    return HttpResponse(data, status = 200)
295

  
296
@api_method('HEAD')
297
def object_meta(request, v_account, v_container, v_object):
298
    # Normal Response Codes: 204
299
    # Error Response Codes: serviceUnavailable (503),
300
    #                       itemNotFound (404),
301
    #                       unauthorized (401),
302
    #                       badRequest (400)
303
    
304
    be = BackEnd(STORAGE_PATH)
305
    try:
306
        info = be.get_object_meta(request.user, v_container, v_object)
307
    except NameError:
308
        raise ItemNotFound()
309
    
310
    response = HttpResponse(status = 204)
311
    response['ETag'] = info['hash']
312
    response['Content-Length'] = info['bytes']
313
    response['Content-Type'] = info['content_type']
314
    response['Last-Modified'] = http_date(info['last_modified'])
315
    for k in [x for x in info.keys() if x.startswith('X-Object-Meta-')]:
316
        response[k.encode('utf-8')] = info[k].encode('utf-8')
317
    
318
    return response
319

  
320
@api_method('GET')
321
def object_read(request, v_account, v_container, v_object):
322
    # Normal Response Codes: 200, 206
323
    # Error Response Codes: serviceUnavailable (503),
324
    #                       rangeNotSatisfiable (416),
325
    #                       preconditionFailed (412),
326
    #                       itemNotFound (404),
327
    #                       unauthorized (401),
328
    #                       badRequest (400),
329
    #                       notModified (304)
330
    
331
    be = BackEnd(STORAGE_PATH)
332
    try:
333
        info = be.get_object_meta(request.user, v_container, v_object)
334
    except NameError:
335
        raise ItemNotFound()
336
    
337
    # TODO: Check if the cloudfiles python bindings expect hash/content_type/last_modified on range requests.
338
    response = HttpResponse()
339
    response['ETag'] = info['hash']
340
    response['Content-Type'] = info['content_type']
341
    response['Last-Modified'] = http_date(info['last_modified'])
342
    
343
    # Range handling.
344
    range = get_range(request)
345
    if range is not None:
346
        offset, length = range
347
        if length:
348
            if offset + length > info['bytes']:
349
                raise RangeNotSatisfiable()
350
        else:
351
            if offset > info['bytes']:
352
                raise RangeNotSatisfiable()
353
        if not length:
354
            length = -1
355
        
356
        response['Content-Length'] = length        
357
        response.status_code = 206
358
    else:
359
        offset = 0
360
        length = -1
361
        
362
        response['Content-Length'] = info['bytes']
363
        response.status_code = 200
364
    
365
    # Conditions (according to RFC2616 must be evaluated at the end).
366
    # TODO: Check etag/date conditions.
367
    if_match = request.META.get('HTTP_IF_MATCH')
368
    if if_match is not None and if_match != '*':
369
        if info['hash'] not in parse_etags(if_match):
370
            raise PreconditionFailed()
371
    
372
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
373
    if if_none_match is not None:
374
        if if_none_match == '*' or info['hash'] in parse_etags(if_none_match):
375
            raise NotModified()
376
    
377
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
378
    if if_modified_since is not None:
379
        if_modified_since = parse_http_date_safe(if_modified_since)
380
    if if_modified_since is not None and info['last_modified'] <= if_modified_since:
381
        raise NotModified()
382

  
383
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
384
    if if_unmodified_since is not None:
385
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
386
    if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:
387
        raise PreconditionFailed()
388
    
389
    try:
390
        response.content = be.get_object(request.user, v_container, v_object, offset, length)
391
    except NameError:
392
        raise ItemNotFound()
393
    
394
    return response
395

  
396
@api_method('PUT')
397
def object_write(request, v_account, v_container, v_object):
398
    # Normal Response Codes: 201
399
    # Error Response Codes: serviceUnavailable (503),
400
    #                       unprocessableEntity (422),
401
    #                       lengthRequired (411),
402
    #                       itemNotFound (404),
403
    #                       unauthorized (401),
404
    #                       badRequest (400)
405
    
406
    be = BackEnd(STORAGE_PATH)
407
    
408
    copy_from = request.META.get('HTTP_X_COPY_FROM')
409
    if copy_from:
410
        parts = copy_from.split('/')
411
        if len(parts) < 3 or parts[0] != '':
412
            raise BadRequest('Bad X-Copy-From path.')
413
        copy_container = parts[1]
414
        copy_name = '/'.join(parts[2:])
415
        
416
        try:
417
            info = be.get_object_meta(request.user, copy_container, copy_name)
418
        except NameError:
419
            raise ItemNotFound()
420
        
421
        content_length = request.META.get('CONTENT_LENGTH')
422
        content_type = request.META.get('CONTENT_TYPE')
423
        # TODO: Why is this required? Copy this ammount?
424
        if not content_length:
425
            raise LengthRequired()
426
        if content_type:
427
            info['content_type'] = content_type
428
        
429
        meta = get_meta(request, 'X-Object-Meta-')
430
        info.update(meta)
431
        
432
        try:
433
            be.copy_object(request.user, copy_container, copy_name, v_container, v_object)
434
            be.update_object_meta(request.user, v_container, v_object, info)
435
        except NameError:
436
            raise ItemNotFound()
437
        
438
        response = HttpResponse(status = 201)
439
    else:
440
        content_length = request.META.get('CONTENT_LENGTH')
441
        content_type = request.META.get('CONTENT_TYPE')
442
        if not content_length or not content_type:
443
            raise LengthRequired()
444
        
445
        info = {'content_type': content_type}
446
        meta = get_meta(request, 'X-Object-Meta-')
447
        info.update(meta)
448
        
449
        data = request.raw_post_data
450
        try:
451
            be.update_object(request.user, v_container, v_object, data)
452
            be.update_object_meta(request.user, v_container, v_object, info)
453
        except NameError:
454
            raise ItemNotFound()
455
        
456
        # TODO: Check before update?
457
        info = be.get_object_meta(request.user, v_container, v_object)
458
        etag = request.META.get('HTTP_ETAG')
459
        if etag:
460
            etag = parse_etags(etag)[0] # TODO: Unescape properly.
461
            if etag != info['hash']:
462
                be.delete_object(request.user, v_container, v_object)
463
                raise UnprocessableEntity()
464
        
465
        response = HttpResponse(status = 201)
466
        response['ETag'] = info['hash']
467
    
468
    return response
469

  
470
@api_method('COPY')
471
def object_copy(request, v_account, v_container, v_object):
472
    # Normal Response Codes: 201
473
    # Error Response Codes: serviceUnavailable (503),
474
    #                       itemNotFound (404),
475
    #                       unauthorized (401),
476
    #                       badRequest (400)
477
    
478
    destination = request.META.get('HTTP_DESTINATION')
479
    if not destination:
480
        raise BadRequest('Missing Destination.');
481
    
482
    parts = destination.split('/')
483
    if len(parts) < 3 or parts[0] != '':
484
        raise BadRequest('Bad Destination path.')
485
    dest_container = parts[1]
486
    dest_name = '/'.join(parts[2:])
487
    
488
    be = BackEnd(STORAGE_PATH)
489
    try:
490
        info = be.get_object_meta(request.user, v_container, v_object)
491
    except NameError:
492
        raise ItemNotFound()
493
    
494
    content_type = request.META.get('CONTENT_TYPE')
495
    if content_type:
496
        info['content_type'] = content_type
497
    meta = get_meta(request, 'X-Object-Meta-')
498
    info.update(meta)
499
    
500
    try:
501
        be.copy_object(request.user, v_container, v_object, dest_container, dest_name)
502
        be.update_object_meta(request.user, dest_container, dest_name, info)
503
    except NameError:
504
        raise ItemNotFound()
505
    
506
    response = HttpResponse(status = 201)
507

  
508
@api_method('POST')
509
def object_update(request, v_account, v_container, v_object):
510
    # Normal Response Codes: 202
511
    # Error Response Codes: serviceUnavailable (503),
512
    #                       itemNotFound (404),
513
    #                       unauthorized (401),
514
    #                       badRequest (400)
515
    
516
    meta = get_meta(request, 'X-Object-Meta-')
517
    
518
    be = BackEnd(STORAGE_PATH)
519
    try:
520
        be.update_object_meta(request.user, v_container, v_object, meta)
521
    except NameError:
522
        raise ItemNotFound()
523
    
524
    return HttpResponse(status = 202)
525

  
526
@api_method('DELETE')
527
def object_delete(request, v_account, v_container, v_object):
528
    # Normal Response Codes: 204
529
    # Error Response Codes: serviceUnavailable (503),
530
    #                       itemNotFound (404),
531
    #                       unauthorized (401),
532
    #                       badRequest (400)
533
    
534
    be = BackEnd(STORAGE_PATH)
535
    try:
536
        be.delete_object(request.user, v_container, v_object)
537
    except NameError:
538
        raise ItemNotFound()
539
    return HttpResponse(status = 204)
540

  
541
@api_method()
542
def method_not_allowed(request):
543
    raise BadRequest('Method not allowed.')
1
import os
2
import logging
3
import hashlib
4
import types
5

  
6
from django.http import HttpResponse
7
from django.template.loader import render_to_string
8
from django.utils import simplejson as json
9
from django.utils.http import parse_etags
10

  
11
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict,
12
    LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity)
13
from pithos.api.util import (printable_meta_dict, get_account_meta, put_account_meta,
14
    get_container_meta, put_container_meta, get_object_meta, put_object_meta,
15
    validate_modification_preconditions, copy_or_move_object, get_range,
16
    raw_input_socket, socket_read_iterator, api_method)
17
from pithos.backends import backend
18

  
19

  
20
logger = logging.getLogger(__name__)
21

  
22

  
23
def top_demux(request):
24
    if request.method == 'GET':
25
        return authenticate(request)
26
    else:
27
        return method_not_allowed(request)
28

  
29
def account_demux(request, v_account):
30
    if request.method == 'HEAD':
31
        return account_meta(request, v_account)
32
    elif request.method == 'GET':
33
        return container_list(request, v_account)
34
    elif request.method == 'POST':
35
        return account_update(request, v_account)
36
    else:
37
        return method_not_allowed(request)
38

  
39
def container_demux(request, v_account, v_container):
40
    if request.method == 'HEAD':
41
        return container_meta(request, v_account, v_container)
42
    elif request.method == 'GET':
43
        return object_list(request, v_account, v_container)
44
    elif request.method == 'PUT':
45
        return container_create(request, v_account, v_container)
46
    elif request.method == 'POST':
47
        return container_update(request, v_account, v_container)
48
    elif request.method == 'DELETE':
49
        return container_delete(request, v_account, v_container)
50
    else:
51
        return method_not_allowed(request)
52

  
53
def object_demux(request, v_account, v_container, v_object):
54
    if request.method == 'HEAD':
55
        return object_meta(request, v_account, v_container, v_object)
56
    elif request.method == 'GET':
57
        return object_read(request, v_account, v_container, v_object)
58
    elif request.method == 'PUT':
59
        return object_write(request, v_account, v_container, v_object)
60
    elif request.method == 'COPY':
61
        return object_copy(request, v_account, v_container, v_object)
62
    elif request.method == 'MOVE':
63
        return object_move(request, v_account, v_container, v_object)
64
    elif request.method == 'POST':
65
        return object_update(request, v_account, v_container, v_object)
66
    elif request.method == 'DELETE':
67
        return object_delete(request, v_account, v_container, v_object)
68
    else:
69
        return method_not_allowed(request)
70

  
71
@api_method('GET')
72
def authenticate(request):
73
    # Normal Response Codes: 204
74
    # Error Response Codes: serviceUnavailable (503),
75
    #                       unauthorized (401),
76
    #                       badRequest (400)
77
    
78
    x_auth_user = request.META.get('HTTP_X_AUTH_USER')
79
    x_auth_key = request.META.get('HTTP_X_AUTH_KEY')
80
    if not x_auth_user or not x_auth_key:
81
        raise BadRequest('Missing X-Auth-User or X-Auth-Key header')
82
    
83
    response = HttpResponse(status=204)
84
    response['X-Auth-Token'] = '0000'
85
    response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')
86
    return response
87

  
88
@api_method('HEAD')
89
def account_meta(request, v_account):
90
    # Normal Response Codes: 204
91
    # Error Response Codes: serviceUnavailable (503),
92
    #                       unauthorized (401),
93
    #                       badRequest (400)
94
    
95
    meta = backend.get_account_meta(request.user)
96
    
97
    response = HttpResponse(status=204)
98
    put_account_meta(response, meta)
99
    return response
100

  
101
@api_method('POST')
102
def account_update(request, v_account):
103
    # Normal Response Codes: 202
104
    # Error Response Codes: serviceUnavailable (503),
105
    #                       unauthorized (401),
106
    #                       badRequest (400)
107
    
108
    meta = get_account_meta(request)    
109
    backend.update_account_meta(request.user, meta)
110
    return HttpResponse(status=202)
111

  
112
@api_method('GET', format_allowed=True)
113
def container_list(request, v_account):
114
    # Normal Response Codes: 200, 204
115
    # Error Response Codes: serviceUnavailable (503),
116
    #                       itemNotFound (404),
117
    #                       unauthorized (401),
118
    #                       badRequest (400)
119
    
120
    meta = backend.get_account_meta(request.user)
121
    
122
    validate_modification_preconditions(request, meta)
123
    
124
    response = HttpResponse()
125
    put_account_meta(response, meta)
126
    
127
    marker = request.GET.get('marker')
128
    limit = request.GET.get('limit')
129
    if limit:
130
        try:
131
            limit = int(limit)
132
            if limit <= 0:
133
                raise ValueError
134
        except ValueError:
135
            limit = 10000
136
    
137
    try:
138
        containers = backend.list_containers(request.user, marker, limit)
139
    except NameError:
140
        containers = []
141
    
142
    if request.serialization == 'text':
143
        if len(containers) == 0:
144
            # The cloudfiles python bindings expect 200 if json/xml.
145
            response.status_code = 204
146
            return response
147
        response.status_code = 200
148
        response.content = '\n'.join(containers) + '\n'
149
        return response
150
    
151
    container_meta = []
152
    for x in containers:
153
        try:
154
            meta = backend.get_container_meta(request.user, x)
155
        except NameError:
156
            continue
157
        container_meta.append(printable_meta_dict(meta))
158
    if request.serialization == 'xml':
159
        data = render_to_string('containers.xml', {'account': request.user, 'containers': container_meta})
160
    elif request.serialization  == 'json':
161
        data = json.dumps(container_meta)
162
    response.status_code = 200
163
    response.content = data
164
    return response
165

  
166
@api_method('HEAD')
167
def container_meta(request, v_account, v_container):
168
    # Normal Response Codes: 204
169
    # Error Response Codes: serviceUnavailable (503),
170
    #                       itemNotFound (404),
171
    #                       unauthorized (401),
172
    #                       badRequest (400)
173
    
174
    try:
175
        meta = backend.get_container_meta(request.user, v_container)
176
    except NameError:
177
        raise ItemNotFound('Container does not exist')
178
    
179
    response = HttpResponse(status=204)
180
    put_container_meta(response, meta)
181
    return response
182

  
183
@api_method('PUT')
184
def container_create(request, v_account, v_container):
185
    # Normal Response Codes: 201, 202
186
    # Error Response Codes: serviceUnavailable (503),
187
    #                       itemNotFound (404),
188
    #                       unauthorized (401),
189
    #                       badRequest (400)
190
    
191
    meta = get_container_meta(request)
192
    
193
    try:
194
        backend.create_container(request.user, v_container)
195
        ret = 201
196
    except NameError:
197
        ret = 202
198
    
199
    if len(meta) > 0:
200
        backend.update_container_meta(request.user, v_container, meta)
201
    
202
    return HttpResponse(status=ret)
203

  
204
@api_method('POST')
205
def container_update(request, v_account, v_container):
206
    # Normal Response Codes: 202
207
    # Error Response Codes: serviceUnavailable (503),
208
    #                       itemNotFound (404),
209
    #                       unauthorized (401),
210
    #                       badRequest (400)
211
    
212
    meta = get_container_meta(request)
213
    try:
214
        backend.update_container_meta(request.user, v_container, meta)
215
    except NameError:
216
        raise ItemNotFound('Container does not exist')
217
    return HttpResponse(status=202)
218

  
219
@api_method('DELETE')
220
def container_delete(request, v_account, v_container):
221
    # Normal Response Codes: 204
222
    # Error Response Codes: serviceUnavailable (503),
223
    #                       conflict (409),
224
    #                       itemNotFound (404),
225
    #                       unauthorized (401),
226
    #                       badRequest (400)
227
    
228
    try:
229
        backend.delete_container(request.user, v_container)
230
    except NameError:
231
        raise ItemNotFound('Container does not exist')
232
    except IndexError:
233
        raise Conflict('Container is not empty')
234
    return HttpResponse(status=204)
235

  
236
@api_method('GET', format_allowed=True)
237
def object_list(request, v_account, v_container):
238
    # Normal Response Codes: 200, 204
239
    # Error Response Codes: serviceUnavailable (503),
240
    #                       itemNotFound (404),
241
    #                       unauthorized (401),
242
    #                       badRequest (400)
243
    
244
    try:
245
        meta = backend.get_container_meta(request.user, v_container)
246
    except NameError:
247
        raise ItemNotFound('Container does not exist')
248
    
249
    validate_modification_preconditions(request, meta)
250
    
251
    response = HttpResponse()
252
    put_container_meta(response, meta)
253
    
254
    path = request.GET.get('path')
255
    prefix = request.GET.get('prefix')
256
    delimiter = request.GET.get('delimiter')
257
    
258
    # Path overrides prefix and delimiter.
259
    virtual = True
260
    if path:
261
        prefix = path
262
        delimiter = '/'
263
        virtual = False
264
    
265
    # Naming policy.
266
    if prefix and delimiter:
267
        prefix = prefix + delimiter
268
    if not prefix:
269
        prefix = ''
270
    prefix = prefix.lstrip('/')
271
    
272
    marker = request.GET.get('marker')
273
    limit = request.GET.get('limit')
274
    if limit:
275
        try:
276
            limit = int(limit)
277
            if limit <= 0:
278
                raise ValueError
279
        except ValueError:
280
            limit = 10000
281
    
282
    try:
283
        objects = backend.list_objects(request.user, v_container, prefix, delimiter, marker, limit, virtual)
284
    except NameError:
285
        raise ItemNotFound('Container does not exist')
286
    
287
    if request.serialization == 'text':
288
        if len(objects) == 0:
289
            # The cloudfiles python bindings expect 200 if json/xml.
290
            response.status_code = 204
291
            return response
292
        response.status_code = 200
293
        response.content = '\n'.join(objects) + '\n'
294
        return response
295
    
296
    object_meta = []
297
    for x in objects:
298
        try:
299
            meta = backend.get_object_meta(request.user, v_container, x)
300
        except NameError:
301
            # Virtual objects/directories.
302
            if virtual and delimiter and x.endswith(delimiter):
303
                object_meta.append({"subdir": x})
304
            continue
305
        object_meta.append(printable_meta_dict(meta))
306
    if request.serialization == 'xml':
307
        data = render_to_string('objects.xml', {'container': v_container, 'objects': object_meta})
308
    elif request.serialization  == 'json':
309
        data = json.dumps(object_meta)
310
    response.status_code = 200
311
    response.content = data
312
    return response
313

  
314
@api_method('HEAD')
315
def object_meta(request, v_account, v_container, v_object):
316
    # Normal Response Codes: 204
317
    # Error Response Codes: serviceUnavailable (503),
318
    #                       itemNotFound (404),
319
    #                       unauthorized (401),
320
    #                       badRequest (400)
321
    
322
    try:
323
        meta = backend.get_object_meta(request.user, v_container, v_object)
324
    except NameError:
325
        raise ItemNotFound('Object does not exist')
326
    
327
    response = HttpResponse(status=204)
328
    put_object_meta(response, meta)
329
    return response
330

  
331
@api_method('GET')
332
def object_read(request, v_account, v_container, v_object):
333
    # Normal Response Codes: 200, 206
334
    # Error Response Codes: serviceUnavailable (503),
335
    #                       rangeNotSatisfiable (416),
336
    #                       preconditionFailed (412),
337
    #                       itemNotFound (404),
338
    #                       unauthorized (401),
339
    #                       badRequest (400),
340
    #                       notModified (304)
341
    
342
    try:
343
        meta = backend.get_object_meta(request.user, v_container, v_object)
344
    except NameError:
345
        raise ItemNotFound('Object does not exist')
346
    
347
    response = HttpResponse()
348
    put_object_meta(response, meta)
349
    
350
    # Range handling.
351
    range = get_range(request)
352
    if range is not None:
353
        offset, length = range
354
        if offset < 0:
355
            offset = meta['bytes'] + offset
356
        if offset > meta['bytes'] or (length and offset + length > meta['bytes']):
357
            raise RangeNotSatisfiable('Requested range exceeds object limits')
358
        if not length:
359
            length = -1
360
        
361
        response['Content-Length'] = length # Update with the correct length.
362
        response.status_code = 206
363
    else:
364
        offset = 0
365
        length = -1
366
        response.status_code = 200
367
    
368
    # Conditions (according to RFC2616 must be evaluated at the end).
369
    validate_modification_preconditions(request, meta)
370
    if_match = request.META.get('HTTP_IF_MATCH')
371
    if if_match is not None and if_match != '*':
372
        if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
373
            raise PreconditionFailed('Object Etag does not match')    
374
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
375
    if if_none_match is not None:
376
        if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
377
            raise NotModified('Object Etag matches')
378
    
379
    try:
380
        response.content = backend.get_object(request.user, v_container, v_object, offset, length)
381
    except NameError:
382
        raise ItemNotFound('Object does not exist')
383
    
384
    return response
385

  
386
@api_method('PUT')
387
def object_write(request, v_account, v_container, v_object):
388
    # Normal Response Codes: 201
389
    # Error Response Codes: serviceUnavailable (503),
390
    #                       unprocessableEntity (422),
391
    #                       lengthRequired (411),
392
    #                       itemNotFound (404),
393
    #                       unauthorized (401),
394
    #                       badRequest (400)
395
    
396
    copy_from = request.META.get('HTTP_X_COPY_FROM')
397
    move_from = request.META.get('HTTP_X_MOVE_FROM')
398
    if copy_from or move_from:
399
        # TODO: Why is this required? Copy this ammount?
400
        content_length = request.META.get('CONTENT_LENGTH')
401
        if not content_length:
402
            raise LengthRequired('Missing Content-Length header')
403
        
404
        if move_from:
405
            copy_or_move_object(request, move_from, (v_container, v_object), move=True)
406
        else:
407
            copy_or_move_object(request, copy_from, (v_container, v_object), move=False)
408
        return HttpResponse(status=201)
409
    
410
    meta = get_object_meta(request)
411
    content_length = -1
412
    if request.META.get('HTTP_TRANSFER_ENCODING') != 'chunked':
413
        content_length = request.META.get('CONTENT_LENGTH')
414
        if not content_length:
415
            raise LengthRequired('Missing Content-Length header')
416
        try:
417
            content_length = int(content_length)
418
            if content_length < 0:
419
                raise ValueError
420
        except ValueError:
421
            raise BadRequest('Invalid Content-Length header')
422
    # Should be BadRequest, but API says otherwise.
423
    if 'Content-Type' not in meta:
424
        raise LengthRequired('Missing Content-Type header')
425
    
426
    sock = raw_input_socket(request)
427
    md5 = hashlib.md5()
428
    offset = 0
429
    for data in socket_read_iterator(sock, content_length):
430
        # TODO: Raise 408 (Request Timeout) if this takes too long.
431
        # TODO: Raise 499 (Client Disconnect) if a length is defined and we stop before getting this much data.
432
        md5.update(data)
433
        try:
434
            backend.update_object(request.user, v_container, v_object, data, offset)
435
        except NameError:
436
            raise ItemNotFound('Object does not exist')
437
        offset += len(data)
438
    
439
    meta['hash'] = md5.hexdigest().lower()
440
    etag = request.META.get('HTTP_ETAG')
441
    if etag and parse_etags(etag)[0].lower() != meta['hash']:
442
        raise UnprocessableEntity('Object Etag does not match')
443
    try:
444
        backend.update_object_meta(request.user, v_container, v_object, meta)
445
    except NameError:
446
        raise ItemNotFound('Object does not exist')
447
    
448
    response = HttpResponse(status=201)
449
    response['ETag'] = meta['hash']
450
    return response
451

  
452
@api_method('COPY')
453
def object_copy(request, v_account, v_container, v_object):
454
    # Normal Response Codes: 201
455
    # Error Response Codes: serviceUnavailable (503),
456
    #                       itemNotFound (404),
457
    #                       unauthorized (401),
458
    #                       badRequest (400)
459
    
460
    dest_path = request.META.get('HTTP_DESTINATION')
461
    if not dest_path:
462
        raise BadRequest('Missing Destination header')
463
    copy_or_move_object(request, (v_container, v_object), dest_path, move=False)
464
    return HttpResponse(status=201)
465

  
466
@api_method('MOVE')
467
def object_move(request, v_account, v_container, v_object):
468
    # Normal Response Codes: 201
469
    # Error Response Codes: serviceUnavailable (503),
470
    #                       itemNotFound (404),
471
    #                       unauthorized (401),
472
    #                       badRequest (400)
473
    
474
    dest_path = request.META.get('HTTP_DESTINATION')
475
    if not dest_path:
476
        raise BadRequest('Missing Destination header')
477
    copy_or_move_object(request, (v_container, v_object), dest_path, move=True)
478
    return HttpResponse(status=201)
479

  
480
@api_method('POST')
481
def object_update(request, v_account, v_container, v_object):
482
    # Normal Response Codes: 202
483
    # Error Response Codes: serviceUnavailable (503),
484
    #                       itemNotFound (404),
485
    #                       unauthorized (401),
486
    #                       badRequest (400)
487
    
488
    meta = get_object_meta(request)
489
    if 'Content-Type' in meta:
490
        del(meta['Content-Type']) # Do not allow changing the Content-Type.
491
    try:
492
        backend.update_object_meta(request.user, v_container, v_object, meta)
493
    except NameError:
494
        raise ItemNotFound('Object does not exist')
495
    return HttpResponse(status=202)
496

  
497
@api_method('DELETE')
498
def object_delete(request, v_account, v_container, v_object):
499
    # Normal Response Codes: 204
500
    # Error Response Codes: serviceUnavailable (503),
501
    #                       itemNotFound (404),
502
    #                       unauthorized (401),
503
    #                       badRequest (400)
504
    
505
    try:
506
        backend.delete_object(request.user, v_container, v_object)
507
    except NameError:
508
        raise ItemNotFound('Object does not exist')
509
    return HttpResponse(status=204)
510

  
511
@api_method()
512
def method_not_allowed(request):
513
    raise BadRequest('Method not allowed')

Also available in: Unified diff