Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ dfa2d4ba

History | View | Annotate | Download (39.8 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 functools import wraps
35
from time import time
36
from traceback import format_exc
37
from wsgiref.handlers import format_date_time
38
from binascii import hexlify, unhexlify
39
from datetime import datetime, tzinfo, timedelta
40
from urllib import quote, unquote
41

    
42
from django.conf import settings
43
from django.http import HttpResponse
44
from django.template.loader import render_to_string
45
from django.utils import simplejson as json
46
from django.utils.http import http_date, parse_etags
47
from django.utils.encoding import smart_unicode, smart_str
48
from django.core.files.uploadhandler import FileUploadHandler
49
from django.core.files.uploadedfile import UploadedFile
50

    
51
from synnefo.lib.parsedate import parse_http_date_safe, parse_http_date
52
from synnefo.lib.astakos import get_user
53

    
54
from pithos.api.faults import (
55
    Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
56
    Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
57
    RangeNotSatisfiable, InternalServerError, NotImplemented)
58
from pithos.api.short_url import encode_url
59
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
60
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
61
                                 BACKEND_BLOCK_UMASK,
62
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
63
                                 BACKEND_QUEUE_EXCHANGE,
64
                                 QUOTAHOLDER_URL, QUOTAHOLDER_TOKEN,
65
                                 BACKEND_QUOTA, BACKEND_VERSIONING,
66
                                 BACKEND_FREE_VERSIONING,
67
                                 AUTHENTICATION_URL, AUTHENTICATION_USERS,
68
                                 COOKIE_NAME, USER_CATALOG_URL,
69
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
70
                                 RADOS_POOL_MAPS)
71
from pithos.backends import connect_backend
72
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
73
                                  VersionNotExists)
74
from synnefo.lib.astakos import (get_user_uuid, get_displayname,
75
                                 get_uuids, get_displaynames)
76

    
77
import logging
78
import re
79
import hashlib
80
import uuid
81
import decimal
82

    
83

    
84
logger = logging.getLogger(__name__)
85

    
86

    
87
class UTC(tzinfo):
88
    def utcoffset(self, dt):
89
        return timedelta(0)
90

    
91
    def tzname(self, dt):
92
        return 'UTC'
93

    
94
    def dst(self, dt):
95
        return timedelta(0)
96

    
97

    
98
def json_encode_decimal(obj):
99
    if isinstance(obj, decimal.Decimal):
100
        return str(obj)
101
    raise TypeError(repr(obj) + " is not JSON serializable")
102

    
103

    
104
def isoformat(d):
105
    """Return an ISO8601 date string that includes a timezone."""
106

    
107
    return d.replace(tzinfo=UTC()).isoformat()
108

    
109

    
110
def rename_meta_key(d, old, new):
111
    if old not in d:
112
        return
113
    d[new] = d[old]
114
    del(d[old])
115

    
116

    
117
def printable_header_dict(d):
118
    """Format a meta dictionary for printing out json/xml.
119

120
    Convert all keys to lower case and replace dashes with underscores.
121
    Format 'last_modified' timestamp.
122
    """
123

    
124
    if 'last_modified' in d and d['last_modified']:
125
        d['last_modified'] = isoformat(
126
            datetime.fromtimestamp(d['last_modified']))
127
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
128

    
129

    
130
def format_header_key(k):
131
    """Convert underscores to dashes and capitalize intra-dash strings."""
132
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
133

    
134

    
135
def get_header_prefix(request, prefix):
136
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
137

    
138
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
139
    # TODO: Document or remove '~' replacing.
140
    return dict([(format_header_key(k[5:]), v.replace('~', '')) for k, v in request.META.iteritems() if k.startswith(prefix) and len(k) > len(prefix)])
141

    
142

    
143
def check_meta_headers(meta):
144
    if len(meta) > 90:
145
        raise BadRequest('Too many headers.')
146
    for k, v in meta.iteritems():
147
        if len(k) > 128:
148
            raise BadRequest('Header name too large.')
149
        if len(v) > 256:
150
            raise BadRequest('Header value too large.')
151

    
152

    
153
def get_account_headers(request):
154
    meta = get_header_prefix(request, 'X-Account-Meta-')
155
    check_meta_headers(meta)
156
    groups = {}
157
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
158
        n = k[16:].lower()
159
        if '-' in n or '_' in n:
160
            raise BadRequest('Bad characters in group name')
161
        groups[n] = v.replace(' ', '').split(',')
162
        while '' in groups[n]:
163
            groups[n].remove('')
164
    return meta, groups
165

    
166

    
167
def put_account_headers(response, meta, groups, policy):
168
    if 'count' in meta:
169
        response['X-Account-Container-Count'] = meta['count']
170
    if 'bytes' in meta:
171
        response['X-Account-Bytes-Used'] = meta['bytes']
172
    response['Last-Modified'] = http_date(int(meta['modified']))
173
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
174
        response[smart_str(
175
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
176
    if 'until_timestamp' in meta:
177
        response['X-Account-Until-Timestamp'] = http_date(
178
            int(meta['until_timestamp']))
179
    for k, v in groups.iteritems():
180
        k = smart_str(k, strings_only=True)
181
        k = format_header_key('X-Account-Group-' + k)
182
        v = smart_str(','.join(v), strings_only=True)
183
        response[k] = v
184
    for k, v in policy.iteritems():
185
        response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
186

    
187

    
188
def get_container_headers(request):
189
    meta = get_header_prefix(request, 'X-Container-Meta-')
190
    check_meta_headers(meta)
191
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
192
    return meta, policy
193

    
194

    
195
def put_container_headers(request, response, meta, policy):
196
    if 'count' in meta:
197
        response['X-Container-Object-Count'] = meta['count']
198
    if 'bytes' in meta:
199
        response['X-Container-Bytes-Used'] = meta['bytes']
200
    response['Last-Modified'] = http_date(int(meta['modified']))
201
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
202
        response[smart_str(
203
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
204
    l = [smart_str(x, strings_only=True) for x in meta['object_meta']
205
         if x.startswith('X-Object-Meta-')]
206
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
207
    response['X-Container-Block-Size'] = request.backend.block_size
208
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
209
    if 'until_timestamp' in meta:
210
        response['X-Container-Until-Timestamp'] = http_date(
211
            int(meta['until_timestamp']))
212
    for k, v in policy.iteritems():
213
        response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
214

    
215

    
216
def get_object_headers(request):
217
    content_type = request.META.get('CONTENT_TYPE', None)
218
    meta = get_header_prefix(request, 'X-Object-Meta-')
219
    check_meta_headers(meta)
220
    if request.META.get('HTTP_CONTENT_ENCODING'):
221
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
222
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
223
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
224
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
225
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
226
    return content_type, meta, get_sharing(request), get_public(request)
227

    
228

    
229
def put_object_headers(response, meta, restricted=False):
230
    response['ETag'] = meta['checksum']
231
    response['Content-Length'] = meta['bytes']
232
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
233
    response['Last-Modified'] = http_date(int(meta['modified']))
234
    if not restricted:
235
        response['X-Object-Hash'] = meta['hash']
236
        response['X-Object-UUID'] = meta['uuid']
237
        response['X-Object-Modified-By'] = smart_str(
238
            meta['modified_by'], strings_only=True)
239
        response['X-Object-Version'] = meta['version']
240
        response['X-Object-Version-Timestamp'] = http_date(
241
            int(meta['version_timestamp']))
242
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
243
            response[smart_str(
244
                k, strings_only=True)] = smart_str(meta[k], strings_only=True)
245
        for k in (
246
            'Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
247
            'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
248
                'X-Object-Public'):
249
            if k in meta:
250
                response[k] = smart_str(meta[k], strings_only=True)
251
    else:
252
        for k in ('Content-Encoding', 'Content-Disposition'):
253
            if k in meta:
254
                response[k] = smart_str(meta[k], strings_only=True)
255

    
256

    
257
def update_manifest_meta(request, v_account, meta):
258
    """Update metadata if the object has an X-Object-Manifest."""
259

    
260
    if 'X-Object-Manifest' in meta:
261
        etag = ''
262
        bytes = 0
263
        try:
264
            src_container, src_name = split_container_object_string(
265
                '/' + meta['X-Object-Manifest'])
266
            objects = request.backend.list_objects(
267
                request.user_uniq, v_account,
268
                src_container, prefix=src_name, virtual=False)
269
            for x in objects:
270
                src_meta = request.backend.get_object_meta(request.user_uniq,
271
                                                           v_account, src_container, x[0], 'pithos', x[1])
272
                etag += src_meta['checksum']
273
                bytes += src_meta['bytes']
274
        except:
275
            # Ignore errors.
276
            return
277
        meta['bytes'] = bytes
278
        md5 = hashlib.md5()
279
        md5.update(etag)
280
        meta['checksum'] = md5.hexdigest().lower()
281

    
282
def is_uuid(str):
283
    try:
284
        uuid.UUID(str)
285
    except ValueError:
286
       return False
287
    else:
288
       return True
289

    
290
##########################
291
# USER CATALOG utilities #
292
##########################
293

    
294
def retrieve_displayname(token, uuid):
295
    try:
296
        return get_displayname(
297
            token, uuid, USER_CATALOG_URL, AUTHENTICATION_USERS)
298
    except:
299
        # if it fails just leave the input intact
300
        return uuid
301

    
302
def retrieve_displaynames(token, uuids):
303
    return get_displaynames(
304
        token, uuids, USER_CATALOG_URL, AUTHENTICATION_USERS)
305

    
306
def retrieve_uuid(token, displayname):
307
    if is_uuid(displayname):
308
        return displayname
309

    
310
    uuid = get_user_uuid(
311
        token, displayname, USER_CATALOG_URL, AUTHENTICATION_USERS)
312
    if not uuid:
313
        raise ItemNotExists(displayname)
314
    return uuid
315

    
316
def retrieve_uuids(token, displaynames):
317
    return get_uuids(
318
        token, displaynames, USER_CATALOG_URL, AUTHENTICATION_USERS)
319

    
320
def replace_permissions_displayname(token, holder):
321
    try:
322
        # check first for a group permission
323
        account, group = holder.split(':')
324
    except ValueError:
325
        return retrieve_uuid(holder)
326
    else:
327
        return ':'.join([retrieve_uuid(account), group])
328

    
329
def replace_permissions_uuid(token, holder):
330
    try:
331
        # check first for a group permission
332
        account, group = holder.split(':')
333
    except ValueError:
334
        return retrieve_displayname(holder)
335
    else:
336
        return ':'.join([retrieve_displayname(account), group])
337

    
338
def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
339
    if permissions is None:
340
        return
341
    allowed, perm_path, perms = permissions
342
    if len(perms) == 0:
343
        return
344

    
345
    # replace uuid with displayname
346
#    perms['read'] = [replace_permissions_uuid(request.token, x) for x in perms.get('read', [])]
347
#    perms['write'] = \
348
#        [replace_permissions_uuid(request.token, x) for x in perms.get('write', [])]
349

    
350
    ret = []
351

    
352
    r = ','.join(perms.get('read', []))
353
    if r:
354
        ret.append('read=' + r)
355
    w = ','.join(perms.get('write', []))
356
    if w:
357
        ret.append('write=' + w)
358
    meta['X-Object-Sharing'] = '; '.join(ret)
359
    if '/'.join((v_account, v_container, v_object)) != perm_path:
360
        meta['X-Object-Shared-By'] = perm_path
361
    if request.user_uniq != v_account:
362
        meta['X-Object-Allowed-To'] = allowed
363

    
364

    
365
def update_public_meta(public, meta):
366
    if not public:
367
        return
368
    meta['X-Object-Public'] = '/public/' + encode_url(public)
369

    
370

    
371
def validate_modification_preconditions(request, meta):
372
    """Check that the modified timestamp conforms with the preconditions set."""
373

    
374
    if 'modified' not in meta:
375
        return  # TODO: Always return?
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 int(meta['modified']) <= if_modified_since:
381
        raise NotModified('Resource has not been modified')
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 int(meta['modified']) > if_unmodified_since:
387
        raise PreconditionFailed('Resource has been modified')
388

    
389

    
390
def validate_matching_preconditions(request, meta):
391
    """Check that the ETag conforms with the preconditions set."""
392

    
393
    etag = meta['checksum']
394
    if not etag:
395
        etag = None
396

    
397
    if_match = request.META.get('HTTP_IF_MATCH')
398
    if if_match is not None:
399
        if etag is None:
400
            raise PreconditionFailed('Resource does not exist')
401
        if if_match != '*' and etag not in [x.lower() for x in parse_etags(if_match)]:
402
            raise PreconditionFailed('Resource ETag does not match')
403

    
404
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
405
    if if_none_match is not None:
406
        # TODO: If this passes, must ignore If-Modified-Since header.
407
        if etag is not None:
408
            if if_none_match == '*' or etag in [x.lower() for x in parse_etags(if_none_match)]:
409
                # TODO: Continue if an If-Modified-Since header is present.
410
                if request.method in ('HEAD', 'GET'):
411
                    raise NotModified('Resource ETag matches')
412
                raise PreconditionFailed('Resource exists or ETag matches')
413

    
414

    
415
def split_container_object_string(s):
416
    if not len(s) > 0 or s[0] != '/':
417
        raise ValueError
418
    s = s[1:]
419
    pos = s.find('/')
420
    if pos == -1 or pos == len(s) - 1:
421
        raise ValueError
422
    return s[:pos], s[(pos + 1):]
423

    
424

    
425
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False, delimiter=None):
426
    """Copy or move an object."""
427

    
428
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
429
        del(request.META['CONTENT_TYPE'])
430
    content_type, meta, permissions, public = get_object_headers(request)
431
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
432
    try:
433
        if move:
434
            version_id = request.backend.move_object(
435
                request.user_uniq, src_account, src_container, src_name,
436
                dest_account, dest_container, dest_name,
437
                content_type, 'pithos', meta, False, permissions, delimiter)
438
        else:
439
            version_id = request.backend.copy_object(
440
                request.user_uniq, src_account, src_container, src_name,
441
                dest_account, dest_container, dest_name,
442
                content_type, 'pithos', meta, False, permissions, src_version, delimiter)
443
    except NotAllowedError:
444
        raise Forbidden('Not allowed')
445
    except (ItemNotExists, VersionNotExists):
446
        raise ItemNotFound('Container or object does not exist')
447
    except ValueError:
448
        raise BadRequest('Invalid sharing header')
449
    except QuotaError, e:
450
        raise RequestEntityTooLarge('Quota error: %s' % e)
451
    if public is not None:
452
        try:
453
            request.backend.update_object_public(request.user_uniq, dest_account, dest_container, dest_name, public)
454
        except NotAllowedError:
455
            raise Forbidden('Not allowed')
456
        except ItemNotExists:
457
            raise ItemNotFound('Object does not exist')
458
    return version_id
459

    
460

    
461
def get_int_parameter(p):
462
    if p is not None:
463
        try:
464
            p = int(p)
465
        except ValueError:
466
            return None
467
        if p < 0:
468
            return None
469
    return p
470

    
471

    
472
def get_content_length(request):
473
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
474
    if content_length is None:
475
        raise LengthRequired('Missing or invalid Content-Length header')
476
    return content_length
477

    
478

    
479
def get_range(request, size):
480
    """Parse a Range header from the request.
481

482
    Either returns None, when the header is not existent or should be ignored,
483
    or a list of (offset, length) tuples - should be further checked.
484
    """
485

    
486
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
487
    if not ranges.startswith('bytes='):
488
        return None
489

    
490
    ret = []
491
    for r in (x.strip() for x in ranges[6:].split(',')):
492
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
493
        m = p.match(r)
494
        if not m:
495
            return None
496
        offset = m.group('offset')
497
        upto = m.group('upto')
498
        if offset == '' and upto == '':
499
            return None
500

    
501
        if offset != '':
502
            offset = int(offset)
503
            if upto != '':
504
                upto = int(upto)
505
                if offset > upto:
506
                    return None
507
                ret.append((offset, upto - offset + 1))
508
            else:
509
                ret.append((offset, size - offset))
510
        else:
511
            length = int(upto)
512
            ret.append((size - length, length))
513

    
514
    return ret
515

    
516

    
517
def get_content_range(request):
518
    """Parse a Content-Range header from the request.
519

520
    Either returns None, when the header is not existent or should be ignored,
521
    or an (offset, length, total) tuple - check as length, total may be None.
522
    Returns (None, None, None) if the provided range is '*/*'.
523
    """
524

    
525
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
526
    if not ranges:
527
        return None
528

    
529
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
530
    m = p.match(ranges)
531
    if not m:
532
        if ranges == 'bytes */*':
533
            return (None, None, None)
534
        return None
535
    offset = int(m.group('offset'))
536
    upto = m.group('upto')
537
    total = m.group('total')
538
    if upto != '':
539
        upto = int(upto)
540
    else:
541
        upto = None
542
    if total != '*':
543
        total = int(total)
544
    else:
545
        total = None
546
    if (upto is not None and offset > upto) or \
547
        (total is not None and offset >= total) or \
548
            (total is not None and upto is not None and upto >= total):
549
        return None
550

    
551
    if upto is None:
552
        length = None
553
    else:
554
        length = upto - offset + 1
555
    return (offset, length, total)
556

    
557

    
558
def get_sharing(request):
559
    """Parse an X-Object-Sharing header from the request.
560

561
    Raises BadRequest on error.
562
    """
563

    
564
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
565
    if permissions is None:
566
        return None
567

    
568
    # TODO: Document or remove '~' replacing.
569
    permissions = permissions.replace('~', '')
570

    
571
    ret = {}
572
    permissions = permissions.replace(' ', '')
573
    if permissions == '':
574
        return ret
575
    for perm in (x for x in permissions.split(';')):
576
        if perm.startswith('read='):
577
            ret['read'] = list(set(
578
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
579
            if '' in ret['read']:
580
                ret['read'].remove('')
581
            if '*' in ret['read']:
582
                ret['read'] = ['*']
583
            if len(ret['read']) == 0:
584
                raise BadRequest(
585
                    'Bad X-Object-Sharing header value: invalid length')
586
        elif perm.startswith('write='):
587
            ret['write'] = list(set(
588
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
589
            if '' in ret['write']:
590
                ret['write'].remove('')
591
            if '*' in ret['write']:
592
                ret['write'] = ['*']
593
            if len(ret['write']) == 0:
594
                raise BadRequest(
595
                    'Bad X-Object-Sharing header value: invalid length')
596
        else:
597
            raise BadRequest(
598
                'Bad X-Object-Sharing header value: missing prefix')
599

    
600
    # replace displayname with uuid
601
#    try:
602
#        ret['read'] = \
603
#            [replace_permissions_displayname(request.token, x) for x in ret.get('read', [])]
604
#        ret['write'] = \
605
#            [replace_permissions_displayname(request.token, x) for x in ret.get('write', [])]
606
#    except ItemNotExists, e:
607
#        raise BadRequest(
608
#            'Bad X-Object-Sharing header value: unknown account: %s' % e)
609

    
610
    # Keep duplicates only in write list.
611
    dups = [x for x in ret.get(
612
        'read', []) if x in ret.get('write', []) and x != '*']
613
    if dups:
614
        for x in dups:
615
            ret['read'].remove(x)
616
        if len(ret['read']) == 0:
617
            del(ret['read'])
618

    
619
    return ret
620

    
621

    
622
def get_public(request):
623
    """Parse an X-Object-Public header from the request.
624

625
    Raises BadRequest on error.
626
    """
627

    
628
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
629
    if public is None:
630
        return None
631

    
632
    public = public.replace(' ', '').lower()
633
    if public == 'true':
634
        return True
635
    elif public == 'false' or public == '':
636
        return False
637
    raise BadRequest('Bad X-Object-Public header value')
638

    
639

    
640
def raw_input_socket(request):
641
    """Return the socket for reading the rest of the request."""
642

    
643
    server_software = request.META.get('SERVER_SOFTWARE')
644
    if server_software and server_software.startswith('mod_python'):
645
        return request._req
646
    if 'wsgi.input' in request.environ:
647
        return request.environ['wsgi.input']
648
    raise NotImplemented('Unknown server software')
649

    
650
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
651

    
652

    
653
def socket_read_iterator(request, length=0, blocksize=4096):
654
    """Return a maximum of blocksize data read from the socket in each iteration.
655

656
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
657
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
658
    """
659

    
660
    sock = raw_input_socket(request)
661
    if length < 0:  # Chunked transfers
662
        # Small version (server does the dechunking).
663
        if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
664
            while length < MAX_UPLOAD_SIZE:
665
                data = sock.read(blocksize)
666
                if data == '':
667
                    return
668
                yield data
669
            raise BadRequest('Maximum size is reached')
670

    
671
        # Long version (do the dechunking).
672
        data = ''
673
        while length < MAX_UPLOAD_SIZE:
674
            # Get chunk size.
675
            if hasattr(sock, 'readline'):
676
                chunk_length = sock.readline()
677
            else:
678
                chunk_length = ''
679
                while chunk_length[-1:] != '\n':
680
                    chunk_length += sock.read(1)
681
                chunk_length.strip()
682
            pos = chunk_length.find(';')
683
            if pos >= 0:
684
                chunk_length = chunk_length[:pos]
685
            try:
686
                chunk_length = int(chunk_length, 16)
687
            except Exception, e:
688
                raise BadRequest('Bad chunk size')
689
                                 # TODO: Change to something more appropriate.
690
            # Check if done.
691
            if chunk_length == 0:
692
                if len(data) > 0:
693
                    yield data
694
                return
695
            # Get the actual data.
696
            while chunk_length > 0:
697
                chunk = sock.read(min(chunk_length, blocksize))
698
                chunk_length -= len(chunk)
699
                if length > 0:
700
                    length += len(chunk)
701
                data += chunk
702
                if len(data) >= blocksize:
703
                    ret = data[:blocksize]
704
                    data = data[blocksize:]
705
                    yield ret
706
            sock.read(2)  # CRLF
707
        raise BadRequest('Maximum size is reached')
708
    else:
709
        if length > MAX_UPLOAD_SIZE:
710
            raise BadRequest('Maximum size is reached')
711
        while length > 0:
712
            data = sock.read(min(length, blocksize))
713
            if not data:
714
                raise BadRequest()
715
            length -= len(data)
716
            yield data
717

    
718

    
719
class SaveToBackendHandler(FileUploadHandler):
720
    """Handle a file from an HTML form the django way."""
721

    
722
    def __init__(self, request=None):
723
        super(SaveToBackendHandler, self).__init__(request)
724
        self.backend = request.backend
725

    
726
    def put_data(self, length):
727
        if len(self.data) >= length:
728
            block = self.data[:length]
729
            self.file.hashmap.append(self.backend.put_block(block))
730
            self.md5.update(block)
731
            self.data = self.data[length:]
732

    
733
    def new_file(self, field_name, file_name, content_type, content_length, charset=None):
734
        self.md5 = hashlib.md5()
735
        self.data = ''
736
        self.file = UploadedFile(
737
            name=file_name, content_type=content_type, charset=charset)
738
        self.file.size = 0
739
        self.file.hashmap = []
740

    
741
    def receive_data_chunk(self, raw_data, start):
742
        self.data += raw_data
743
        self.file.size += len(raw_data)
744
        self.put_data(self.request.backend.block_size)
745
        return None
746

    
747
    def file_complete(self, file_size):
748
        l = len(self.data)
749
        if l > 0:
750
            self.put_data(l)
751
        self.file.etag = self.md5.hexdigest().lower()
752
        return self.file
753

    
754

    
755
class ObjectWrapper(object):
756
    """Return the object's data block-per-block in each iteration.
757

758
    Read from the object using the offset and length provided in each entry of the range list.
759
    """
760

    
761
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
762
        self.backend = backend
763
        self.ranges = ranges
764
        self.sizes = sizes
765
        self.hashmaps = hashmaps
766
        self.boundary = boundary
767
        self.size = sum(self.sizes)
768

    
769
        self.file_index = 0
770
        self.block_index = 0
771
        self.block_hash = -1
772
        self.block = ''
773

    
774
        self.range_index = -1
775
        self.offset, self.length = self.ranges[0]
776

    
777
    def __iter__(self):
778
        return self
779

    
780
    def part_iterator(self):
781
        if self.length > 0:
782
            # Get the file for the current offset.
783
            file_size = self.sizes[self.file_index]
784
            while self.offset >= file_size:
785
                self.offset -= file_size
786
                self.file_index += 1
787
                file_size = self.sizes[self.file_index]
788

    
789
            # Get the block for the current position.
790
            self.block_index = int(self.offset / self.backend.block_size)
791
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
792
                self.block_hash = self.hashmaps[
793
                    self.file_index][self.block_index]
794
                try:
795
                    self.block = self.backend.get_block(self.block_hash)
796
                except ItemNotExists:
797
                    raise ItemNotFound('Block does not exist')
798

    
799
            # Get the data from the block.
800
            bo = self.offset % self.backend.block_size
801
            bs = self.backend.block_size
802
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
803
                    self.sizes[self.file_index] % self.backend.block_size):
804
                bs = self.sizes[self.file_index] % self.backend.block_size
805
            bl = min(self.length, bs - bo)
806
            data = self.block[bo:bo + bl]
807
            self.offset += bl
808
            self.length -= bl
809
            return data
810
        else:
811
            raise StopIteration
812

    
813
    def next(self):
814
        if len(self.ranges) == 1:
815
            return self.part_iterator()
816
        if self.range_index == len(self.ranges):
817
            raise StopIteration
818
        try:
819
            if self.range_index == -1:
820
                raise StopIteration
821
            return self.part_iterator()
822
        except StopIteration:
823
            self.range_index += 1
824
            out = []
825
            if self.range_index < len(self.ranges):
826
                # Part header.
827
                self.offset, self.length = self.ranges[self.range_index]
828
                self.file_index = 0
829
                if self.range_index > 0:
830
                    out.append('')
831
                out.append('--' + self.boundary)
832
                out.append('Content-Range: bytes %d-%d/%d' % (
833
                    self.offset, self.offset + self.length - 1, self.size))
834
                out.append('Content-Transfer-Encoding: binary')
835
                out.append('')
836
                out.append('')
837
                return '\r\n'.join(out)
838
            else:
839
                # Footer.
840
                out.append('')
841
                out.append('--' + self.boundary + '--')
842
                out.append('')
843
                return '\r\n'.join(out)
844

    
845

    
846
def object_data_response(request, sizes, hashmaps, meta, public=False):
847
    """Get the HttpResponse object for replying with the object's data."""
848

    
849
    # Range handling.
850
    size = sum(sizes)
851
    ranges = get_range(request, size)
852
    if ranges is None:
853
        ranges = [(0, size)]
854
        ret = 200
855
    else:
856
        check = [True for offset, length in ranges if
857
                 length <= 0 or length > size or
858
                 offset < 0 or offset >= size or
859
                 offset + length > size]
860
        if len(check) > 0:
861
            raise RangeNotSatisfiable('Requested range exceeds object limits')
862
        ret = 206
863
        if_range = request.META.get('HTTP_IF_RANGE')
864
        if if_range:
865
            try:
866
                # Modification time has passed instead.
867
                last_modified = parse_http_date(if_range)
868
                if last_modified != meta['modified']:
869
                    ranges = [(0, size)]
870
                    ret = 200
871
            except ValueError:
872
                if if_range != meta['checksum']:
873
                    ranges = [(0, size)]
874
                    ret = 200
875

    
876
    if ret == 206 and len(ranges) > 1:
877
        boundary = uuid.uuid4().hex
878
    else:
879
        boundary = ''
880
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
881
    response = HttpResponse(wrapper, status=ret)
882
    put_object_headers(response, meta, public)
883
    if ret == 206:
884
        if len(ranges) == 1:
885
            offset, length = ranges[0]
886
            response[
887
                'Content-Length'] = length  # Update with the correct length.
888
            response['Content-Range'] = 'bytes %d-%d/%d' % (
889
                offset, offset + length - 1, size)
890
        else:
891
            del(response['Content-Length'])
892
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
893
                boundary,)
894
    return response
895

    
896

    
897
def put_object_block(request, hashmap, data, offset):
898
    """Put one block of data at the given offset."""
899

    
900
    bi = int(offset / request.backend.block_size)
901
    bo = offset % request.backend.block_size
902
    bl = min(len(data), request.backend.block_size - bo)
903
    if bi < len(hashmap):
904
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
905
    else:
906
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
907
    return bl  # Return ammount of data written.
908

    
909

    
910
def hashmap_md5(backend, hashmap, size):
911
    """Produce the MD5 sum from the data in the hashmap."""
912

    
913
    # TODO: Search backend for the MD5 of another object with the same hashmap and size...
914
    md5 = hashlib.md5()
915
    bs = backend.block_size
916
    for bi, hash in enumerate(hashmap):
917
        data = backend.get_block(hash)  # Blocks come in padded.
918
        if bi == len(hashmap) - 1:
919
            data = data[:size % bs]
920
        md5.update(data)
921
    return md5.hexdigest().lower()
922

    
923

    
924
def simple_list_response(request, l):
925
    if request.serialization == 'text':
926
        return '\n'.join(l) + '\n'
927
    if request.serialization == 'xml':
928
        return render_to_string('items.xml', {'items': l})
929
    if request.serialization == 'json':
930
        return json.dumps(l)
931

    
932

    
933
from pithos.backends.util import PithosBackendPool
934
POOL_SIZE = 5
935
if RADOS_STORAGE:
936
    BLOCK_PARAMS = { 'mappool': RADOS_POOL_MAPS,
937
                     'blockpool': RADOS_POOL_BLOCKS,
938
                   }
939
else:
940
    BLOCK_PARAMS = { 'mappool': None,
941
                     'blockpool': None,
942
                   }
943

    
944

    
945
_pithos_backend_pool = PithosBackendPool(size=POOL_SIZE,
946
                                         db_module=BACKEND_DB_MODULE,
947
                                         db_connection=BACKEND_DB_CONNECTION,
948
                                         block_module=BACKEND_BLOCK_MODULE,
949
                                         block_path=BACKEND_BLOCK_PATH,
950
                                         block_umask=BACKEND_BLOCK_UMASK,
951
                                         queue_module=BACKEND_QUEUE_MODULE,
952
                                         queue_hosts=BACKEND_QUEUE_HOSTS,
953
                                         queue_exchange=BACKEND_QUEUE_EXCHANGE,
954
                                         quotaholder_url=QUOTAHOLDER_URL,
955
                                         quotaholder_token=QUOTAHOLDER_TOKEN,
956
                                         free_versioning=BACKEND_FREE_VERSIONING,
957
                                         block_params=BLOCK_PARAMS)
958

    
959
def get_backend():
960
    backend = _pithos_backend_pool.pool_get()
961
    backend.default_policy['quota'] = BACKEND_QUOTA
962
    backend.default_policy['versioning'] = BACKEND_VERSIONING
963
    backend.messages = []
964
    return backend
965

    
966

    
967
def update_request_headers(request):
968
    # Handle URL-encoded keys and values.
969
    meta = dict([(
970
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
971
    for k, v in meta.iteritems():
972
        try:
973
            k.decode('ascii')
974
            v.decode('ascii')
975
        except UnicodeDecodeError:
976
            raise BadRequest('Bad character in headers.')
977
        if '%' in k or '%' in v:
978
            del(request.META[k])
979
            request.META[unquote(k)] = smart_unicode(unquote(
980
                v), strings_only=True)
981

    
982

    
983
def update_response_headers(request, response):
984
    if request.serialization == 'xml':
985
        response['Content-Type'] = 'application/xml; charset=UTF-8'
986
    elif request.serialization == 'json':
987
        response['Content-Type'] = 'application/json; charset=UTF-8'
988
    elif not response['Content-Type']:
989
        response['Content-Type'] = 'text/plain; charset=UTF-8'
990

    
991
    if (not response.has_header('Content-Length') and
992
        not (response.has_header('Content-Type') and
993
             response['Content-Type'].startswith('multipart/byteranges'))):
994
        response['Content-Length'] = len(response.content)
995

    
996
    # URL-encode unicode in headers.
997
    meta = response.items()
998
    for k, v in meta:
999
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1000
                k.startswith('X-Object-') or k.startswith('Content-')):
1001
            del(response[k])
1002
            response[quote(k)] = quote(v, safe='/=,:@; ')
1003

    
1004

    
1005
def render_fault(request, fault):
1006
    if isinstance(fault, InternalServerError) and settings.DEBUG:
1007
        fault.details = format_exc(fault)
1008

    
1009
    request.serialization = 'text'
1010
    data = fault.message + '\n'
1011
    if fault.details:
1012
        data += '\n' + fault.details
1013
    response = HttpResponse(data, status=fault.code)
1014
    update_response_headers(request, response)
1015
    return response
1016

    
1017

    
1018
def request_serialization(request, format_allowed=False):
1019
    """Return the serialization format requested.
1020

1021
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
1022
    """
1023

    
1024
    if not format_allowed:
1025
        return 'text'
1026

    
1027
    format = request.GET.get('format')
1028
    if format == 'json':
1029
        return 'json'
1030
    elif format == 'xml':
1031
        return 'xml'
1032

    
1033
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
1034
        accept, sep, rest = item.strip().partition(';')
1035
        if accept == 'application/json':
1036
            return 'json'
1037
        elif accept == 'application/xml' or accept == 'text/xml':
1038
            return 'xml'
1039

    
1040
    return 'text'
1041

    
1042
def get_pithos_usage(usage):
1043
    for u in usage:
1044
        if u.get('name') == 'pithos+.diskspace':
1045
            return u
1046

    
1047
def api_method(http_method=None, format_allowed=False, user_required=True,
1048
        request_usage=False):
1049
    """Decorator function for views that implement an API method."""
1050

    
1051
    def decorator(func):
1052
        @wraps(func)
1053
        def wrapper(request, *args, **kwargs):
1054
            try:
1055
                if http_method and request.method != http_method:
1056
                    raise BadRequest('Method not allowed.')
1057

    
1058
                if user_required:
1059
                    token = None
1060
                    if request.method in ('HEAD', 'GET') and COOKIE_NAME in request.COOKIES:
1061
                        cookie_value = unquote(
1062
                            request.COOKIES.get(COOKIE_NAME, ''))
1063
                        account, sep, token = cookie_value.partition('|')
1064
                    get_user(request,
1065
                             AUTHENTICATION_URL,
1066
                             AUTHENTICATION_USERS,
1067
                             token,
1068
                             user_required)
1069
                    if  getattr(request, 'user', None) is None:
1070
                        raise Unauthorized('Access denied')
1071
                    assert getattr(request, 'user_uniq', None) != None
1072
                    request.user_usage = get_pithos_usage(request.user.get('usage', []))
1073
                    request.token = request.GET.get('X-Auth-Token', request.META.get('HTTP_X_AUTH_TOKEN', token))
1074

    
1075
                # The args variable may contain up to (account, container, object).
1076
                if len(args) > 1 and len(args[1]) > 256:
1077
                    raise BadRequest('Container name too large.')
1078
                if len(args) > 2 and len(args[2]) > 1024:
1079
                    raise BadRequest('Object name too large.')
1080

    
1081
                # Format and check headers.
1082
                update_request_headers(request)
1083

    
1084
                # Fill in custom request variables.
1085
                request.serialization = request_serialization(
1086
                    request, format_allowed)
1087
                request.backend = get_backend()
1088

    
1089
                response = func(request, *args, **kwargs)
1090
                update_response_headers(request, response)
1091
                return response
1092
            except Fault, fault:
1093
                if fault.code >= 500:
1094
                    logger.exception("API Fault")
1095
                return render_fault(request, fault)
1096
            except BaseException, e:
1097
                logger.exception('Unexpected error: %s' % e)
1098
                fault = InternalServerError('Unexpected error: %s' % e)
1099
                return render_fault(request, fault)
1100
            finally:
1101
                if getattr(request, 'backend', None) is not None:
1102
                    request.backend.close()
1103
        return wrapper
1104
    return decorator