Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (41.1 kB)

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

    
34
from functools import wraps
35
from time import time
36
from traceback import format_exc
37
from wsgiref.handlers import format_date_time
38
from binascii import hexlify, unhexlify
39
from datetime import datetime, tzinfo, timedelta
40
from urllib import quote, unquote
41

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

    
51
from synnefo.lib.parsedate import parse_http_date_safe, parse_http_date
52
from synnefo.lib.astakos import get_user
53
from snf_django.lib.api import faults
54

    
55
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
56
                                 BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
57
                                 BACKEND_BLOCK_UMASK,
58
                                 BACKEND_QUEUE_MODULE, BACKEND_QUEUE_HOSTS,
59
                                 BACKEND_QUEUE_EXCHANGE, USE_QUOTAHOLDER,
60
                                 QUOTAHOLDER_URL, QUOTAHOLDER_TOKEN,
61
                                 QUOTAHOLDER_POOLSIZE,
62
                                 BACKEND_QUOTA, BACKEND_VERSIONING,
63
                                 BACKEND_FREE_VERSIONING,
64
                                 AUTHENTICATION_URL, AUTHENTICATION_USERS,
65
                                 COOKIE_NAME, USER_CATALOG_URL,
66
                                 RADOS_STORAGE, RADOS_POOL_BLOCKS,
67
                                 RADOS_POOL_MAPS, TRANSLATE_UUIDS,
68
                                 PUBLIC_URL_SECURITY,
69
                                 PUBLIC_URL_ALPHABET)
70
from pithos.backends import connect_backend
71
from pithos.backends.base import (NotAllowedError, QuotaError, ItemNotExists,
72
                                  VersionNotExists)
73
from synnefo.lib.astakos import (get_user_uuid, get_displayname,
74
                                 get_uuids, get_displaynames)
75

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

    
82
logger = logging.getLogger(__name__)
83

    
84

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

    
89
    def tzname(self, dt):
90
        return 'UTC'
91

    
92
    def dst(self, dt):
93
        return timedelta(0)
94

    
95

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

    
101

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

    
105
    return d.replace(tzinfo=UTC()).isoformat()
106

    
107

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

    
114

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

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

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

    
127

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

    
132

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

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

    
140

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

    
150

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

    
164

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

    
185

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

    
192

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

    
213

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

    
226

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

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

    
292
##########################
293
# USER CATALOG utilities #
294
##########################
295

    
296
def retrieve_displayname(token, uuid, fail_silently=True):
297
    displayname = get_displayname(
298
            token, uuid, USER_CATALOG_URL, AUTHENTICATION_USERS)
299
    if not displayname and not fail_silently:
300
        raise ItemNotExists(uuid)
301
    elif not displayname:
302
        # just return the uuid
303
        return uuid
304
    return displayname
305

    
306
def retrieve_displaynames(token, uuids, return_dict=False, fail_silently=True):
307
    catalog =  get_displaynames(
308
            token, uuids, USER_CATALOG_URL, AUTHENTICATION_USERS) or {}
309
    missing = list(set(uuids) - set(catalog))
310
    if missing and not fail_silently:
311
        raise ItemNotExists('Unknown displaynames: %s' % ', '.join(missing))
312
    return catalog if return_dict else [catalog.get(i) for i in uuids]
313

    
314
def retrieve_uuid(token, displayname):
315
    if is_uuid(displayname):
316
        return displayname
317

    
318
    uuid = get_user_uuid(
319
        token, displayname, USER_CATALOG_URL, AUTHENTICATION_USERS)
320
    if not uuid:
321
        raise ItemNotExists(displayname)
322
    return uuid
323

    
324
def retrieve_uuids(token, displaynames, return_dict=False, fail_silently=True):
325
    catalog = get_uuids(
326
            token, displaynames, USER_CATALOG_URL, AUTHENTICATION_USERS) or {}
327
    missing = list(set(displaynames) - set(catalog))
328
    if missing and not fail_silently:
329
        raise ItemNotExists('Unknown uuids: %s' % ', '.join(missing))
330
    return catalog if return_dict else [catalog.get(i) for i in displaynames]
331

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

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

    
354
def update_sharing_meta(request, permissions, v_account, 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 and int(meta['modified']) <= if_modified_since:
401
        raise faults.NotModified('Resource has not been modified')
402

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

    
409

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

    
413
    etag = meta['checksum']
414
    if not etag:
415
        etag = None
416

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

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

    
434

    
435
def split_container_object_string(s):
436
    if not len(s) > 0 or s[0] != '/':
437
        raise ValueError
438
    s = s[1:]
439
    pos = s.find('/')
440
    if pos == -1 or pos == len(s) - 1:
441
        raise ValueError
442
    return s[:pos], s[(pos + 1):]
443

    
444

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

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

    
480

    
481
def get_int_parameter(p):
482
    if p is not None:
483
        try:
484
            p = int(p)
485
        except ValueError:
486
            return None
487
        if p < 0:
488
            return None
489
    return p
490

    
491

    
492
def get_content_length(request):
493
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
494
    if content_length is None:
495
        raise faults.LengthRequired('Missing or invalid Content-Length header')
496
    return content_length
497

    
498

    
499
def get_range(request, size):
500
    """Parse a Range header from the request.
501

502
    Either returns None, when the header is not existent or should be ignored,
503
    or a list of (offset, length) tuples - should be further checked.
504
    """
505

    
506
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
507
    if not ranges.startswith('bytes='):
508
        return None
509

    
510
    ret = []
511
    for r in (x.strip() for x in ranges[6:].split(',')):
512
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
513
        m = p.match(r)
514
        if not m:
515
            return None
516
        offset = m.group('offset')
517
        upto = m.group('upto')
518
        if offset == '' and upto == '':
519
            return None
520

    
521
        if offset != '':
522
            offset = int(offset)
523
            if upto != '':
524
                upto = int(upto)
525
                if offset > upto:
526
                    return None
527
                ret.append((offset, upto - offset + 1))
528
            else:
529
                ret.append((offset, size - offset))
530
        else:
531
            length = int(upto)
532
            ret.append((size - length, length))
533

    
534
    return ret
535

    
536

    
537
def get_content_range(request):
538
    """Parse a Content-Range header from the request.
539

540
    Either returns None, when the header is not existent or should be ignored,
541
    or an (offset, length, total) tuple - check as length, total may be None.
542
    Returns (None, None, None) if the provided range is '*/*'.
543
    """
544

    
545
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
546
    if not ranges:
547
        return None
548

    
549
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
550
    m = p.match(ranges)
551
    if not m:
552
        if ranges == 'bytes */*':
553
            return (None, None, None)
554
        return None
555
    offset = int(m.group('offset'))
556
    upto = m.group('upto')
557
    total = m.group('total')
558
    if upto != '':
559
        upto = int(upto)
560
    else:
561
        upto = None
562
    if total != '*':
563
        total = int(total)
564
    else:
565
        total = None
566
    if (upto is not None and offset > upto) or \
567
        (total is not None and offset >= total) or \
568
            (total is not None and upto is not None and upto >= total):
569
        return None
570

    
571
    if upto is None:
572
        length = None
573
    else:
574
        length = upto - offset + 1
575
    return (offset, length, total)
576

    
577

    
578
def get_sharing(request):
579
    """Parse an X-Object-Sharing header from the request.
580

581
    Raises BadRequest on error.
582
    """
583

    
584
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
585
    if permissions is None:
586
        return None
587

    
588
    # TODO: Document or remove '~' replacing.
589
    permissions = permissions.replace('~', '')
590

    
591
    ret = {}
592
    permissions = permissions.replace(' ', '')
593
    if permissions == '':
594
        return ret
595
    for perm in (x for x in permissions.split(';')):
596
        if perm.startswith('read='):
597
            ret['read'] = list(set(
598
                [v.replace(' ', '').lower() for v in perm[5:].split(',')]))
599
            if '' in ret['read']:
600
                ret['read'].remove('')
601
            if '*' in ret['read']:
602
                ret['read'] = ['*']
603
            if len(ret['read']) == 0:
604
                raise faults.BadRequest(
605
                    'Bad X-Object-Sharing header value: invalid length')
606
        elif perm.startswith('write='):
607
            ret['write'] = list(set(
608
                [v.replace(' ', '').lower() for v in perm[6:].split(',')]))
609
            if '' in ret['write']:
610
                ret['write'].remove('')
611
            if '*' in ret['write']:
612
                ret['write'] = ['*']
613
            if len(ret['write']) == 0:
614
                raise faults.BadRequest(
615
                    'Bad X-Object-Sharing header value: invalid length')
616
        else:
617
            raise faults.BadRequest(
618
                'Bad X-Object-Sharing header value: missing prefix')
619

    
620
    # replace displayname with uuid
621
    if TRANSLATE_UUIDS:
622
        try:
623
            ret['read'] = [replace_permissions_displayname(
624
                    getattr(request, 'token', None), x) \
625
                        for x in ret.get('read', [])]
626
            ret['write'] = [replace_permissions_displayname(
627
                    getattr(request, 'token', None), x) \
628
                        for x in ret.get('write', [])]
629
        except ItemNotExists, e:
630
            raise faults.BadRequest(
631
                'Bad X-Object-Sharing header value: unknown account: %s' % e)
632

    
633
    # Keep duplicates only in write list.
634
    dups = [x for x in ret.get(
635
        'read', []) if x in ret.get('write', []) and x != '*']
636
    if dups:
637
        for x in dups:
638
            ret['read'].remove(x)
639
        if len(ret['read']) == 0:
640
            del(ret['read'])
641

    
642
    return ret
643

    
644

    
645
def get_public(request):
646
    """Parse an X-Object-Public header from the request.
647

648
    Raises BadRequest on error.
649
    """
650

    
651
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
652
    if public is None:
653
        return None
654

    
655
    public = public.replace(' ', '').lower()
656
    if public == 'true':
657
        return True
658
    elif public == 'false' or public == '':
659
        return False
660
    raise faults.BadRequest('Bad X-Object-Public header value')
661

    
662

    
663
def raw_input_socket(request):
664
    """Return the socket for reading the rest of the request."""
665

    
666
    server_software = request.META.get('SERVER_SOFTWARE')
667
    if server_software and server_software.startswith('mod_python'):
668
        return request._req
669
    if 'wsgi.input' in request.environ:
670
        return request.environ['wsgi.input']
671
    raise NotImplemented('Unknown server software')
672

    
673
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024)  # 5GB
674

    
675

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

679
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
680
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
681
    """
682

    
683
    sock = raw_input_socket(request)
684
    if length < 0:  # Chunked transfers
685
        # Small version (server does the dechunking).
686
        if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
687
            while length < MAX_UPLOAD_SIZE:
688
                data = sock.read(blocksize)
689
                if data == '':
690
                    return
691
                yield data
692
            raise faults.BadRequest('Maximum size is reached')
693

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

    
741

    
742
class SaveToBackendHandler(FileUploadHandler):
743
    """Handle a file from an HTML form the django way."""
744

    
745
    def __init__(self, request=None):
746
        super(SaveToBackendHandler, self).__init__(request)
747
        self.backend = request.backend
748

    
749
    def put_data(self, length):
750
        if len(self.data) >= length:
751
            block = self.data[:length]
752
            self.file.hashmap.append(self.backend.put_block(block))
753
            self.md5.update(block)
754
            self.data = self.data[length:]
755

    
756
    def new_file(self, field_name, file_name, content_type, content_length, charset=None):
757
        self.md5 = hashlib.md5()
758
        self.data = ''
759
        self.file = UploadedFile(
760
            name=file_name, content_type=content_type, charset=charset)
761
        self.file.size = 0
762
        self.file.hashmap = []
763

    
764
    def receive_data_chunk(self, raw_data, start):
765
        self.data += raw_data
766
        self.file.size += len(raw_data)
767
        self.put_data(self.request.backend.block_size)
768
        return None
769

    
770
    def file_complete(self, file_size):
771
        l = len(self.data)
772
        if l > 0:
773
            self.put_data(l)
774
        self.file.etag = self.md5.hexdigest().lower()
775
        return self.file
776

    
777

    
778
class ObjectWrapper(object):
779
    """Return the object's data block-per-block in each iteration.
780

781
    Read from the object using the offset and length provided in each entry of the range list.
782
    """
783

    
784
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
785
        self.backend = backend
786
        self.ranges = ranges
787
        self.sizes = sizes
788
        self.hashmaps = hashmaps
789
        self.boundary = boundary
790
        self.size = sum(self.sizes)
791

    
792
        self.file_index = 0
793
        self.block_index = 0
794
        self.block_hash = -1
795
        self.block = ''
796

    
797
        self.range_index = -1
798
        self.offset, self.length = self.ranges[0]
799

    
800
    def __iter__(self):
801
        return self
802

    
803
    def part_iterator(self):
804
        if self.length > 0:
805
            # Get the file for the current offset.
806
            file_size = self.sizes[self.file_index]
807
            while self.offset >= file_size:
808
                self.offset -= file_size
809
                self.file_index += 1
810
                file_size = self.sizes[self.file_index]
811

    
812
            # Get the block for the current position.
813
            self.block_index = int(self.offset / self.backend.block_size)
814
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
815
                self.block_hash = self.hashmaps[
816
                    self.file_index][self.block_index]
817
                try:
818
                    self.block = self.backend.get_block(self.block_hash)
819
                except ItemNotExists:
820
                    raise faults.ItemNotFound('Block does not exist')
821

    
822
            # Get the data from the block.
823
            bo = self.offset % self.backend.block_size
824
            bs = self.backend.block_size
825
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
826
                    self.sizes[self.file_index] % self.backend.block_size):
827
                bs = self.sizes[self.file_index] % self.backend.block_size
828
            bl = min(self.length, bs - bo)
829
            data = self.block[bo:bo + bl]
830
            self.offset += bl
831
            self.length -= bl
832
            return data
833
        else:
834
            raise StopIteration
835

    
836
    def next(self):
837
        if len(self.ranges) == 1:
838
            return self.part_iterator()
839
        if self.range_index == len(self.ranges):
840
            raise StopIteration
841
        try:
842
            if self.range_index == -1:
843
                raise StopIteration
844
            return self.part_iterator()
845
        except StopIteration:
846
            self.range_index += 1
847
            out = []
848
            if self.range_index < len(self.ranges):
849
                # Part header.
850
                self.offset, self.length = self.ranges[self.range_index]
851
                self.file_index = 0
852
                if self.range_index > 0:
853
                    out.append('')
854
                out.append('--' + self.boundary)
855
                out.append('Content-Range: bytes %d-%d/%d' % (
856
                    self.offset, self.offset + self.length - 1, self.size))
857
                out.append('Content-Transfer-Encoding: binary')
858
                out.append('')
859
                out.append('')
860
                return '\r\n'.join(out)
861
            else:
862
                # Footer.
863
                out.append('')
864
                out.append('--' + self.boundary + '--')
865
                out.append('')
866
                return '\r\n'.join(out)
867

    
868

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

    
872
    # Range handling.
873
    size = sum(sizes)
874
    ranges = get_range(request, size)
875
    if ranges is None:
876
        ranges = [(0, size)]
877
        ret = 200
878
    else:
879
        check = [True for offset, length in ranges if
880
                 length <= 0 or length > size or
881
                 offset < 0 or offset >= size or
882
                 offset + length > size]
883
        if len(check) > 0:
884
            raise faults.RangeNotSatisfiable('Requested range exceeds object limits')
885
        ret = 206
886
        if_range = request.META.get('HTTP_IF_RANGE')
887
        if if_range:
888
            try:
889
                # Modification time has passed instead.
890
                last_modified = parse_http_date(if_range)
891
                if last_modified != meta['modified']:
892
                    ranges = [(0, size)]
893
                    ret = 200
894
            except ValueError:
895
                if if_range != meta['checksum']:
896
                    ranges = [(0, size)]
897
                    ret = 200
898

    
899
    if ret == 206 and len(ranges) > 1:
900
        boundary = uuid.uuid4().hex
901
    else:
902
        boundary = ''
903
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
904
    response = HttpResponse(wrapper, status=ret)
905
    put_object_headers(
906
            response, meta, restricted=public, token=getattr(request, 'token', None))
907
    if ret == 206:
908
        if len(ranges) == 1:
909
            offset, length = ranges[0]
910
            response[
911
                'Content-Length'] = length  # Update with the correct length.
912
            response['Content-Range'] = 'bytes %d-%d/%d' % (
913
                offset, offset + length - 1, size)
914
        else:
915
            del(response['Content-Length'])
916
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (
917
                boundary,)
918
    return response
919

    
920

    
921
def put_object_block(request, hashmap, data, offset):
922
    """Put one block of data at the given offset."""
923

    
924
    bi = int(offset / request.backend.block_size)
925
    bo = offset % request.backend.block_size
926
    bl = min(len(data), request.backend.block_size - bo)
927
    if bi < len(hashmap):
928
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
929
    else:
930
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
931
    return bl  # Return ammount of data written.
932

    
933

    
934
def hashmap_md5(backend, hashmap, size):
935
    """Produce the MD5 sum from the data in the hashmap."""
936

    
937
    # TODO: Search backend for the MD5 of another object with the same hashmap and size...
938
    md5 = hashlib.md5()
939
    bs = backend.block_size
940
    for bi, hash in enumerate(hashmap):
941
        data = backend.get_block(hash)  # Blocks come in padded.
942
        if bi == len(hashmap) - 1:
943
            data = data[:size % bs]
944
        md5.update(data)
945
    return md5.hexdigest().lower()
946

    
947

    
948
def simple_list_response(request, l):
949
    if request.serialization == 'text':
950
        return '\n'.join(l) + '\n'
951
    if request.serialization == 'xml':
952
        return render_to_string('items.xml', {'items': l})
953
    if request.serialization == 'json':
954
        return json.dumps(l)
955

    
956

    
957
from pithos.backends.util import PithosBackendPool
958
POOL_SIZE = 5
959
if RADOS_STORAGE:
960
    BLOCK_PARAMS = { 'mappool': RADOS_POOL_MAPS,
961
                     'blockpool': RADOS_POOL_BLOCKS,
962
                   }
963
else:
964
    BLOCK_PARAMS = { 'mappool': None,
965
                     'blockpool': None,
966
                   }
967

    
968

    
969
_pithos_backend_pool = PithosBackendPool(
970
        size=POOL_SIZE,
971
        db_module=BACKEND_DB_MODULE,
972
        db_connection=BACKEND_DB_CONNECTION,
973
        block_module=BACKEND_BLOCK_MODULE,
974
        block_path=BACKEND_BLOCK_PATH,
975
        block_umask=BACKEND_BLOCK_UMASK,
976
        queue_module=BACKEND_QUEUE_MODULE,
977
        queue_hosts=BACKEND_QUEUE_HOSTS,
978
        queue_exchange=BACKEND_QUEUE_EXCHANGE,
979
        quotaholder_enabled=USE_QUOTAHOLDER,
980
        quotaholder_url=QUOTAHOLDER_URL,
981
        quotaholder_token=QUOTAHOLDER_TOKEN,
982
        quotaholder_client_poolsize=QUOTAHOLDER_POOLSIZE,
983
        free_versioning=BACKEND_FREE_VERSIONING,
984
        block_params=BLOCK_PARAMS,
985
        public_url_security=PUBLIC_URL_SECURITY,
986
        public_url_alphabet=PUBLIC_URL_ALPHABET)
987

    
988
def get_backend():
989
    backend = _pithos_backend_pool.pool_get()
990
    backend.default_policy['quota'] = BACKEND_QUOTA
991
    backend.default_policy['versioning'] = BACKEND_VERSIONING
992
    backend.messages = []
993
    return backend
994

    
995

    
996
def update_request_headers(request):
997
    # Handle URL-encoded keys and values.
998
    meta = dict([(
999
        k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
1000
    for k, v in meta.iteritems():
1001
        try:
1002
            k.decode('ascii')
1003
            v.decode('ascii')
1004
        except UnicodeDecodeError:
1005
            raise faults.BadRequest('Bad character in headers.')
1006
        if '%' in k or '%' in v:
1007
            del(request.META[k])
1008
            request.META[unquote(k)] = smart_unicode(unquote(
1009
                v), strings_only=True)
1010

    
1011

    
1012
def update_response_headers(request, response):
1013
    if request.serialization == 'xml':
1014
        response['Content-Type'] = 'application/xml; charset=UTF-8'
1015
    elif request.serialization == 'json':
1016
        response['Content-Type'] = 'application/json; charset=UTF-8'
1017
    elif not response['Content-Type']:
1018
        response['Content-Type'] = 'text/plain; charset=UTF-8'
1019

    
1020
    if (not response.has_header('Content-Length') and
1021
        not (response.has_header('Content-Type') and
1022
             response['Content-Type'].startswith('multipart/byteranges'))):
1023
        response['Content-Length'] = len(response.content)
1024

    
1025
    # URL-encode unicode in headers.
1026
    meta = response.items()
1027
    for k, v in meta:
1028
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
1029
                k.startswith('X-Object-') or k.startswith('Content-')):
1030
            del(response[k])
1031
            response[quote(k)] = quote(v, safe='/=,:@; ')
1032

    
1033

    
1034
def render_fault(request, fault):
1035
    if isinstance(fault, faults.InternalServerError) and settings.DEBUG:
1036
        fault.details = format_exc(fault)
1037

    
1038
    request.serialization = 'text'
1039
    data = fault.message + '\n'
1040
    if fault.details:
1041
        data += '\n' + fault.details
1042
    response = HttpResponse(data, status=fault.code)
1043
    update_response_headers(request, response)
1044
    return response
1045

    
1046

    
1047
def request_serialization(request, format_allowed=False):
1048
    """Return the serialization format requested.
1049

1050
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
1051
    """
1052

    
1053
    if not format_allowed:
1054
        return 'text'
1055

    
1056
    format = request.GET.get('format')
1057
    if format == 'json':
1058
        return 'json'
1059
    elif format == 'xml':
1060
        return 'xml'
1061

    
1062
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
1063
        accept, sep, rest = item.strip().partition(';')
1064
        if accept == 'application/json':
1065
            return 'json'
1066
        elif accept == 'application/xml' or accept == 'text/xml':
1067
            return 'xml'
1068

    
1069
    return 'text'
1070

    
1071
def get_pithos_usage(usage):
1072
    for u in usage:
1073
        if u.get('name') == 'pithos+.diskspace':
1074
            return u
1075

    
1076
def api_method(http_method=None, format_allowed=False, user_required=True,
1077
        request_usage=False):
1078
    """Decorator function for views that implement an API method."""
1079

    
1080
    def decorator(func):
1081
        @wraps(func)
1082
        def wrapper(request, *args, **kwargs):
1083
            try:
1084
                if http_method and request.method != http_method:
1085
                    raise faults.BadRequest('Method not allowed.')
1086

    
1087
                if user_required:
1088
                    token = None
1089
                    if request.method in ('HEAD', 'GET') and COOKIE_NAME in request.COOKIES:
1090
                        cookie_value = unquote(
1091
                            request.COOKIES.get(COOKIE_NAME, ''))
1092
                        account, sep, token = cookie_value.partition('|')
1093
                    get_user(request,
1094
                             AUTHENTICATION_URL,
1095
                             AUTHENTICATION_USERS,
1096
                             token,
1097
                             request_usage)
1098
                    if  getattr(request, 'user', None) is None:
1099
                        raise faults.Unauthorized('Access denied')
1100
                    assert getattr(request, 'user_uniq', None) != None
1101
                    request.user_usage = get_pithos_usage(request.user.get('usage', []))
1102
                    request.token = request.GET.get('X-Auth-Token', request.META.get('HTTP_X_AUTH_TOKEN', token))
1103

    
1104
                # The args variable may contain up to (account, container, object).
1105
                if len(args) > 1 and len(args[1]) > 256:
1106
                    raise faults.BadRequest('Container name too large.')
1107
                if len(args) > 2 and len(args[2]) > 1024:
1108
                    raise faults.BadRequest('Object name too large.')
1109

    
1110
                # Format and check headers.
1111
                update_request_headers(request)
1112

    
1113
                # Fill in custom request variables.
1114
                request.serialization = request_serialization(
1115
                    request, format_allowed)
1116
                request.backend = get_backend()
1117

    
1118
                response = func(request, *args, **kwargs)
1119
                update_response_headers(request, response)
1120
                return response
1121
            except faults.Fault, fault:
1122
                if fault.code >= 500:
1123
                    logger.exception("API Fault")
1124
                return render_fault(request, fault)
1125
            except BaseException, e:
1126
                logger.exception('Unexpected error: %s' % e)
1127
                fault = faults.InternalServerError('Unexpected error')
1128
                return render_fault(request, fault)
1129
            finally:
1130
                if getattr(request, 'backend', None) is not None:
1131
                    request.backend.close()
1132
        return wrapper
1133
    return decorator