Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (41.6 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, HttpResponseRedirect, Http404,
39
                         HttpResponseForbidden)
40
from django.template.loader import render_to_string
41
from django.utils import simplejson as json
42
from django.utils.http import http_date, parse_etags
43
from django.utils.encoding import smart_unicode, smart_str
44
from django.core.files.uploadhandler import FileUploadHandler
45
from django.core.files.uploadedfile import UploadedFile
46
from django.core.urlresolvers import reverse
47

    
48
from snf_django.lib.api.parsedate import parse_http_date_safe, parse_http_date
49
from snf_django.lib import api
50
from snf_django.lib.api import faults, utils
51

    
52
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
53
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
54
                                 BACKEND_BLOCK_UMASK,
55
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
56
                                 BACKEND_QUEUE_EXCHANGE,
57
                                 ASTAKOSCLIENT_POOLSIZE,
58
                                 SERVICE_TOKEN,
59
                                 ASTAKOS_AUTH_URL,
60
                                 BACKEND_ACCOUNT_QUOTA,
61
                                 BACKEND_CONTAINER_QUOTA,
62
                                 BACKEND_VERSIONING, BACKEND_FREE_VERSIONING,
63
                                 BACKEND_POOL_ENABLED, BACKEND_POOL_SIZE,
64
                                 BACKEND_BLOCK_SIZE, BACKEND_HASH_ALGORITHM,
65
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
66
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
67
                                 PUBLIC_URL_SECURITY, PUBLIC_URL_ALPHABET,
68
                                 COOKIE_NAME, BASE_HOST, UPDATE_MD5, LOGIN_URL)
69

    
70
from pithos.api.resources import resources
71
from pithos.backends import connect_backend
72
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
73
                                  VersionNotExists)
74

    
75
from synnefo.lib import join_urls
76

    
77
from astakosclient import AstakosClient
78
from astakosclient.errors import NoUserName, NoUUID
79

    
80
import logging
81
import re
82
import hashlib
83
import uuid
84
import decimal
85

    
86
logger = logging.getLogger(__name__)
87

    
88

    
89
def json_encode_decimal(obj):
90
    if isinstance(obj, decimal.Decimal):
91
        return str(obj)
92
    raise TypeError(repr(obj) + " is not JSON serializable")
93

    
94

    
95
def rename_meta_key(d, old, new):
96
    if old not in d:
97
        return
98
    d[new] = d[old]
99
    del(d[old])
100

    
101

    
102
def printable_header_dict(d):
103
    """Format a meta dictionary for printing out json/xml.
104

105
    Convert all keys to lower case and replace dashes with underscores.
106
    Format 'last_modified' timestamp.
107
    """
108

    
109
    timestamps = ('last_modified', 'x_container_until_timestamp',
110
                  'x_acount_until_timestamp')
111
    for timestamp in timestamps:
112
        if timestamp in d and d[timestamp]:
113
            d[timestamp] = utils.isoformat(
114
                datetime.fromtimestamp(d[timestamp]))
115
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
116

    
117

    
118
def format_header_key(k):
119
    """Convert underscores to dashes and capitalize intra-dash strings."""
120
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
121

    
122

    
123
def get_header_prefix(request, prefix):
124
    """Get all prefix-* request headers in a dict.
125
       Reformat keys with format_header_key()."""
126

    
127
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
128
    # TODO: Document or remove '~' replacing.
129
    return dict([(format_header_key(k[5:]), v.replace('~', ''))
130
                for k, v in request.META.iteritems()
131
                if k.startswith(prefix) and len(k) > len(prefix)])
132

    
133

    
134
def check_meta_headers(meta):
135
    if len(meta) > 90:
136
        raise faults.BadRequest('Too many headers.')
137
    for k, v in meta.iteritems():
138
        if len(k) > 128:
139
            raise faults.BadRequest('Header name too large.')
140
        if len(v) > 256:
141
            raise faults.BadRequest('Header value too large.')
142

    
143

    
144
def get_account_headers(request):
145
    meta = get_header_prefix(request, 'X-Account-Meta-')
146
    check_meta_headers(meta)
147
    groups = {}
148
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
149
        n = k[16:].lower()
150
        if '-' in n or '_' in n:
151
            raise faults.BadRequest('Bad characters in group name')
152
        groups[n] = v.replace(' ', '').split(',')
153
        while '' in groups[n]:
154
            groups[n].remove('')
155
    return meta, groups
156

    
157

    
158
def put_account_headers(response, meta, groups, policy):
159
    if 'count' in meta:
160
        response['X-Account-Container-Count'] = meta['count']
161
    if 'bytes' in meta:
162
        response['X-Account-Bytes-Used'] = meta['bytes']
163
    response['Last-Modified'] = http_date(int(meta['modified']))
164
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
165
        response[smart_str(
166
            k, strings_only=True)] = smart_str(meta[k], strings_only=True)
167
    if 'until_timestamp' in meta:
168
        response['X-Account-Until-Timestamp'] = http_date(
169
            int(meta['until_timestamp']))
170
    for k, v in groups.iteritems():
171
        k = smart_str(k, strings_only=True)
172
        k = format_header_key('X-Account-Group-' + k)
173
        v = smart_str(','.join(v), strings_only=True)
174
        response[k] = v
175
    for k, v in policy.iteritems():
176
        response[smart_str(format_header_key('X-Account-Policy-' + k),
177
                 strings_only=True)] = smart_str(v, strings_only=True)
178

    
179

    
180
def get_container_headers(request):
181
    meta = get_header_prefix(request, 'X-Container-Meta-')
182
    check_meta_headers(meta)
183
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in
184
                  get_header_prefix(request,
185
                                    'X-Container-Policy-').iteritems()])
186
    return meta, policy
187

    
188

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

    
211

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

    
224

    
225
def put_object_headers(response, meta, restricted=False, token=None):
226
    response['ETag'] = meta['hash'] if not UPDATE_MD5 else meta['checksum']
227
    response['Content-Length'] = meta['bytes']
228
    response.override_serialization = True
229
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
230
    response['Last-Modified'] = http_date(int(meta['modified']))
231
    if not restricted:
232
        response['X-Object-Hash'] = meta['hash']
233
        response['X-Object-UUID'] = meta['uuid']
234
        if TRANSLATE_UUIDS:
235
            meta['modified_by'] = \
236
                retrieve_displayname(token, meta['modified_by'])
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(
271
                    request.user_uniq, v_account, src_container, x[0],
272
                    'pithos', x[1])
273
                etag += (src_meta['hash'] if not UPDATE_MD5 else
274
                         src_meta['checksum'])
275
                bytes += src_meta['bytes']
276
        except:
277
            # Ignore errors.
278
            return
279
        meta['bytes'] = bytes
280
        md5 = hashlib.md5()
281
        md5.update(etag)
282
        meta['checksum'] = md5.hexdigest().lower()
283

    
284

    
285
def is_uuid(str):
286
    if str is None:
287
        return False
288
    try:
289
        uuid.UUID(str)
290
    except ValueError:
291
        return False
292
    else:
293
        return True
294

    
295

    
296
##########################
297
# USER CATALOG utilities #
298
##########################
299

    
300
def retrieve_displayname(token, uuid, fail_silently=True):
301
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
302
                            retry=2, use_pool=True,
303
                            logger=logger)
304
    try:
305
        displayname = astakos.get_username(uuid)
306
    except NoUserName:
307
        if not fail_silently:
308
            raise ItemNotExists(uuid)
309
        else:
310
            # just return the uuid
311
            return uuid
312
    return displayname
313

    
314

    
315
def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True):
316
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
317
                            retry=2, use_pool=True,
318
                            logger=logger)
319
    catalog = astakos.get_usernames(uuids) or {}
320
    missing = list(set(uuids) - set(catalog))
321
    if missing and not fail_silently:
322
        raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing))
323
    return catalog if return_dict else [catalog.get(i) for i in uuids]
324

    
325

    
326
def retrieve_uuid(token, displayname):
327
    if is_uuid(displayname):
328
        return displayname
329

    
330
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
331
                            retry=2, use_pool=True,
332
                            logger=logger)
333
    try:
334
        uuid = astakos.get_uuid(displayname)
335
    except NoUUID:
336
        raise ItemNotExists(displayname)
337
    return uuid
338

    
339

    
340
def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True):
341
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
342
                            retry=2, use_pool=True,
343
                            logger=logger)
344
    catalog = astakos.get_uuids(displaynames) or {}
345
    missing = list(set(displaynames) - set(catalog))
346
    if missing and not fail_silently:
347
        raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing))
348
    return catalog if return_dict else [catalog.get(i) for i in displaynames]
349

    
350

    
351
def replace_permissions_displayname(token, holder):
352
    if holder == '*':
353
        return holder
354
    try:
355
        # check first for a group permission
356
        account, group = holder.split(':', 1)
357
    except ValueError:
358
        return retrieve_uuid(token, holder)
359
    else:
360
        return ':'.join([retrieve_uuid(token, account), group])
361

    
362

    
363
def replace_permissions_uuid(token, holder):
364
    if holder == '*':
365
        return holder
366
    try:
367
        # check first for a group permission
368
        account, group = holder.split(':', 1)
369
    except ValueError:
370
        return retrieve_displayname(token, holder)
371
    else:
372
        return ':'.join([retrieve_displayname(token, account), group])
373

    
374

    
375
def update_sharing_meta(request, permissions, v_account,
376
                        v_container, v_object, meta):
377
    if permissions is None:
378
        return
379
    allowed, perm_path, perms = permissions
380
    if len(perms) == 0:
381
        return
382

    
383
    # replace uuid with displayname
384
    if TRANSLATE_UUIDS:
385
        perms['read'] = [replace_permissions_uuid(
386
            getattr(request, 'token', None), x)
387
            for x in perms.get('read', [])]
388
        perms['write'] = [replace_permissions_uuid(
389
            getattr(request, 'token', None), x)
390
            for x in perms.get('write', [])]
391

    
392
    ret = []
393

    
394
    r = ','.join(perms.get('read', []))
395
    if r:
396
        ret.append('read=' + r)
397
    w = ','.join(perms.get('write', []))
398
    if w:
399
        ret.append('write=' + w)
400
    meta['X-Object-Sharing'] = '; '.join(ret)
401
    if '/'.join((v_account, v_container, v_object)) != perm_path:
402
        meta['X-Object-Shared-By'] = perm_path
403
    if request.user_uniq != v_account:
404
        meta['X-Object-Allowed-To'] = allowed
405

    
406

    
407
def update_public_meta(public, meta):
408
    if not public:
409
        return
410
    meta['X-Object-Public'] = join_urls(
411
        BASE_HOST, reverse('pithos.api.public.public_demux', args=(public,)))
412

    
413

    
414
def validate_modification_preconditions(request, meta):
415
    """Check the modified timestamp conforms with the preconditions set."""
416

    
417
    if 'modified' not in meta:
418
        return  # TODO: Always return?
419

    
420
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
421
    if if_modified_since is not None:
422
        if_modified_since = parse_http_date_safe(if_modified_since)
423
    if (if_modified_since is not None
424
            and int(meta['modified']) <= if_modified_since):
425
        raise faults.NotModified('Resource has not been modified')
426

    
427
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
428
    if if_unmodified_since is not None:
429
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
430
    if (if_unmodified_since is not None
431
            and int(meta['modified']) > if_unmodified_since):
432
        raise faults.PreconditionFailed('Resource has been modified')
433

    
434

    
435
def validate_matching_preconditions(request, meta):
436
    """Check that the ETag conforms with the preconditions set."""
437

    
438
    etag = meta['hash'] if not UPDATE_MD5 else meta['checksum']
439
    if not etag:
440
        etag = None
441

    
442
    if_match = request.META.get('HTTP_IF_MATCH')
443
    if if_match is not None:
444
        if etag is None:
445
            raise faults.PreconditionFailed('Resource does not exist')
446
        if (if_match != '*'
447
                and etag not in [x.lower() for x in parse_etags(if_match)]):
448
            raise faults.PreconditionFailed('Resource ETag does not match')
449

    
450
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
451
    if if_none_match is not None:
452
        # TODO: If this passes, must ignore If-Modified-Since header.
453
        if etag is not None:
454
            if (if_none_match == '*' or etag in [x.lower() for x in
455
                                                 parse_etags(if_none_match)]):
456
                # TODO: Continue if an If-Modified-Since header is present.
457
                if request.method in ('HEAD', 'GET'):
458
                    raise faults.NotModified('Resource ETag matches')
459
                raise faults.PreconditionFailed(
460
                    'Resource exists or ETag matches')
461

    
462

    
463
def split_container_object_string(s):
464
    if not len(s) > 0 or s[0] != '/':
465
        raise ValueError
466
    s = s[1:]
467
    pos = s.find('/')
468
    if pos == -1 or pos == len(s) - 1:
469
        raise ValueError
470
    return s[:pos], s[(pos + 1):]
471

    
472

    
473
def copy_or_move_object(request, src_account, src_container, src_name,
474
                        dest_account, dest_container, dest_name,
475
                        move=False, delimiter=None):
476
    """Copy or move an object."""
477

    
478
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
479
        del(request.META['CONTENT_TYPE'])
480
    content_type, meta, permissions, public = get_object_headers(request)
481
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
482
    try:
483
        if move:
484
            version_id = request.backend.move_object(
485
                request.user_uniq, src_account, src_container, src_name,
486
                dest_account, dest_container, dest_name,
487
                content_type, 'pithos', meta, False, permissions, delimiter)
488
        else:
489
            version_id = request.backend.copy_object(
490
                request.user_uniq, src_account, src_container, src_name,
491
                dest_account, dest_container, dest_name,
492
                content_type, 'pithos', meta, False, permissions,
493
                src_version, delimiter)
494
    except NotAllowedError:
495
        raise faults.Forbidden('Not allowed')
496
    except (ItemNotExists, VersionNotExists):
497
        raise faults.ItemNotFound('Container or object does not exist')
498
    except ValueError:
499
        raise faults.BadRequest('Invalid sharing header')
500
    except QuotaError, e:
501
        raise faults.RequestEntityTooLarge('Quota error: %s' % e)
502
    if public is not None:
503
        try:
504
            request.backend.update_object_public(
505
                request.user_uniq, dest_account,
506
                dest_container, dest_name, public)
507
        except NotAllowedError:
508
            raise faults.Forbidden('Not allowed')
509
        except ItemNotExists:
510
            raise faults.ItemNotFound('Object does not exist')
511
    return version_id
512

    
513

    
514
def get_int_parameter(p):
515
    if p is not None:
516
        try:
517
            p = int(p)
518
        except ValueError:
519
            return None
520
        if p < 0:
521
            return None
522
    return p
523

    
524

    
525
def get_content_length(request):
526
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
527
    if content_length is None:
528
        raise faults.LengthRequired('Missing or invalid Content-Length header')
529
    return content_length
530

    
531

    
532
def get_range(request, size):
533
    """Parse a Range header from the request.
534

535
    Either returns None, when the header is not existent or should be ignored,
536
    or a list of (offset, length) tuples - should be further checked.
537
    """
538

    
539
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
540
    if not ranges.startswith('bytes='):
541
        return None
542

    
543
    ret = []
544
    for r in (x.strip() for x in ranges[6:].split(',')):
545
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
546
        m = p.match(r)
547
        if not m:
548
            return None
549
        offset = m.group('offset')
550
        upto = m.group('upto')
551
        if offset == '' and upto == '':
552
            return None
553

    
554
        if offset != '':
555
            offset = int(offset)
556
            if upto != '':
557
                upto = int(upto)
558
                if offset > upto:
559
                    return None
560
                ret.append((offset, upto - offset + 1))
561
            else:
562
                ret.append((offset, size - offset))
563
        else:
564
            length = int(upto)
565
            ret.append((size - length, length))
566

    
567
    return ret
568

    
569

    
570
def get_content_range(request):
571
    """Parse a Content-Range header from the request.
572

573
    Either returns None, when the header is not existent or should be ignored,
574
    or an (offset, length, total) tuple - check as length, total may be None.
575
    Returns (None, None, None) if the provided range is '*/*'.
576
    """
577

    
578
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
579
    if not ranges:
580
        return None
581

    
582
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
583
    m = p.match(ranges)
584
    if not m:
585
        if ranges == 'bytes */*':
586
            return (None, None, None)
587
        return None
588
    offset = int(m.group('offset'))
589
    upto = m.group('upto')
590
    total = m.group('total')
591
    if upto != '':
592
        upto = int(upto)
593
    else:
594
        upto = None
595
    if total != '*':
596
        total = int(total)
597
    else:
598
        total = None
599
    if (upto is not None and offset > upto) or \
600
        (total is not None and offset >= total) or \
601
            (total is not None and upto is not None and upto >= total):
602
        return None
603

    
604
    if upto is None:
605
        length = None
606
    else:
607
        length = upto - offset + 1
608
    return (offset, length, total)
609

    
610

    
611
def get_sharing(request):
612
    """Parse an X-Object-Sharing header from the request.
613

614
    Raises BadRequest on error.
615
    """
616

    
617
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
618
    if permissions is None:
619
        return None
620

    
621
    # TODO: Document or remove '~' replacing.
622
    permissions = permissions.replace('~', '')
623

    
624
    ret = {}
625
    permissions = permissions.replace(' ', '')
626
    if permissions == '':
627
        return ret
628
    for perm in (x for x in permissions.split(';')):
629
        if perm.startswith('read='):
630
            ret['read'] = list(set(
631
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
632
            if '' in ret['read']:
633
                ret['read'].remove('')
634
            if '*' in ret['read']:
635
                ret['read'] = ['*']
636
            if len(ret['read']) == 0:
637
                raise faults.BadRequest(
638
                    'Bad X-Object-Sharing header value: invalid length')
639
        elif perm.startswith('write='):
640
            ret['write'] = list(set(
641
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
642
            if '' in ret['write']:
643
                ret['write'].remove('')
644
            if '*' in ret['write']:
645
                ret['write'] = ['*']
646
            if len(ret['write']) == 0:
647
                raise faults.BadRequest(
648
                    'Bad X-Object-Sharing header value: invalid length')
649
        else:
650
            raise faults.BadRequest(
651
                'Bad X-Object-Sharing header value: missing prefix')
652

    
653
    # replace displayname with uuid
654
    if TRANSLATE_UUIDS:
655
        try:
656
            ret['read'] = [replace_permissions_displayname(
657
                getattr(request, 'token', None), x)
658
                for x in ret.get('read', [])]
659
            ret['write'] = [replace_permissions_displayname(
660
                getattr(request, 'token', None), x)
661
                for x in ret.get('write', [])]
662
        except ItemNotExists, e:
663
            raise faults.BadRequest(
664
                'Bad X-Object-Sharing header value: unknown account: %s' % e)
665

    
666
    # Keep duplicates only in write list.
667
    dups = [x for x in ret.get(
668
        'read', []) if x in ret.get('write', []) and x != '*']
669
    if dups:
670
        for x in dups:
671
            ret['read'].remove(x)
672
        if len(ret['read']) == 0:
673
            del(ret['read'])
674

    
675
    return ret
676

    
677

    
678
def get_public(request):
679
    """Parse an X-Object-Public header from the request.
680

681
    Raises BadRequest on error.
682
    """
683

    
684
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
685
    if public is None:
686
        return None
687

    
688
    public = public.replace(' ', '').lower()
689
    if public == 'true':
690
        return True
691
    elif public == 'false' or public == '':
692
        return False
693
    raise faults.BadRequest('Bad X-Object-Public header value')
694

    
695

    
696
def raw_input_socket(request):
697
    """Return the socket for reading the rest of the request."""
698

    
699
    server_software = request.META.get('SERVER_SOFTWARE')
700
    if server_software and server_software.startswith('mod_python'):
701
        return request._req
702
    if 'wsgi.input' in request.environ:
703
        return request.environ['wsgi.input']
704
    raise NotImplemented('Unknown server software')
705

    
706
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
707

    
708

    
709
def socket_read_iterator(request, length=0, blocksize=4096):
710
    """Return maximum of blocksize data read from the socket in each iteration
711

712
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
713
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
714
    """
715

    
716
    sock = raw_input_socket(request)
717
    if length < 0:  # Chunked transfers
718
        # Small version (server does the dechunking).
719
        if (request.environ.get('mod_wsgi.input_chunked', None)
720
                or request.META['SERVER_SOFTWARE'].startswith('gunicorn')):
721
            while length < MAX_UPLOAD_SIZE:
722
                data = sock.read(blocksize)
723
                if data == '':
724
                    return
725
                yield data
726
            raise faults.BadRequest('Maximum size is reached')
727

    
728
        # Long version (do the dechunking).
729
        data = ''
730
        while length < MAX_UPLOAD_SIZE:
731
            # Get chunk size.
732
            if hasattr(sock, 'readline'):
733
                chunk_length = sock.readline()
734
            else:
735
                chunk_length = ''
736
                while chunk_length[-1:] != '\n':
737
                    chunk_length += sock.read(1)
738
                chunk_length.strip()
739
            pos = chunk_length.find(';')
740
            if pos >= 0:
741
                chunk_length = chunk_length[:pos]
742
            try:
743
                chunk_length = int(chunk_length, 16)
744
            except Exception:
745
                raise faults.BadRequest('Bad chunk size')
746
                                 # TODO: Change to something more appropriate.
747
            # Check if done.
748
            if chunk_length == 0:
749
                if len(data) > 0:
750
                    yield data
751
                return
752
            # Get the actual data.
753
            while chunk_length > 0:
754
                chunk = sock.read(min(chunk_length, blocksize))
755
                chunk_length -= len(chunk)
756
                if length > 0:
757
                    length += len(chunk)
758
                data += chunk
759
                if len(data) >= blocksize:
760
                    ret = data[:blocksize]
761
                    data = data[blocksize:]
762
                    yield ret
763
            sock.read(2)  # CRLF
764
        raise faults.BadRequest('Maximum size is reached')
765
    else:
766
        if length > MAX_UPLOAD_SIZE:
767
            raise faults.BadRequest('Maximum size is reached')
768
        while length > 0:
769
            data = sock.read(min(length, blocksize))
770
            if not data:
771
                raise faults.BadRequest()
772
            length -= len(data)
773
            yield data
774

    
775

    
776
class SaveToBackendHandler(FileUploadHandler):
777
    """Handle a file from an HTML form the django way."""
778

    
779
    def __init__(self, request=None):
780
        super(SaveToBackendHandler, self).__init__(request)
781
        self.backend = request.backend
782

    
783
    def put_data(self, length):
784
        if len(self.data) >= length:
785
            block = self.data[:length]
786
            self.file.hashmap.append(self.backend.put_block(block))
787
            self.checksum_compute.update(block)
788
            self.data = self.data[length:]
789

    
790
    def new_file(self, field_name, file_name, content_type,
791
                 content_length, charset=None):
792
        self.checksum_compute = NoChecksum() if not UPDATE_MD5 else Checksum()
793
        self.data = ''
794
        self.file = UploadedFile(
795
            name=file_name, content_type=content_type, charset=charset)
796
        self.file.size = 0
797
        self.file.hashmap = []
798

    
799
    def receive_data_chunk(self, raw_data, start):
800
        self.data += raw_data
801
        self.file.size += len(raw_data)
802
        self.put_data(self.request.backend.block_size)
803
        return None
804

    
805
    def file_complete(self, file_size):
806
        l = len(self.data)
807
        if l > 0:
808
            self.put_data(l)
809
        self.file.etag = self.checksum_compute.hexdigest()
810
        return self.file
811

    
812

    
813
class ObjectWrapper(object):
814
    """Return the object's data block-per-block in each iteration.
815

816
    Read from the object using the offset and length provided
817
    in each entry of the range list.
818
    """
819

    
820
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
821
        self.backend = backend
822
        self.ranges = ranges
823
        self.sizes = sizes
824
        self.hashmaps = hashmaps
825
        self.boundary = boundary
826
        self.size = sum(self.sizes)
827

    
828
        self.file_index = 0
829
        self.block_index = 0
830
        self.block_hash = -1
831
        self.block = ''
832

    
833
        self.range_index = -1
834
        self.offset, self.length = self.ranges[0]
835

    
836
    def __iter__(self):
837
        return self
838

    
839
    def part_iterator(self):
840
        if self.length > 0:
841
            # Get the file for the current offset.
842
            file_size = self.sizes[self.file_index]
843
            while self.offset >= file_size:
844
                self.offset -= file_size
845
                self.file_index += 1
846
                file_size = self.sizes[self.file_index]
847

    
848
            # Get the block for the current position.
849
            self.block_index = int(self.offset / self.backend.block_size)
850
            if self.block_hash != \
851
                    self.hashmaps[self.file_index][self.block_index]:
852
                self.block_hash = self.hashmaps[
853
                    self.file_index][self.block_index]
854
                try:
855
                    self.block = self.backend.get_block(self.block_hash)
856
                except ItemNotExists:
857
                    raise faults.ItemNotFound('Block does not exist')
858

    
859
            # Get the data from the block.
860
            bo = self.offset % self.backend.block_size
861
            bs = self.backend.block_size
862
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
863
                    self.sizes[self.file_index] % self.backend.block_size):
864
                bs = self.sizes[self.file_index] % self.backend.block_size
865
            bl = min(self.length, bs - bo)
866
            data = self.block[bo:bo + bl]
867
            self.offset += bl
868
            self.length -= bl
869
            return data
870
        else:
871
            raise StopIteration
872

    
873
    def next(self):
874
        if len(self.ranges) == 1:
875
            return self.part_iterator()
876
        if self.range_index == len(self.ranges):
877
            raise StopIteration
878
        try:
879
            if self.range_index == -1:
880
                raise StopIteration
881
            return self.part_iterator()
882
        except StopIteration:
883
            self.range_index += 1
884
            out = []
885
            if self.range_index < len(self.ranges):
886
                # Part header.
887
                self.offset, self.length = self.ranges[self.range_index]
888
                self.file_index = 0
889
                if self.range_index > 0:
890
                    out.append('')
891
                out.append('--' + self.boundary)
892
                out.append('Content-Range: bytes %d-%d/%d' % (
893
                    self.offset, self.offset + self.length - 1, self.size))
894
                out.append('Content-Transfer-Encoding: binary')
895
                out.append('')
896
                out.append('')
897
                return '\r\n'.join(out)
898
            else:
899
                # Footer.
900
                out.append('')
901
                out.append('--' + self.boundary + '--')
902
                out.append('')
903
                return '\r\n'.join(out)
904

    
905

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

    
909
    # Range handling.
910
    size = sum(sizes)
911
    ranges = get_range(request, size)
912
    if ranges is None:
913
        ranges = [(0, size)]
914
        ret = 200
915
    else:
916
        check = [True for offset, length in ranges if
917
                 length <= 0 or length > size or
918
                 offset < 0 or offset >= size or
919
                 offset + length > size]
920
        if len(check) > 0:
921
            raise faults.RangeNotSatisfiable(
922
                'Requested range exceeds object limits')
923
        ret = 206
924
        if_range = request.META.get('HTTP_IF_RANGE')
925
        if if_range:
926
            try:
927
                # Modification time has passed instead.
928
                last_modified = parse_http_date(if_range)
929
                if last_modified != meta['modified']:
930
                    ranges = [(0, size)]
931
                    ret = 200
932
            except ValueError:
933
                if if_range != meta['checksum']:
934
                    ranges = [(0, size)]
935
                    ret = 200
936

    
937
    if ret == 206 and len(ranges) > 1:
938
        boundary = uuid.uuid4().hex
939
    else:
940
        boundary = ''
941
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
942
    response = HttpResponse(wrapper, status=ret)
943
    put_object_headers(
944
        response, meta, restricted=public,
945
        token=getattr(request, 'token', None))
946
    if ret == 206:
947
        if len(ranges) == 1:
948
            offset, length = ranges[0]
949
            response[
950
                'Content-Length'] = length  # Update with the correct length.
951
            response['Content-Range'] = 'bytes %d-%d/%d' % (
952
                offset, offset + length - 1, size)
953
        else:
954
            del(response['Content-Length'])
955
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
956
                boundary,)
957
    return response
958

    
959

    
960
def put_object_block(request, hashmap, data, offset):
961
    """Put one block of data at the given offset."""
962

    
963
    bi = int(offset / request.backend.block_size)
964
    bo = offset % request.backend.block_size
965
    bl = min(len(data), request.backend.block_size - bo)
966
    if bi < len(hashmap):
967
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
968
    else:
969
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
970
    return bl  # Return ammount of data written.
971

    
972

    
973
def hashmap_md5(backend, hashmap, size):
974
    """Produce the MD5 sum from the data in the hashmap."""
975

    
976
    # TODO: Search backend for the MD5 of another object
977
    #       with the same hashmap and size...
978
    md5 = hashlib.md5()
979
    bs = backend.block_size
980
    for bi, hash in enumerate(hashmap):
981
        data = backend.get_block(hash)  # Blocks come in padded.
982
        if bi == len(hashmap) - 1:
983
            data = data[:size % bs]
984
        md5.update(data)
985
    return md5.hexdigest().lower()
986

    
987

    
988
def simple_list_response(request, l):
989
    if request.serialization == 'text':
990
        return '\n'.join(l) + '\n'
991
    if request.serialization == 'xml':
992
        return render_to_string('items.xml', {'items': l})
993
    if request.serialization == 'json':
994
        return json.dumps(l)
995

    
996

    
997
from pithos.backends.util import PithosBackendPool
998

    
999
if RADOS_STORAGE:
1000
    BLOCK_PARAMS = {'mappool': RADOS_POOL_MAPS,
1001
                    'blockpool': RADOS_POOL_BLOCKS, }
1002
else:
1003
    BLOCK_PARAMS = {'mappool': None,
1004
                    'blockpool': None, }
1005

    
1006
BACKEND_KWARGS = dict(
1007
    db_module=BACKEND_DB_MODULE,
1008
    db_connection=BACKEND_DB_CONNECTION,
1009
    block_module=BACKEND_BLOCK_MODULE,
1010
    block_path=BACKEND_BLOCK_PATH,
1011
    block_umask=BACKEND_BLOCK_UMASK,
1012
    block_size=BACKEND_BLOCK_SIZE,
1013
    hash_algorithm=BACKEND_HASH_ALGORITHM,
1014
    queue_module=BACKEND_QUEUE_MODULE,
1015
    queue_hosts=BACKEND_QUEUE_HOSTS,
1016
    queue_exchange=BACKEND_QUEUE_EXCHANGE,
1017
    astakos_auth_url=ASTAKOS_AUTH_URL,
1018
    service_token=SERVICE_TOKEN,
1019
    astakosclient_poolsize=ASTAKOSCLIENT_POOLSIZE,
1020
    free_versioning=BACKEND_FREE_VERSIONING,
1021
    block_params=BLOCK_PARAMS,
1022
    public_url_security=PUBLIC_URL_SECURITY,
1023
    public_url_alphabet=PUBLIC_URL_ALPHABET,
1024
    account_quota_policy=BACKEND_ACCOUNT_QUOTA,
1025
    container_quota_policy=BACKEND_CONTAINER_QUOTA,
1026
    container_versioning_policy=BACKEND_VERSIONING)
1027

    
1028
_pithos_backend_pool = PithosBackendPool(size=BACKEND_POOL_SIZE,
1029
                                         **BACKEND_KWARGS)
1030

    
1031

    
1032
def get_backend():
1033
    if BACKEND_POOL_ENABLED:
1034
        backend = _pithos_backend_pool.pool_get()
1035
    else:
1036
        backend = connect_backend(**BACKEND_KWARGS)
1037
    backend.serials = []
1038
    backend.messages = []
1039
    return backend
1040

    
1041

    
1042
def update_request_headers(request):
1043
    # Handle URL-encoded keys and values.
1044
    meta = dict([(
1045
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
1046
    for k, v in meta.iteritems():
1047
        try:
1048
            k.decode('ascii')
1049
            v.decode('ascii')
1050
        except UnicodeDecodeError:
1051
            raise faults.BadRequest('Bad character in headers.')
1052
        if '%' in k or '%' in v:
1053
            del(request.META[k])
1054
            request.META[unquote(k)] = smart_unicode(unquote(
1055
                v), strings_only=True)
1056

    
1057

    
1058
def update_response_headers(request, response):
1059
    # URL-encode unicode in headers.
1060
    meta = response.items()
1061
    for k, v in meta:
1062
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1063
                k.startswith('X-Object-') or k.startswith('Content-')):
1064
            del(response[k])
1065
            response[quote(k)] = quote(v, safe='/=,:@; ')
1066

    
1067

    
1068
def get_pithos_usage(token):
1069
    """Get Pithos Usage from astakos."""
1070
    astakos = AstakosClient(token, ASTAKOS_AUTH_URL,
1071
                            retry=2, use_pool=True,
1072
                            logger=logger)
1073
    quotas = astakos.get_quotas()['system']
1074
    pithos_resources = [r['name'] for r in resources]
1075
    map(quotas.pop, filter(lambda k: k not in pithos_resources, quotas.keys()))
1076
    return quotas.popitem()[-1]  # assume only one resource
1077

    
1078

    
1079
def api_method(http_method=None, token_required=True, user_required=True,
1080
               logger=None, format_allowed=False, serializations=None,
1081
               strict_serlization=False, lock_container_path=False):
1082
    serializations = serializations or ['json', 'xml']
1083

    
1084
    def decorator(func):
1085
        @api.api_method(http_method=http_method, token_required=token_required,
1086
                        user_required=user_required,
1087
                        logger=logger, format_allowed=format_allowed,
1088
                        astakos_auth_url=ASTAKOS_AUTH_URL,
1089
                        serializations=serializations,
1090
                        strict_serlization=strict_serlization)
1091
        @wraps(func)
1092
        def wrapper(request, *args, **kwargs):
1093
            # The args variable may contain up to (account, container, object).
1094
            if len(args) > 1 and len(args[1]) > 256:
1095
                raise faults.BadRequest("Container name too large")
1096
            if len(args) > 2 and len(args[2]) > 1024:
1097
                raise faults.BadRequest('Object name too large.')
1098

    
1099
            success_status = False
1100
            try:
1101
                # Add a PithosBackend as attribute of the request object
1102
                request.backend = get_backend()
1103
                request.backend.pre_exec(lock_container_path)
1104

    
1105
                # Many API method expect thet X-Auth-Token in request,token
1106
                request.token = request.x_auth_token
1107
                update_request_headers(request)
1108
                response = func(request, *args, **kwargs)
1109
                update_response_headers(request, response)
1110

    
1111
                success_status = True
1112
                return response
1113
            finally:
1114
                # Always close PithosBackend connection
1115
                if getattr(request, "backend", None) is not None:
1116
                    request.backend.post_exec(success_status)
1117
                    request.backend.close()
1118
        return wrapper
1119
    return decorator
1120

    
1121

    
1122
def get_token_from_cookie(request):
1123
    token = None
1124
    if COOKIE_NAME in request.COOKIES:
1125
        cookie_value = unquote(request.COOKIES.get(COOKIE_NAME, ''))
1126
        account, sep, token = cookie_value.partition('|')
1127
    return token
1128

    
1129

    
1130
def view_method():
1131
    """Decorator function for views."""
1132

    
1133
    def decorator(func):
1134
        @wraps(func)
1135
        def wrapper(request, *args, **kwargs):
1136
            token = get_token_from_cookie(request)
1137
            if token is None:
1138
                return HttpResponseRedirect('%s?next=%s' % (
1139
                    LOGIN_URL, join_urls(BASE_HOST, request.path)))
1140
            request.META['HTTP_X_AUTH_TOKEN'] = token
1141
            # Get the response object
1142
            response = func(request, *args, **kwargs)
1143
            if response.status_code == 404:
1144
                raise Http404()
1145
            elif response.status_code in [401, 403]:
1146
                return HttpResponseForbidden()
1147
            return response
1148
        return wrapper
1149
    return decorator
1150

    
1151

    
1152
class Checksum:
1153
    def __init__(self):
1154
        self.md5 = hashlib.md5()
1155

    
1156
    def update(self, data):
1157
        self.md5.update(data)
1158

    
1159
    def hexdigest(self):
1160
        return self.md5.hexdigest().lower()
1161

    
1162

    
1163
class NoChecksum:
1164
    def update(self, data):
1165
        pass
1166

    
1167
    def hexdigest(self):
1168
        return ''