Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 02de6286

History | View | Annotate | Download (38.7 kB)

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

    
34
from functools import wraps
35
from datetime import datetime
36
from urllib import quote, unquote
37

    
38
from django.http import HttpResponse
39
from django.template.loader import render_to_string
40
from django.utils import simplejson as json
41
from django.utils.http import http_date, parse_etags
42
from django.utils.encoding import smart_unicode, smart_str
43
from django.core.files.uploadhandler import FileUploadHandler
44
from django.core.files.uploadedfile import UploadedFile
45

    
46
from snf_django.lib.api.parsedate import parse_http_date_safe, parse_http_date
47
from snf_django.lib import api
48
from snf_django.lib.api import faults, utils
49

    
50
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
51
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
52
                                 BACKEND_BLOCK_UMASK,
53
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
54
                                 BACKEND_QUEUE_EXCHANGE,
55
                                 ASTAKOSCLIENT_POOLSIZE,
56
                                 SERVICE_TOKEN,
57
                                 ASTAKOS_URL,
58
                                 BACKEND_ACCOUNT_QUOTA, BACKEND_CONTAINER_QUOTA,
59
                                 BACKEND_VERSIONING,
60
                                 BACKEND_FREE_VERSIONING, BACKEND_POOL_SIZE,
61
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
62
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
63
                                 PUBLIC_URL_SECURITY,
64
                                 PUBLIC_URL_ALPHABET)
65
from pithos.api.resources import resources
66
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
67
                                  VersionNotExists)
68
from astakosclient import AstakosClient
69
from astakosclient.errors import NoUserName, NoUUID
70

    
71
import logging
72
import re
73
import hashlib
74
import uuid
75
import decimal
76

    
77
logger = logging.getLogger(__name__)
78

    
79

    
80
def json_encode_decimal(obj):
81
    if isinstance(obj, decimal.Decimal):
82
        return str(obj)
83
    raise TypeError(repr(obj) + " is not JSON serializable")
84

    
85

    
86
def rename_meta_key(d, old, new):
87
    if old not in d:
88
        return
89
    d[new] = d[old]
90
    del(d[old])
91

    
92

    
93
def printable_header_dict(d):
94
    """Format a meta dictionary for printing out json/xml.
95

96
    Convert all keys to lower case and replace dashes with underscores.
97
    Format 'last_modified' timestamp.
98
    """
99

    
100
    if 'last_modified' in d and d['last_modified']:
101
        d['last_modified'] = utils.isoformat(
102
            datetime.fromtimestamp(d['last_modified']))
103
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
104

    
105

    
106
def format_header_key(k):
107
    """Convert underscores to dashes and capitalize intra-dash strings."""
108
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
109

    
110

    
111
def get_header_prefix(request, prefix):
112
    """Get all prefix-* request headers in a dict.
113
       Reformat keys with format_header_key()."""
114

    
115
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
116
    # TODO: Document or remove '~' replacing.
117
    return dict([(format_header_key(k[5:]), v.replace('~', ''))
118
                for k, v in request.META.iteritems()
119
                if k.startswith(prefix) and len(k) > len(prefix)])
120

    
121

    
122
def check_meta_headers(meta):
123
    if len(meta) > 90:
124
        raise faults.BadRequest('Too many headers.')
125
    for k, v in meta.iteritems():
126
        if len(k) > 128:
127
            raise faults.BadRequest('Header name too large.')
128
        if len(v) > 256:
129
            raise faults.BadRequest('Header value too large.')
130

    
131

    
132
def get_account_headers(request):
133
    meta = get_header_prefix(request, 'X-Account-Meta-')
134
    check_meta_headers(meta)
135
    groups = {}
136
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
137
        n = k[16:].lower()
138
        if '-' in n or '_' in n:
139
            raise faults.BadRequest('Bad characters in group name')
140
        groups[n] = v.replace(' ', '').split(',')
141
        while '' in groups[n]:
142
            groups[n].remove('')
143
    return meta, groups
144

    
145

    
146
def put_account_headers(response, meta, groups, policy):
147
    if 'count' in meta:
148
        response['X-Account-Container-Count'] = meta['count']
149
    if 'bytes' in meta:
150
        response['X-Account-Bytes-Used'] = meta['bytes']
151
    response['Last-Modified'] = http_date(int(meta['modified']))
152
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
153
        response[smart_str(
154
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
155
    if 'until_timestamp' in meta:
156
        response['X-Account-Until-Timestamp'] = http_date(
157
            int(meta['until_timestamp']))
158
    for k, v in groups.iteritems():
159
        k = smart_str(k, strings_only=True)
160
        k = format_header_key('X-Account-Group-' + k)
161
        v = smart_str(','.join(v), strings_only=True)
162
        response[k] = v
163
    for k, v in policy.iteritems():
164
        response[smart_str(format_header_key('X-Account-Policy-' + k),
165
                 strings_only=True)] = smart_str(v, strings_only=True)
166

    
167

    
168
def get_container_headers(request):
169
    meta = get_header_prefix(request, 'X-Container-Meta-')
170
    check_meta_headers(meta)
171
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in
172
                  get_header_prefix(request,
173
                                    'X-Container-Policy-').iteritems()])
174
    return meta, policy
175

    
176

    
177
def put_container_headers(request, response, meta, policy):
178
    if 'count' in meta:
179
        response['X-Container-Object-Count'] = meta['count']
180
    if 'bytes' in meta:
181
        response['X-Container-Bytes-Used'] = meta['bytes']
182
    response['Last-Modified'] = http_date(int(meta['modified']))
183
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
184
        response[smart_str(
185
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
186
    l = [smart_str(x, strings_only=True) for x in meta['object_meta']
187
         if x.startswith('X-Object-Meta-')]
188
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
189
    response['X-Container-Block-Size'] = request.backend.block_size
190
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
191
    if 'until_timestamp' in meta:
192
        response['X-Container-Until-Timestamp'] = http_date(
193
            int(meta['until_timestamp']))
194
    for k, v in policy.iteritems():
195
        response[smart_str(format_header_key('X-Container-Policy-' + k),
196
                           strings_only=True)] = smart_str(v, strings_only=True)
197

    
198

    
199
def get_object_headers(request):
200
    content_type = request.META.get('CONTENT_TYPE', None)
201
    meta = get_header_prefix(request, 'X-Object-Meta-')
202
    check_meta_headers(meta)
203
    if request.META.get('HTTP_CONTENT_ENCODING'):
204
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
205
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
206
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
207
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
208
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
209
    return content_type, meta, get_sharing(request), get_public(request)
210

    
211

    
212
def put_object_headers(response, meta, restricted=False, token=None):
213
    response['ETag'] = meta['checksum']
214
    response['Content-Length'] = meta['bytes']
215
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
216
    response['Last-Modified'] = http_date(int(meta['modified']))
217
    if not restricted:
218
        response['X-Object-Hash'] = meta['hash']
219
        response['X-Object-UUID'] = meta['uuid']
220
        if TRANSLATE_UUIDS:
221
            meta['modified_by'] = \
222
                retrieve_displayname(token, meta['modified_by'])
223
        response['X-Object-Modified-By'] = smart_str(
224
            meta['modified_by'], strings_only=True)
225
        response['X-Object-Version'] = meta['version']
226
        response['X-Object-Version-Timestamp'] = http_date(
227
            int(meta['version_timestamp']))
228
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
229
            response[smart_str(
230
                k, strings_only=True)] = smart_str(meta[k], strings_only=True)
231
        for k in (
232
            'Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
233
            'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
234
                'X-Object-Public'):
235
            if k in meta:
236
                response[k] = smart_str(meta[k], strings_only=True)
237
    else:
238
        for k in ('Content-Encoding', 'Content-Disposition'):
239
            if k in meta:
240
                response[k] = smart_str(meta[k], strings_only=True)
241

    
242

    
243
def update_manifest_meta(request, v_account, meta):
244
    """Update metadata if the object has an X-Object-Manifest."""
245

    
246
    if 'X-Object-Manifest' in meta:
247
        etag = ''
248
        bytes = 0
249
        try:
250
            src_container, src_name = split_container_object_string(
251
                '/' + meta['X-Object-Manifest'])
252
            objects = request.backend.list_objects(
253
                request.user_uniq, v_account,
254
                src_container, prefix=src_name, virtual=False)
255
            for x in objects:
256
                src_meta = request.backend.get_object_meta(request.user_uniq,
257
                                                           v_account,
258
                                                           src_container,
259
                                                           x[0], 'pithos', x[1])
260
                etag += src_meta['checksum']
261
                bytes += src_meta['bytes']
262
        except:
263
            # Ignore errors.
264
            return
265
        meta['bytes'] = bytes
266
        md5 = hashlib.md5()
267
        md5.update(etag)
268
        meta['checksum'] = md5.hexdigest().lower()
269

    
270

    
271
def is_uuid(str):
272
    if str is None:
273
        return False
274
    try:
275
        uuid.UUID(str)
276
    except ValueError:
277
        return False
278
    else:
279
        return True
280

    
281

    
282
##########################
283
# USER CATALOG utilities #
284
##########################
285

    
286
def retrieve_displayname(token, uuid, fail_silently=True):
287
    astakos = AstakosClient(ASTAKOS_URL, retry=2, use_pool=True, logger=logger)
288
    try:
289
        displayname = astakos.get_username(token, uuid)
290
    except NoUserName:
291
        if not fail_silently:
292
            raise ItemNotExists(uuid)
293
        else:
294
            # just return the uuid
295
            return uuid
296
    return displayname
297

    
298

    
299
def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True):
300
    astakos = AstakosClient(ASTAKOS_URL, retry=2, use_pool=True, logger=logger)
301
    catalog = astakos.get_usernames(token, uuids) or {}
302
    missing = list(set(uuids) - set(catalog))
303
    if missing and not fail_silently:
304
        raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing))
305
    return catalog if return_dict else [catalog.get(i) for i in uuids]
306

    
307

    
308
def retrieve_uuid(token, displayname):
309
    if is_uuid(displayname):
310
        return displayname
311

    
312
    astakos = AstakosClient(ASTAKOS_URL, retry=2, use_pool=True, logger=logger)
313
    try:
314
        uuid = astakos.get_uuid(token, displayname)
315
    except NoUUID:
316
        raise ItemNotExists(displayname)
317
    return uuid
318

    
319

    
320
def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True):
321
    astakos = AstakosClient(ASTAKOS_URL, retry=2, use_pool=True, logger=logger)
322
    catalog = astakos.get_uuids(token, displaynames) or {}
323
    missing = list(set(displaynames) - set(catalog))
324
    if missing and not fail_silently:
325
        raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing))
326
    return catalog if return_dict else [catalog.get(i) for i in displaynames]
327

    
328

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

    
340

    
341
def replace_permissions_uuid(token, holder):
342
    if holder == '*':
343
        return holder
344
    try:
345
        # check first for a group permission
346
        account, group = holder.split(':', 1)
347
    except ValueError:
348
        return retrieve_displayname(token, holder)
349
    else:
350
        return ':'.join([retrieve_displayname(token, account), group])
351

    
352

    
353
def update_sharing_meta(request, permissions, v_account,
354
                        v_container, v_object, meta):
355
    if permissions is None:
356
        return
357
    allowed, perm_path, perms = permissions
358
    if len(perms) == 0:
359
        return
360

    
361
    # replace uuid with displayname
362
    if TRANSLATE_UUIDS:
363
        perms['read'] = [replace_permissions_uuid(
364
            getattr(request, 'token', None), x)
365
            for x in perms.get('read', [])]
366
        perms['write'] = [replace_permissions_uuid(
367
            getattr(request, 'token', None), x)
368
            for x in perms.get('write', [])]
369

    
370
    ret = []
371

    
372
    r = ','.join(perms.get('read', []))
373
    if r:
374
        ret.append('read=' + r)
375
    w = ','.join(perms.get('write', []))
376
    if w:
377
        ret.append('write=' + w)
378
    meta['X-Object-Sharing'] = '; '.join(ret)
379
    if '/'.join((v_account, v_container, v_object)) != perm_path:
380
        meta['X-Object-Shared-By'] = perm_path
381
    if request.user_uniq != v_account:
382
        meta['X-Object-Allowed-To'] = allowed
383

    
384

    
385
def update_public_meta(public, meta):
386
    if not public:
387
        return
388
    meta['X-Object-Public'] = '/public/' + public
389

    
390

    
391
def validate_modification_preconditions(request, meta):
392
    """Check that the modified timestamp conforms with the preconditions set."""
393

    
394
    if 'modified' not in meta:
395
        return  # TODO: Always return?
396

    
397
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
398
    if if_modified_since is not None:
399
        if_modified_since = parse_http_date_safe(if_modified_since)
400
    if (if_modified_since is not None
401
            and int(meta['modified']) <= if_modified_since):
402
        raise faults.NotModified('Resource has not been modified')
403

    
404
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
405
    if if_unmodified_since is not None:
406
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
407
    if (if_unmodified_since is not None
408
            and int(meta['modified']) > if_unmodified_since):
409
        raise faults.PreconditionFailed('Resource has been modified')
410

    
411

    
412
def validate_matching_preconditions(request, meta):
413
    """Check that the ETag conforms with the preconditions set."""
414

    
415
    etag = meta['checksum']
416
    if not etag:
417
        etag = None
418

    
419
    if_match = request.META.get('HTTP_IF_MATCH')
420
    if if_match is not None:
421
        if etag is None:
422
            raise faults.PreconditionFailed('Resource does not exist')
423
        if (if_match != '*'
424
                and etag not in [x.lower() for x in parse_etags(if_match)]):
425
            raise faults.PreconditionFailed('Resource ETag does not match')
426

    
427
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
428
    if if_none_match is not None:
429
        # TODO: If this passes, must ignore If-Modified-Since header.
430
        if etag is not None:
431
            if (if_none_match == '*'
432
                    or etag in [x.lower() for x in parse_etags(if_none_match)]):
433
                # TODO: Continue if an If-Modified-Since header is present.
434
                if request.method in ('HEAD', 'GET'):
435
                    raise faults.NotModified('Resource ETag matches')
436
                raise faults.PreconditionFailed(
437
                    'Resource exists or ETag matches')
438

    
439

    
440
def split_container_object_string(s):
441
    if not len(s) > 0 or s[0] != '/':
442
        raise ValueError
443
    s = s[1:]
444
    pos = s.find('/')
445
    if pos == -1 or pos == len(s) - 1:
446
        raise ValueError
447
    return s[:pos], s[(pos + 1):]
448

    
449

    
450
def copy_or_move_object(request, src_account, src_container, src_name,
451
                        dest_account, dest_container, dest_name,
452
                        move=False, delimiter=None):
453
    """Copy or move an object."""
454

    
455
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
456
        del(request.META['CONTENT_TYPE'])
457
    content_type, meta, permissions, public = get_object_headers(request)
458
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
459
    try:
460
        if move:
461
            version_id = request.backend.move_object(
462
                request.user_uniq, src_account, src_container, src_name,
463
                dest_account, dest_container, dest_name,
464
                content_type, 'pithos', meta, False, permissions, delimiter)
465
        else:
466
            version_id = request.backend.copy_object(
467
                request.user_uniq, src_account, src_container, src_name,
468
                dest_account, dest_container, dest_name,
469
                content_type, 'pithos', meta, False, permissions,
470
                src_version, delimiter)
471
    except NotAllowedError:
472
        raise faults.Forbidden('Not allowed')
473
    except (ItemNotExists, VersionNotExists):
474
        raise faults.ItemNotFound('Container or object does not exist')
475
    except ValueError:
476
        raise faults.BadRequest('Invalid sharing header')
477
    except QuotaError, e:
478
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
479
    if public is not None:
480
        try:
481
            request.backend.update_object_public(
482
                request.user_uniq, dest_account,
483
                dest_container, dest_name, public)
484
        except NotAllowedError:
485
            raise faults.Forbidden('Not allowed')
486
        except ItemNotExists:
487
            raise faults.ItemNotFound('Object does not exist')
488
    return version_id
489

    
490

    
491
def get_int_parameter(p):
492
    if p is not None:
493
        try:
494
            p = int(p)
495
        except ValueError:
496
            return None
497
        if p < 0:
498
            return None
499
    return p
500

    
501

    
502
def get_content_length(request):
503
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
504
    if content_length is None:
505
        raise faults.LengthRequired('Missing or invalid Content-Length header')
506
    return content_length
507

    
508

    
509
def get_range(request, size):
510
    """Parse a Range header from the request.
511

512
    Either returns None, when the header is not existent or should be ignored,
513
    or a list of (offset, length) tuples - should be further checked.
514
    """
515

    
516
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
517
    if not ranges.startswith('bytes='):
518
        return None
519

    
520
    ret = []
521
    for r in (x.strip() for x in ranges[6:].split(',')):
522
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
523
        m = p.match(r)
524
        if not m:
525
            return None
526
        offset = m.group('offset')
527
        upto = m.group('upto')
528
        if offset == '' and upto == '':
529
            return None
530

    
531
        if offset != '':
532
            offset = int(offset)
533
            if upto != '':
534
                upto = int(upto)
535
                if offset > upto:
536
                    return None
537
                ret.append((offset, upto - offset + 1))
538
            else:
539
                ret.append((offset, size - offset))
540
        else:
541
            length = int(upto)
542
            ret.append((size - length, length))
543

    
544
    return ret
545

    
546

    
547
def get_content_range(request):
548
    """Parse a Content-Range header from the request.
549

550
    Either returns None, when the header is not existent or should be ignored,
551
    or an (offset, length, total) tuple - check as length, total may be None.
552
    Returns (None, None, None) if the provided range is '*/*'.
553
    """
554

    
555
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
556
    if not ranges:
557
        return None
558

    
559
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
560
    m = p.match(ranges)
561
    if not m:
562
        if ranges == 'bytes */*':
563
            return (None, None, None)
564
        return None
565
    offset = int(m.group('offset'))
566
    upto = m.group('upto')
567
    total = m.group('total')
568
    if upto != '':
569
        upto = int(upto)
570
    else:
571
        upto = None
572
    if total != '*':
573
        total = int(total)
574
    else:
575
        total = None
576
    if (upto is not None and offset > upto) or \
577
        (total is not None and offset >= total) or \
578
            (total is not None and upto is not None and upto >= total):
579
        return None
580

    
581
    if upto is None:
582
        length = None
583
    else:
584
        length = upto - offset + 1
585
    return (offset, length, total)
586

    
587

    
588
def get_sharing(request):
589
    """Parse an X-Object-Sharing header from the request.
590

591
    Raises BadRequest on error.
592
    """
593

    
594
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
595
    if permissions is None:
596
        return None
597

    
598
    # TODO: Document or remove '~' replacing.
599
    permissions = permissions.replace('~', '')
600

    
601
    ret = {}
602
    permissions = permissions.replace(' ', '')
603
    if permissions == '':
604
        return ret
605
    for perm in (x for x in permissions.split(';')):
606
        if perm.startswith('read='):
607
            ret['read'] = list(set(
608
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
609
            if '' in ret['read']:
610
                ret['read'].remove('')
611
            if '*' in ret['read']:
612
                ret['read'] = ['*']
613
            if len(ret['read']) == 0:
614
                raise faults.BadRequest(
615
                    'Bad X-Object-Sharing header value: invalid length')
616
        elif perm.startswith('write='):
617
            ret['write'] = list(set(
618
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
619
            if '' in ret['write']:
620
                ret['write'].remove('')
621
            if '*' in ret['write']:
622
                ret['write'] = ['*']
623
            if len(ret['write']) == 0:
624
                raise faults.BadRequest(
625
                    'Bad X-Object-Sharing header value: invalid length')
626
        else:
627
            raise faults.BadRequest(
628
                'Bad X-Object-Sharing header value: missing prefix')
629

    
630
    # replace displayname with uuid
631
    if TRANSLATE_UUIDS:
632
        try:
633
            ret['read'] = [replace_permissions_displayname(
634
                getattr(request, 'token', None), x)
635
                for x in ret.get('read', [])]
636
            ret['write'] = [replace_permissions_displayname(
637
                getattr(request, 'token', None), x)
638
                for x in ret.get('write', [])]
639
        except ItemNotExists, e:
640
            raise faults.BadRequest(
641
                'Bad X-Object-Sharing header value: unknown account: %s' % e)
642

    
643
    # Keep duplicates only in write list.
644
    dups = [x for x in ret.get(
645
        'read', []) if x in ret.get('write', []) and x != '*']
646
    if dups:
647
        for x in dups:
648
            ret['read'].remove(x)
649
        if len(ret['read']) == 0:
650
            del(ret['read'])
651

    
652
    return ret
653

    
654

    
655
def get_public(request):
656
    """Parse an X-Object-Public header from the request.
657

658
    Raises BadRequest on error.
659
    """
660

    
661
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
662
    if public is None:
663
        return None
664

    
665
    public = public.replace(' ', '').lower()
666
    if public == 'true':
667
        return True
668
    elif public == 'false' or public == '':
669
        return False
670
    raise faults.BadRequest('Bad X-Object-Public header value')
671

    
672

    
673
def raw_input_socket(request):
674
    """Return the socket for reading the rest of the request."""
675

    
676
    server_software = request.META.get('SERVER_SOFTWARE')
677
    if server_software and server_software.startswith('mod_python'):
678
        return request._req
679
    if 'wsgi.input' in request.environ:
680
        return request.environ['wsgi.input']
681
    raise NotImplemented('Unknown server software')
682

    
683
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
684

    
685

    
686
def socket_read_iterator(request, length=0, blocksize=4096):
687
    """Return a maximum of blocksize data read from the socket in each iteration
688

689
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
690
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
691
    """
692

    
693
    sock = raw_input_socket(request)
694
    if length < 0:  # Chunked transfers
695
        # Small version (server does the dechunking).
696
        if (request.environ.get('mod_wsgi.input_chunked', None)
697
                or request.META['SERVER_SOFTWARE'].startswith('gunicorn')):
698
            while length < MAX_UPLOAD_SIZE:
699
                data = sock.read(blocksize)
700
                if data == '':
701
                    return
702
                yield data
703
            raise faults.BadRequest('Maximum size is reached')
704

    
705
        # Long version (do the dechunking).
706
        data = ''
707
        while length < MAX_UPLOAD_SIZE:
708
            # Get chunk size.
709
            if hasattr(sock, 'readline'):
710
                chunk_length = sock.readline()
711
            else:
712
                chunk_length = ''
713
                while chunk_length[-1:] != '\n':
714
                    chunk_length += sock.read(1)
715
                chunk_length.strip()
716
            pos = chunk_length.find(';')
717
            if pos >= 0:
718
                chunk_length = chunk_length[:pos]
719
            try:
720
                chunk_length = int(chunk_length, 16)
721
            except Exception:
722
                raise faults.BadRequest('Bad chunk size')
723
                                 # TODO: Change to something more appropriate.
724
            # Check if done.
725
            if chunk_length == 0:
726
                if len(data) > 0:
727
                    yield data
728
                return
729
            # Get the actual data.
730
            while chunk_length > 0:
731
                chunk = sock.read(min(chunk_length, blocksize))
732
                chunk_length -= len(chunk)
733
                if length > 0:
734
                    length += len(chunk)
735
                data += chunk
736
                if len(data) >= blocksize:
737
                    ret = data[:blocksize]
738
                    data = data[blocksize:]
739
                    yield ret
740
            sock.read(2)  # CRLF
741
        raise faults.BadRequest('Maximum size is reached')
742
    else:
743
        if length > MAX_UPLOAD_SIZE:
744
            raise faults.BadRequest('Maximum size is reached')
745
        while length > 0:
746
            data = sock.read(min(length, blocksize))
747
            if not data:
748
                raise faults.BadRequest()
749
            length -= len(data)
750
            yield data
751

    
752

    
753
class SaveToBackendHandler(FileUploadHandler):
754
    """Handle a file from an HTML form the django way."""
755

    
756
    def __init__(self, request=None):
757
        super(SaveToBackendHandler, self).__init__(request)
758
        self.backend = request.backend
759

    
760
    def put_data(self, length):
761
        if len(self.data) >= length:
762
            block = self.data[:length]
763
            self.file.hashmap.append(self.backend.put_block(block))
764
            self.md5.update(block)
765
            self.data = self.data[length:]
766

    
767
    def new_file(self, field_name, file_name, content_type,
768
                 content_length, charset=None):
769
        self.md5 = hashlib.md5()
770
        self.data = ''
771
        self.file = UploadedFile(
772
            name=file_name, content_type=content_type, charset=charset)
773
        self.file.size = 0
774
        self.file.hashmap = []
775

    
776
    def receive_data_chunk(self, raw_data, start):
777
        self.data += raw_data
778
        self.file.size += len(raw_data)
779
        self.put_data(self.request.backend.block_size)
780
        return None
781

    
782
    def file_complete(self, file_size):
783
        l = len(self.data)
784
        if l > 0:
785
            self.put_data(l)
786
        self.file.etag = self.md5.hexdigest().lower()
787
        return self.file
788

    
789

    
790
class ObjectWrapper(object):
791
    """Return the object's data block-per-block in each iteration.
792

793
    Read from the object using the offset and length provided
794
    in each entry of the range list.
795
    """
796

    
797
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
798
        self.backend = backend
799
        self.ranges = ranges
800
        self.sizes = sizes
801
        self.hashmaps = hashmaps
802
        self.boundary = boundary
803
        self.size = sum(self.sizes)
804

    
805
        self.file_index = 0
806
        self.block_index = 0
807
        self.block_hash = -1
808
        self.block = ''
809

    
810
        self.range_index = -1
811
        self.offset, self.length = self.ranges[0]
812

    
813
    def __iter__(self):
814
        return self
815

    
816
    def part_iterator(self):
817
        if self.length > 0:
818
            # Get the file for the current offset.
819
            file_size = self.sizes[self.file_index]
820
            while self.offset >= file_size:
821
                self.offset -= file_size
822
                self.file_index += 1
823
                file_size = self.sizes[self.file_index]
824

    
825
            # Get the block for the current position.
826
            self.block_index = int(self.offset / self.backend.block_size)
827
            if self.block_hash != \
828
                    self.hashmaps[self.file_index][self.block_index]:
829
                self.block_hash = self.hashmaps[
830
                    self.file_index][self.block_index]
831
                try:
832
                    self.block = self.backend.get_block(self.block_hash)
833
                except ItemNotExists:
834
                    raise faults.ItemNotFound('Block does not exist')
835

    
836
            # Get the data from the block.
837
            bo = self.offset % self.backend.block_size
838
            bs = self.backend.block_size
839
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
840
                    self.sizes[self.file_index] % self.backend.block_size):
841
                bs = self.sizes[self.file_index] % self.backend.block_size
842
            bl = min(self.length, bs - bo)
843
            data = self.block[bo:bo + bl]
844
            self.offset += bl
845
            self.length -= bl
846
            return data
847
        else:
848
            raise StopIteration
849

    
850
    def next(self):
851
        if len(self.ranges) == 1:
852
            return self.part_iterator()
853
        if self.range_index == len(self.ranges):
854
            raise StopIteration
855
        try:
856
            if self.range_index == -1:
857
                raise StopIteration
858
            return self.part_iterator()
859
        except StopIteration:
860
            self.range_index += 1
861
            out = []
862
            if self.range_index < len(self.ranges):
863
                # Part header.
864
                self.offset, self.length = self.ranges[self.range_index]
865
                self.file_index = 0
866
                if self.range_index > 0:
867
                    out.append('')
868
                out.append('--' + self.boundary)
869
                out.append('Content-Range: bytes %d-%d/%d' % (
870
                    self.offset, self.offset + self.length - 1, self.size))
871
                out.append('Content-Transfer-Encoding: binary')
872
                out.append('')
873
                out.append('')
874
                return '\r\n'.join(out)
875
            else:
876
                # Footer.
877
                out.append('')
878
                out.append('--' + self.boundary + '--')
879
                out.append('')
880
                return '\r\n'.join(out)
881

    
882

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

    
886
    # Range handling.
887
    size = sum(sizes)
888
    ranges = get_range(request, size)
889
    if ranges is None:
890
        ranges = [(0, size)]
891
        ret = 200
892
    else:
893
        check = [True for offset, length in ranges if
894
                 length <= 0 or length > size or
895
                 offset < 0 or offset >= size or
896
                 offset + length > size]
897
        if len(check) > 0:
898
            raise faults.RangeNotSatisfiable(
899
                'Requested range exceeds object limits')
900
        ret = 206
901
        if_range = request.META.get('HTTP_IF_RANGE')
902
        if if_range:
903
            try:
904
                # Modification time has passed instead.
905
                last_modified = parse_http_date(if_range)
906
                if last_modified != meta['modified']:
907
                    ranges = [(0, size)]
908
                    ret = 200
909
            except ValueError:
910
                if if_range != meta['checksum']:
911
                    ranges = [(0, size)]
912
                    ret = 200
913

    
914
    if ret == 206 and len(ranges) > 1:
915
        boundary = uuid.uuid4().hex
916
    else:
917
        boundary = ''
918
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
919
    response = HttpResponse(wrapper, status=ret)
920
    put_object_headers(
921
        response, meta, restricted=public,
922
        token=getattr(request, 'token', None))
923
    if ret == 206:
924
        if len(ranges) == 1:
925
            offset, length = ranges[0]
926
            response[
927
                'Content-Length'] = length  # Update with the correct length.
928
            response['Content-Range'] = 'bytes %d-%d/%d' % (
929
                offset, offset + length - 1, size)
930
        else:
931
            del(response['Content-Length'])
932
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
933
                boundary,)
934
    return response
935

    
936

    
937
def put_object_block(request, hashmap, data, offset):
938
    """Put one block of data at the given offset."""
939

    
940
    bi = int(offset / request.backend.block_size)
941
    bo = offset % request.backend.block_size
942
    bl = min(len(data), request.backend.block_size - bo)
943
    if bi < len(hashmap):
944
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
945
    else:
946
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
947
    return bl  # Return ammount of data written.
948

    
949

    
950
def hashmap_md5(backend, hashmap, size):
951
    """Produce the MD5 sum from the data in the hashmap."""
952

    
953
    # TODO: Search backend for the MD5 of another object
954
    #       with the same hashmap and size...
955
    md5 = hashlib.md5()
956
    bs = backend.block_size
957
    for bi, hash in enumerate(hashmap):
958
        data = backend.get_block(hash)  # Blocks come in padded.
959
        if bi == len(hashmap) - 1:
960
            data = data[:size % bs]
961
        md5.update(data)
962
    return md5.hexdigest().lower()
963

    
964

    
965
def simple_list_response(request, l):
966
    if request.serialization == 'text':
967
        return '\n'.join(l) + '\n'
968
    if request.serialization == 'xml':
969
        return render_to_string('items.xml', {'items': l})
970
    if request.serialization == 'json':
971
        return json.dumps(l)
972

    
973

    
974
from pithos.backends.util import PithosBackendPool
975

    
976
if RADOS_STORAGE:
977
    BLOCK_PARAMS = {'mappool': RADOS_POOL_MAPS,
978
                    'blockpool': RADOS_POOL_BLOCKS, }
979
else:
980
    BLOCK_PARAMS = {'mappool': None,
981
                    'blockpool': None, }
982

    
983

    
984
_pithos_backend_pool = PithosBackendPool(
985
        size=BACKEND_POOL_SIZE,
986
        db_module=BACKEND_DB_MODULE,
987
        db_connection=BACKEND_DB_CONNECTION,
988
        block_module=BACKEND_BLOCK_MODULE,
989
        block_path=BACKEND_BLOCK_PATH,
990
        block_umask=BACKEND_BLOCK_UMASK,
991
        queue_module=BACKEND_QUEUE_MODULE,
992
        queue_hosts=BACKEND_QUEUE_HOSTS,
993
        queue_exchange=BACKEND_QUEUE_EXCHANGE,
994
        astakos_url=ASTAKOS_URL,
995
        service_token=SERVICE_TOKEN,
996
        astakosclient_poolsize=ASTAKOSCLIENT_POOLSIZE,
997
        free_versioning=BACKEND_FREE_VERSIONING,
998
        block_params=BLOCK_PARAMS,
999
        public_url_security=PUBLIC_URL_SECURITY,
1000
        public_url_alphabet=PUBLIC_URL_ALPHABET,
1001
        account_quota_policy=BACKEND_ACCOUNT_QUOTA,
1002
        container_quota_policy=BACKEND_CONTAINER_QUOTA,
1003
        container_versioning_policy=BACKEND_VERSIONING)
1004

    
1005

    
1006
def get_backend():
1007
    backend = _pithos_backend_pool.pool_get()
1008
    backend.messages = []
1009
    return backend
1010

    
1011

    
1012
def update_request_headers(request):
1013
    # Handle URL-encoded keys and values.
1014
    meta = dict([(
1015
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
1016
    for k, v in meta.iteritems():
1017
        try:
1018
            k.decode('ascii')
1019
            v.decode('ascii')
1020
        except UnicodeDecodeError:
1021
            raise faults.BadRequest('Bad character in headers.')
1022
        if '%' in k or '%' in v:
1023
            del(request.META[k])
1024
            request.META[unquote(k)] = smart_unicode(unquote(
1025
                v), strings_only=True)
1026

    
1027

    
1028
def update_response_headers(request, response):
1029
    if (not response.has_header('Content-Length') and
1030
        not (response.has_header('Content-Type') and
1031
             response['Content-Type'].startswith('multipart/byteranges'))):
1032
        response['Content-Length'] = len(response.content)
1033

    
1034
    # URL-encode unicode in headers.
1035
    meta = response.items()
1036
    for k, v in meta:
1037
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1038
                k.startswith('X-Object-') or k.startswith('Content-')):
1039
            del(response[k])
1040
            response[quote(k)] = quote(v, safe='/=,:@; ')
1041

    
1042

    
1043
def get_pithos_usage(token):
1044
    """Get Pithos Usage from astakos."""
1045
    astakos = AstakosClient(ASTAKOS_URL, retry=2, use_pool=True, logger=logger)
1046
    quotas = astakos.get_quotas(token)['system']
1047
    pithos_resources = [r['name'] for r in resources]
1048
    map(quotas.pop, filter(lambda k: k not in pithos_resources, quotas.keys()))
1049
    return quotas.popitem()[-1] # assume only one resource
1050

    
1051

    
1052
def api_method(http_method=None, user_required=True, logger=None,
1053
               format_allowed=False):
1054
    def decorator(func):
1055
        @api.api_method(http_method=http_method, user_required=user_required,
1056
                        logger=logger, format_allowed=format_allowed,
1057
                        astakos_url=ASTAKOS_URL)
1058
        @wraps(func)
1059
        def wrapper(request, *args, **kwargs):
1060
            # The args variable may contain up to (account, container, object).
1061
            if len(args) > 1 and len(args[1]) > 256:
1062
                raise faults.BadRequest("Container name too large")
1063
            if len(args) > 2 and len(args[2]) > 1024:
1064
                raise faults.BadRequest('Object name too large.')
1065

    
1066
            try:
1067
                # Add a PithosBackend as attribute of the request object
1068
                request.backend = get_backend()
1069
                # Many API method expect thet X-Auth-Token in request,token
1070
                request.token = request.x_auth_token
1071
                update_request_headers(request)
1072
                response = func(request, *args, **kwargs)
1073
                update_response_headers(request, response)
1074
                return response
1075
            finally:
1076
                # Always close PithosBackend connection
1077
                if getattr(request, "backend", None) is not None:
1078
                    request.backend.close()
1079
        return wrapper
1080
    return decorator