Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 61efb530

History | View | Annotate | Download (30.6 kB)

1
# Copyright 2011 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

    
41
from django.conf import settings
42
from django.http import HttpResponse
43
from django.utils import simplejson as json
44
from django.utils.http import http_date, parse_etags
45
from django.utils.encoding import smart_str
46

    
47
from pithos.api.compat import parse_http_date_safe, parse_http_date
48
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
49
                                Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
50
                                RangeNotSatisfiable, ServiceUnavailable)
51
from pithos.backends import connect_backend
52
from pithos.backends.base import NotAllowedError, QuotaError
53

    
54
import logging
55
import re
56
import hashlib
57
import uuid
58
import decimal
59

    
60
logger = logging.getLogger(__name__)
61

    
62

    
63
class UTC(tzinfo):
64
   def utcoffset(self, dt):
65
       return timedelta(0)
66

    
67
   def tzname(self, dt):
68
       return 'UTC'
69

    
70
   def dst(self, dt):
71
       return timedelta(0)
72

    
73
def json_encode_decimal(obj):
74
    if isinstance(obj, decimal.Decimal):
75
        return str(obj)
76
    raise TypeError(repr(obj) + " is not JSON serializable")
77

    
78
def isoformat(d):
79
   """Return an ISO8601 date string that includes a timezone."""
80

    
81
   return d.replace(tzinfo=UTC()).isoformat()
82

    
83
def rename_meta_key(d, old, new):
84
    if old not in d:
85
        return
86
    d[new] = d[old]
87
    del(d[old])
88

    
89
def printable_header_dict(d):
90
    """Format a meta dictionary for printing out json/xml.
91
    
92
    Convert all keys to lower case and replace dashes with underscores.
93
    Format 'last_modified' timestamp.
94
    """
95
    
96
    d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
97
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
98

    
99
def format_header_key(k):
100
    """Convert underscores to dashes and capitalize intra-dash strings."""
101
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
102

    
103
def get_header_prefix(request, prefix):
104
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
105
    
106
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
107
    # TODO: Document or remove '~' replacing.
108
    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)])
109

    
110
def get_account_headers(request):
111
    meta = get_header_prefix(request, 'X-Account-Meta-')
112
    groups = {}
113
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
114
        n = k[16:].lower()
115
        if '-' in n or '_' in n:
116
            raise BadRequest('Bad characters in group name')
117
        groups[n] = v.replace(' ', '').split(',')
118
        while '' in groups[n]:
119
            groups[n].remove('')
120
    return meta, groups
121

    
122
def put_account_headers(response, meta, groups, policy):
123
    if 'count' in meta:
124
        response['X-Account-Container-Count'] = meta['count']
125
    if 'bytes' in meta:
126
        response['X-Account-Bytes-Used'] = meta['bytes']
127
    response['Last-Modified'] = http_date(int(meta['modified']))
128
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
129
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
130
    if 'until_timestamp' in meta:
131
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
132
    for k, v in groups.iteritems():
133
        k = smart_str(k, strings_only=True)
134
        k = format_header_key('X-Account-Group-' + k)
135
        v = smart_str(','.join(v), strings_only=True)
136
        response[k] = v
137
    for k, v in policy.iteritems():
138
        response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
139

    
140
def get_container_headers(request):
141
    meta = get_header_prefix(request, 'X-Container-Meta-')
142
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
143
    return meta, policy
144

    
145
def put_container_headers(request, response, meta, policy):
146
    if 'count' in meta:
147
        response['X-Container-Object-Count'] = meta['count']
148
    if 'bytes' in meta:
149
        response['X-Container-Bytes-Used'] = meta['bytes']
150
    response['Last-Modified'] = http_date(int(meta['modified']))
151
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
152
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
153
    l = [smart_str(x, strings_only=True) for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
154
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
155
    response['X-Container-Block-Size'] = request.backend.block_size
156
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
157
    if 'until_timestamp' in meta:
158
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
159
    for k, v in policy.iteritems():
160
        response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
161

    
162
def get_object_headers(request):
163
    meta = get_header_prefix(request, 'X-Object-Meta-')
164
    if request.META.get('CONTENT_TYPE'):
165
        meta['Content-Type'] = request.META['CONTENT_TYPE']
166
    if request.META.get('HTTP_CONTENT_ENCODING'):
167
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
168
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
169
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
170
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
171
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
172
    return meta, get_sharing(request), get_public(request)
173

    
174
def put_object_headers(response, meta, restricted=False):
175
    response['ETag'] = meta['hash']
176
    response['Content-Length'] = meta['bytes']
177
    response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
178
    response['Last-Modified'] = http_date(int(meta['modified']))
179
    if not restricted:
180
        response['X-Object-Modified-By'] = smart_str(meta['modified_by'], strings_only=True)
181
        response['X-Object-Version'] = meta['version']
182
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
183
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
184
            response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
185
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
186
                  'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
187
                  'X-Object-Public'):
188
            if k in meta:
189
                response[k] = smart_str(meta[k], strings_only=True)
190
    else:
191
        for k in ('Content-Encoding', 'Content-Disposition'):
192
            if k in meta:
193
                response[k] = meta[k]
194

    
195
def update_manifest_meta(request, v_account, meta):
196
    """Update metadata if the object has an X-Object-Manifest."""
197
    
198
    if 'X-Object-Manifest' in meta:
199
        hash = ''
200
        bytes = 0
201
        try:
202
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
203
            objects = request.backend.list_objects(request.user_uniq, v_account,
204
                                src_container, prefix=src_name, virtual=False)
205
            for x in objects:
206
                src_meta = request.backend.get_object_meta(request.user_uniq,
207
                                        v_account, src_container, x[0], x[1])
208
                hash += src_meta['hash']
209
                bytes += src_meta['bytes']
210
        except:
211
            # Ignore errors.
212
            return
213
        meta['bytes'] = bytes
214
        md5 = hashlib.md5()
215
        md5.update(hash)
216
        meta['hash'] = md5.hexdigest().lower()
217

    
218
def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
219
    if permissions is None:
220
        return
221
    allowed, perm_path, perms = permissions
222
    if len(perms) == 0:
223
        return
224
    ret = []
225
    r = ','.join(perms.get('read', []))
226
    if r:
227
        ret.append('read=' + r)
228
    w = ','.join(perms.get('write', []))
229
    if w:
230
        ret.append('write=' + w)
231
    meta['X-Object-Sharing'] = '; '.join(ret)
232
    if '/'.join((v_account, v_container, v_object)) != perm_path:
233
        meta['X-Object-Shared-By'] = perm_path
234
    if request.user_uniq != v_account:
235
        meta['X-Object-Allowed-To'] = allowed
236

    
237
def update_public_meta(public, meta):
238
    if not public:
239
        return
240
    meta['X-Object-Public'] = public
241

    
242
def validate_modification_preconditions(request, meta):
243
    """Check that the modified timestamp conforms with the preconditions set."""
244
    
245
    if 'modified' not in meta:
246
        return # TODO: Always return?
247
    
248
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
249
    if if_modified_since is not None:
250
        if_modified_since = parse_http_date_safe(if_modified_since)
251
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
252
        raise NotModified('Resource has not been modified')
253
    
254
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
255
    if if_unmodified_since is not None:
256
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
257
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
258
        raise PreconditionFailed('Resource has been modified')
259

    
260
def validate_matching_preconditions(request, meta):
261
    """Check that the ETag conforms with the preconditions set."""
262
    
263
    hash = meta.get('hash', None)
264
    
265
    if_match = request.META.get('HTTP_IF_MATCH')
266
    if if_match is not None:
267
        if hash is None:
268
            raise PreconditionFailed('Resource does not exist')
269
        if if_match != '*' and hash not in [x.lower() for x in parse_etags(if_match)]:
270
            raise PreconditionFailed('Resource ETag does not match')
271
    
272
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
273
    if if_none_match is not None:
274
        # TODO: If this passes, must ignore If-Modified-Since header.
275
        if hash is not None:
276
            if if_none_match == '*' or hash in [x.lower() for x in parse_etags(if_none_match)]:
277
                # TODO: Continue if an If-Modified-Since header is present.
278
                if request.method in ('HEAD', 'GET'):
279
                    raise NotModified('Resource ETag matches')
280
                raise PreconditionFailed('Resource exists or ETag matches')
281

    
282
def split_container_object_string(s):
283
    if not len(s) > 0 or s[0] != '/':
284
        raise ValueError
285
    s = s[1:]
286
    pos = s.find('/')
287
    if pos == -1:
288
        raise ValueError
289
    return s[:pos], s[(pos + 1):]
290

    
291
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False):
292
    """Copy or move an object."""
293
    
294
    meta, permissions, public = get_object_headers(request)
295
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
296
    try:
297
        if move:
298
            version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
299
                                                        dest_account, dest_container, dest_name,
300
                                                        meta, False, permissions)
301
        else:
302
            version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
303
                                                        dest_account, dest_container, dest_name,
304
                                                        meta, False, permissions, src_version)
305
    except NotAllowedError:
306
        raise Forbidden('Not allowed')
307
    except (NameError, IndexError):
308
        raise ItemNotFound('Container or object does not exist')
309
    except ValueError:
310
        raise BadRequest('Invalid sharing header')
311
    except AttributeError, e:
312
        raise Conflict('\n'.join(e.data) + '\n')
313
    except QuotaError:
314
        raise RequestEntityTooLarge('Quota exceeded')
315
    if public is not None:
316
        try:
317
            request.backend.update_object_public(request.user_uniq, dest_account, dest_container, dest_name, public)
318
        except NotAllowedError:
319
            raise Forbidden('Not allowed')
320
        except NameError:
321
            raise ItemNotFound('Object does not exist')
322
    return version_id
323

    
324
def get_int_parameter(p):
325
    if p is not None:
326
        try:
327
            p = int(p)
328
        except ValueError:
329
            return None
330
        if p < 0:
331
            return None
332
    return p
333

    
334
def get_content_length(request):
335
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
336
    if content_length is None:
337
        raise LengthRequired('Missing or invalid Content-Length header')
338
    return content_length
339

    
340
def get_range(request, size):
341
    """Parse a Range header from the request.
342
    
343
    Either returns None, when the header is not existent or should be ignored,
344
    or a list of (offset, length) tuples - should be further checked.
345
    """
346
    
347
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
348
    if not ranges.startswith('bytes='):
349
        return None
350
    
351
    ret = []
352
    for r in (x.strip() for x in ranges[6:].split(',')):
353
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
354
        m = p.match(r)
355
        if not m:
356
            return None
357
        offset = m.group('offset')
358
        upto = m.group('upto')
359
        if offset == '' and upto == '':
360
            return None
361
        
362
        if offset != '':
363
            offset = int(offset)
364
            if upto != '':
365
                upto = int(upto)
366
                if offset > upto:
367
                    return None
368
                ret.append((offset, upto - offset + 1))
369
            else:
370
                ret.append((offset, size - offset))
371
        else:
372
            length = int(upto)
373
            ret.append((size - length, length))
374
    
375
    return ret
376

    
377
def get_content_range(request):
378
    """Parse a Content-Range header from the request.
379
    
380
    Either returns None, when the header is not existent or should be ignored,
381
    or an (offset, length, total) tuple - check as length, total may be None.
382
    Returns (None, None, None) if the provided range is '*/*'.
383
    """
384
    
385
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
386
    if not ranges:
387
        return None
388
    
389
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
390
    m = p.match(ranges)
391
    if not m:
392
        if ranges == 'bytes */*':
393
            return (None, None, None)
394
        return None
395
    offset = int(m.group('offset'))
396
    upto = m.group('upto')
397
    total = m.group('total')
398
    if upto != '':
399
        upto = int(upto)
400
    else:
401
        upto = None
402
    if total != '*':
403
        total = int(total)
404
    else:
405
        total = None
406
    if (upto is not None and offset > upto) or \
407
        (total is not None and offset >= total) or \
408
        (total is not None and upto is not None and upto >= total):
409
        return None
410
    
411
    if upto is None:
412
        length = None
413
    else:
414
        length = upto - offset + 1
415
    return (offset, length, total)
416

    
417
def get_sharing(request):
418
    """Parse an X-Object-Sharing header from the request.
419
    
420
    Raises BadRequest on error.
421
    """
422
    
423
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
424
    if permissions is None:
425
        return None
426
    
427
    # TODO: Document or remove '~' replacing.
428
    permissions = permissions.replace('~', '')
429
    
430
    ret = {}
431
    permissions = permissions.replace(' ', '')
432
    if permissions == '':
433
        return ret
434
    for perm in (x for x in permissions.split(';')):
435
        if perm.startswith('read='):
436
            ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
437
            if '' in ret['read']:
438
                ret['read'].remove('')
439
            if '*' in ret['read']:
440
                ret['read'] = ['*']
441
            if len(ret['read']) == 0:
442
                raise BadRequest('Bad X-Object-Sharing header value')
443
        elif perm.startswith('write='):
444
            ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
445
            if '' in ret['write']:
446
                ret['write'].remove('')
447
            if '*' in ret['write']:
448
                ret['write'] = ['*']
449
            if len(ret['write']) == 0:
450
                raise BadRequest('Bad X-Object-Sharing header value')
451
        else:
452
            raise BadRequest('Bad X-Object-Sharing header value')
453
    
454
    # Keep duplicates only in write list.
455
    dups = [x for x in ret.get('read', []) if x in ret.get('write', []) and x != '*']
456
    if dups:
457
        for x in dups:
458
            ret['read'].remove(x)
459
        if len(ret['read']) == 0:
460
            del(ret['read'])
461
    
462
    return ret
463

    
464
def get_public(request):
465
    """Parse an X-Object-Public header from the request.
466
    
467
    Raises BadRequest on error.
468
    """
469
    
470
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
471
    if public is None:
472
        return None
473
    
474
    public = public.replace(' ', '').lower()
475
    if public == 'true':
476
        return True
477
    elif public == 'false' or public == '':
478
        return False
479
    raise BadRequest('Bad X-Object-Public header value')
480

    
481
def raw_input_socket(request):
482
    """Return the socket for reading the rest of the request."""
483
    
484
    server_software = request.META.get('SERVER_SOFTWARE')
485
    if server_software and server_software.startswith('mod_python'):
486
        return request._req
487
    if 'wsgi.input' in request.environ:
488
        return request.environ['wsgi.input']
489
    raise ServiceUnavailable('Unknown server software')
490

    
491
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024) # 5GB
492

    
493
def socket_read_iterator(request, length=0, blocksize=4096):
494
    """Return a maximum of blocksize data read from the socket in each iteration.
495
    
496
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
497
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
498
    """
499
    
500
    sock = raw_input_socket(request)
501
    if length < 0: # Chunked transfers
502
        # Small version (server does the dechunking).
503
        if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
504
            while length < MAX_UPLOAD_SIZE:
505
                data = sock.read(blocksize)
506
                if data == '':
507
                    return
508
                yield data
509
            raise BadRequest('Maximum size is reached')
510
        
511
        # Long version (do the dechunking).
512
        data = ''
513
        while length < MAX_UPLOAD_SIZE:
514
            # Get chunk size.
515
            if hasattr(sock, 'readline'):
516
                chunk_length = sock.readline()
517
            else:
518
                chunk_length = ''
519
                while chunk_length[-1:] != '\n':
520
                    chunk_length += sock.read(1)
521
                chunk_length.strip()
522
            pos = chunk_length.find(';')
523
            if pos >= 0:
524
                chunk_length = chunk_length[:pos]
525
            try:
526
                chunk_length = int(chunk_length, 16)
527
            except Exception, e:
528
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
529
            # Check if done.
530
            if chunk_length == 0:
531
                if len(data) > 0:
532
                    yield data
533
                return
534
            # Get the actual data.
535
            while chunk_length > 0:
536
                chunk = sock.read(min(chunk_length, blocksize))
537
                chunk_length -= len(chunk)
538
                if length > 0:
539
                    length += len(chunk)
540
                data += chunk
541
                if len(data) >= blocksize:
542
                    ret = data[:blocksize]
543
                    data = data[blocksize:]
544
                    yield ret
545
            sock.read(2) # CRLF
546
        raise BadRequest('Maximum size is reached')
547
    else:
548
        if length > MAX_UPLOAD_SIZE:
549
            raise BadRequest('Maximum size is reached')
550
        while length > 0:
551
            data = sock.read(min(length, blocksize))
552
            if not data:
553
                raise BadRequest()
554
            length -= len(data)
555
            yield data
556

    
557
class ObjectWrapper(object):
558
    """Return the object's data block-per-block in each iteration.
559
    
560
    Read from the object using the offset and length provided in each entry of the range list.
561
    """
562
    
563
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
564
        self.backend = backend
565
        self.ranges = ranges
566
        self.sizes = sizes
567
        self.hashmaps = hashmaps
568
        self.boundary = boundary
569
        self.size = sum(self.sizes)
570
        
571
        self.file_index = 0
572
        self.block_index = 0
573
        self.block_hash = -1
574
        self.block = ''
575
        
576
        self.range_index = -1
577
        self.offset, self.length = self.ranges[0]
578
    
579
    def __iter__(self):
580
        return self
581
    
582
    def part_iterator(self):
583
        if self.length > 0:
584
            # Get the file for the current offset.
585
            file_size = self.sizes[self.file_index]
586
            while self.offset >= file_size:
587
                self.offset -= file_size
588
                self.file_index += 1
589
                file_size = self.sizes[self.file_index]
590
            
591
            # Get the block for the current position.
592
            self.block_index = int(self.offset / self.backend.block_size)
593
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
594
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
595
                try:
596
                    self.block = self.backend.get_block(self.block_hash)
597
                except NameError:
598
                    raise ItemNotFound('Block does not exist')
599
            
600
            # Get the data from the block.
601
            bo = self.offset % self.backend.block_size
602
            bl = min(self.length, len(self.block) - bo)
603
            data = self.block[bo:bo + bl]
604
            self.offset += bl
605
            self.length -= bl
606
            return data
607
        else:
608
            raise StopIteration
609
    
610
    def next(self):
611
        if len(self.ranges) == 1:
612
            return self.part_iterator()
613
        if self.range_index == len(self.ranges):
614
            raise StopIteration
615
        try:
616
            if self.range_index == -1:
617
                raise StopIteration
618
            return self.part_iterator()
619
        except StopIteration:
620
            self.range_index += 1
621
            out = []
622
            if self.range_index < len(self.ranges):
623
                # Part header.
624
                self.offset, self.length = self.ranges[self.range_index]
625
                self.file_index = 0
626
                if self.range_index > 0:
627
                    out.append('')
628
                out.append('--' + self.boundary)
629
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
630
                out.append('Content-Transfer-Encoding: binary')
631
                out.append('')
632
                out.append('')
633
                return '\r\n'.join(out)
634
            else:
635
                # Footer.
636
                out.append('')
637
                out.append('--' + self.boundary + '--')
638
                out.append('')
639
                return '\r\n'.join(out)
640

    
641
def object_data_response(request, sizes, hashmaps, meta, public=False):
642
    """Get the HttpResponse object for replying with the object's data."""
643
    
644
    # Range handling.
645
    size = sum(sizes)
646
    ranges = get_range(request, size)
647
    if ranges is None:
648
        ranges = [(0, size)]
649
        ret = 200
650
    else:
651
        check = [True for offset, length in ranges if
652
                    length <= 0 or length > size or
653
                    offset < 0 or offset >= size or
654
                    offset + length > size]
655
        if len(check) > 0:
656
            raise RangeNotSatisfiable('Requested range exceeds object limits')
657
        ret = 206
658
        if_range = request.META.get('HTTP_IF_RANGE')
659
        if if_range:
660
            try:
661
                # Modification time has passed instead.
662
                last_modified = parse_http_date(if_range)
663
                if last_modified != meta['modified']:
664
                    ranges = [(0, size)]
665
                    ret = 200
666
            except ValueError:
667
                if if_range != meta['hash']:
668
                    ranges = [(0, size)]
669
                    ret = 200
670
    
671
    if ret == 206 and len(ranges) > 1:
672
        boundary = uuid.uuid4().hex
673
    else:
674
        boundary = ''
675
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
676
    response = HttpResponse(wrapper, status=ret)
677
    put_object_headers(response, meta, public)
678
    if ret == 206:
679
        if len(ranges) == 1:
680
            offset, length = ranges[0]
681
            response['Content-Length'] = length # Update with the correct length.
682
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
683
        else:
684
            del(response['Content-Length'])
685
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
686
    return response
687

    
688
def put_object_block(request, hashmap, data, offset):
689
    """Put one block of data at the given offset."""
690
    
691
    bi = int(offset / request.backend.block_size)
692
    bo = offset % request.backend.block_size
693
    bl = min(len(data), request.backend.block_size - bo)
694
    if bi < len(hashmap):
695
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
696
    else:
697
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
698
    return bl # Return ammount of data written.
699

    
700
def hashmap_hash(request, hashmap):
701
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
702
    
703
    def subhash(d):
704
        h = hashlib.new(request.backend.hash_algorithm)
705
        h.update(d)
706
        return h.digest()
707
    
708
    if len(hashmap) == 0:
709
        return hexlify(subhash(''))
710
    if len(hashmap) == 1:
711
        return hashmap[0]
712
    
713
    s = 2
714
    while s < len(hashmap):
715
        s = s * 2
716
    h = [unhexlify(x) for x in hashmap]
717
    h += [('\x00' * len(h[0]))] * (s - len(hashmap))
718
    while len(h) > 1:
719
        h = [subhash(h[x] + h[x + 1]) for x in range(0, len(h), 2)]
720
    return hexlify(h[0])
721

    
722
def update_response_headers(request, response):
723
    if request.serialization == 'xml':
724
        response['Content-Type'] = 'application/xml; charset=UTF-8'
725
    elif request.serialization == 'json':
726
        response['Content-Type'] = 'application/json; charset=UTF-8'
727
    elif not response['Content-Type']:
728
        response['Content-Type'] = 'text/plain; charset=UTF-8'
729
    
730
    if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
731
        response['Content-Length'] = len(response.content)
732
    
733
    if settings.TEST:
734
        response['Date'] = format_date_time(time())
735

    
736
def render_fault(request, fault):
737
    if settings.DEBUG or settings.TEST:
738
        fault.details = format_exc(fault)
739
    
740
    request.serialization = 'text'
741
    data = '\n'.join((fault.message, fault.details)) + '\n'
742
    response = HttpResponse(data, status=fault.code)
743
    update_response_headers(request, response)
744
    return response
745

    
746
def request_serialization(request, format_allowed=False):
747
    """Return the serialization format requested.
748
    
749
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
750
    """
751
    
752
    if not format_allowed:
753
        return 'text'
754
    
755
    format = request.GET.get('format')
756
    if format == 'json':
757
        return 'json'
758
    elif format == 'xml':
759
        return 'xml'
760
    
761
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
762
        accept, sep, rest = item.strip().partition(';')
763
        if accept == 'application/json':
764
            return 'json'
765
        elif accept == 'application/xml' or accept == 'text/xml':
766
            return 'xml'
767
    
768
    return 'text'
769

    
770
def api_method(http_method=None, format_allowed=False, user_required=True):
771
    """Decorator function for views that implement an API method."""
772
    
773
    def decorator(func):
774
        @wraps(func)
775
        def wrapper(request, *args, **kwargs):
776
            try:
777
                if http_method and request.method != http_method:
778
                    raise BadRequest('Method not allowed.')
779
                if user_required and getattr(request, 'user', None) is None:
780
                    raise Unauthorized('Access denied')
781
                
782
                # The args variable may contain up to (account, container, object).
783
                if len(args) > 1 and len(args[1]) > 256:
784
                    raise BadRequest('Container name too large.')
785
                if len(args) > 2 and len(args[2]) > 1024:
786
                    raise BadRequest('Object name too large.')
787
                
788
                # Fill in custom request variables.
789
                request.serialization = request_serialization(request, format_allowed)
790
                request.backend = connect_backend()
791

    
792
                response = func(request, *args, **kwargs)
793
                update_response_headers(request, response)
794
                return response
795
            except Fault, fault:
796
                return render_fault(request, fault)
797
            except BaseException, e:
798
                logger.exception('Unexpected error: %s' % e)
799
                fault = ServiceUnavailable('Unexpected error')
800
                return render_fault(request, fault)
801
            finally:
802
                if getattr(request, 'backend', None) is not None:
803
                    request.backend.wrapper.conn.close()
804
        return wrapper
805
    return decorator