Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ c032f34d

History | View | Annotate | Download (28 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
39

    
40
from django.conf import settings
41
from django.http import HttpResponse
42
from django.utils import simplejson as json
43
from django.utils.http import http_date, parse_etags
44

    
45
from pithos.api.compat import parse_http_date_safe, parse_http_date
46
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, ItemNotFound,
47
                                Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable,
48
                                ServiceUnavailable)
49
from pithos.backends import backend
50
from pithos.backends.base import NotAllowedError
51

    
52
import datetime
53
import logging
54
import re
55
import hashlib
56
import uuid
57

    
58

    
59
logger = logging.getLogger(__name__)
60

    
61

    
62
def rename_meta_key(d, old, new):
63
    if old not in d:
64
        return
65
    d[new] = d[old]
66
    del(d[old])
67

    
68
def printable_header_dict(d):
69
    """Format a meta dictionary for printing out json/xml.
70
    
71
    Convert all keys to lower case and replace dashes with underscores.
72
    Format 'last_modified' timestamp.
73
    """
74
    
75
    d['last_modified'] = datetime.datetime.fromtimestamp(int(d['last_modified'])).isoformat()
76
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
77

    
78
def format_header_key(k):
79
    """Convert underscores to dashes and capitalize intra-dash strings."""
80
    
81
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
82

    
83
def get_header_prefix(request, prefix):
84
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
85
    
86
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
87
    # TODO: Document or remove '~' replacing.
88
    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)])
89

    
90
def get_account_headers(request):
91
    meta = get_header_prefix(request, 'X-Account-Meta-')
92
    groups = {}
93
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
94
        n = k[16:].lower()
95
        if '-' in n or '_' in n:
96
            raise BadRequest('Bad characters in group name')
97
        groups[n] = v.replace(' ', '').split(',')
98
        if '' in groups[n]:
99
            groups[n].remove('')
100
    return meta, groups
101

    
102
def put_account_headers(response, meta, groups):
103
    if 'count' in meta:
104
        response['X-Account-Container-Count'] = meta['count']
105
    if 'bytes' in meta:
106
        response['X-Account-Bytes-Used'] = meta['bytes']
107
    response['Last-Modified'] = http_date(int(meta['modified']))
108
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
109
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
110
    if 'until_timestamp' in meta:
111
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
112
    for k, v in groups.iteritems():
113
        response[format_header_key('X-Account-Group-' + k).encode('utf-8')] = (','.join(v)).encode('utf-8')
114

    
115
def get_container_headers(request):
116
    meta = get_header_prefix(request, 'X-Container-Meta-')
117
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
118
    return meta, policy
119

    
120
def put_container_headers(response, meta, policy):
121
    if 'count' in meta:
122
        response['X-Container-Object-Count'] = meta['count']
123
    if 'bytes' in meta:
124
        response['X-Container-Bytes-Used'] = meta['bytes']
125
    response['Last-Modified'] = http_date(int(meta['modified']))
126
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
127
        response[k.encode('utf-8')] = meta[k].encode('utf-8')
128
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in meta['object_meta'] if x.startswith('X-Object-Meta-')])
129
    response['X-Container-Block-Size'] = backend.block_size
130
    response['X-Container-Block-Hash'] = backend.hash_algorithm
131
    if 'until_timestamp' in meta:
132
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
133
    for k, v in policy.iteritems():
134
        response[format_header_key('X-Container-Policy-' + k).encode('utf-8')] = v.encode('utf-8')
135

    
136
def get_object_headers(request):
137
    meta = get_header_prefix(request, 'X-Object-Meta-')
138
    if request.META.get('CONTENT_TYPE'):
139
        meta['Content-Type'] = request.META['CONTENT_TYPE']
140
    if request.META.get('HTTP_CONTENT_ENCODING'):
141
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
142
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
143
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
144
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
145
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
146
    return meta, get_sharing(request), get_public(request)
147

    
148
def put_object_headers(response, meta, restricted=False):
149
    response['ETag'] = meta['hash']
150
    response['Content-Length'] = meta['bytes']
151
    response['Content-Type'] = meta.get('Content-Type', 'application/octet-stream')
152
    response['Last-Modified'] = http_date(int(meta['modified']))
153
    if not restricted:
154
        response['X-Object-Modified-By'] = meta['modified_by']
155
        response['X-Object-Version'] = meta['version']
156
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
157
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
158
            response[k.encode('utf-8')] = meta[k].encode('utf-8')
159
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest', 'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Public'):
160
            if k in meta:
161
                response[k] = meta[k]
162
    else:
163
        for k in ('Content-Encoding', 'Content-Disposition'):
164
            if k in meta:
165
                response[k] = meta[k]
166

    
167
def update_manifest_meta(request, v_account, meta):
168
    """Update metadata if the object has an X-Object-Manifest."""
169
    
170
    if 'X-Object-Manifest' in meta:
171
        hash = ''
172
        bytes = 0
173
        try:
174
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
175
            objects = backend.list_objects(request.user, v_account, src_container, prefix=src_name, virtual=False)
176
            for x in objects:
177
                src_meta = backend.get_object_meta(request.user, v_account, src_container, x[0], x[1])
178
                hash += src_meta['hash']
179
                bytes += src_meta['bytes']
180
        except:
181
            # Ignore errors.
182
            return
183
        meta['bytes'] = bytes
184
        md5 = hashlib.md5()
185
        md5.update(hash)
186
        meta['hash'] = md5.hexdigest().lower()
187

    
188
def update_sharing_meta(permissions, v_account, v_container, v_object, meta):
189
    if permissions is None:
190
        return
191
    perm_path, perms = permissions
192
    if len(perms) == 0:
193
        return
194
    ret = []
195
    r = ','.join(perms.get('read', []))
196
    if r:
197
        ret.append('read=' + r)
198
    w = ','.join(perms.get('write', []))
199
    if w:
200
        ret.append('write=' + w)
201
    meta['X-Object-Sharing'] = '; '.join(ret)
202
    if '/'.join((v_account, v_container, v_object)) != perm_path:
203
        meta['X-Object-Shared-By'] = perm_path
204

    
205
def update_public_meta(public, meta):
206
    if not public:
207
        return
208
    meta['X-Object-Public'] = public
209

    
210
def validate_modification_preconditions(request, meta):
211
    """Check that the modified timestamp conforms with the preconditions set."""
212
    
213
    if 'modified' not in meta:
214
        return # TODO: Always return?
215
    
216
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
217
    if if_modified_since is not None:
218
        if_modified_since = parse_http_date_safe(if_modified_since)
219
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
220
        raise NotModified('Resource has not been modified')
221
    
222
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
223
    if if_unmodified_since is not None:
224
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
225
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
226
        raise PreconditionFailed('Resource has been modified')
227

    
228
def validate_matching_preconditions(request, meta):
229
    """Check that the ETag conforms with the preconditions set."""
230
    
231
    hash = meta.get('hash', None)
232
    
233
    if_match = request.META.get('HTTP_IF_MATCH')
234
    if if_match is not None:
235
        if hash is None:
236
            raise PreconditionFailed('Resource does not exist')
237
        if if_match != '*' and hash not in [x.lower() for x in parse_etags(if_match)]:
238
            raise PreconditionFailed('Resource ETag does not match')
239
    
240
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
241
    if if_none_match is not None:
242
        # TODO: If this passes, must ignore If-Modified-Since header.
243
        if hash is not None:
244
            if if_none_match == '*' or hash in [x.lower() for x in parse_etags(if_none_match)]:
245
                # TODO: Continue if an If-Modified-Since header is present.
246
                if request.method in ('HEAD', 'GET'):
247
                    raise NotModified('Resource ETag matches')
248
                raise PreconditionFailed('Resource exists or ETag matches')
249

    
250
def split_container_object_string(s):
251
    if not len(s) > 0 or s[0] != '/':
252
        raise ValueError
253
    s = s[1:]
254
    pos = s.find('/')
255
    if pos == -1:
256
        raise ValueError
257
    return s[:pos], s[(pos + 1):]
258

    
259
def copy_or_move_object(request, v_account, src_container, src_name, dest_container, dest_name, move=False):
260
    """Copy or move an object."""
261
    
262
    meta, permissions, public = get_object_headers(request)
263
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')    
264
    try:
265
        if move:
266
            backend.move_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions)
267
        else:
268
            backend.copy_object(request.user, v_account, src_container, src_name, dest_container, dest_name, meta, False, permissions, src_version)
269
    except NotAllowedError:
270
        raise Unauthorized('Access denied')
271
    except (NameError, IndexError):
272
        raise ItemNotFound('Container or object does not exist')
273
    except ValueError:
274
        raise BadRequest('Invalid sharing header')
275
    except AttributeError, e:
276
        raise Conflict(json.dumps(e.data))
277
    if public is not None:
278
        try:
279
            backend.update_object_public(request.user, v_account, dest_container, dest_name, public)
280
        except NotAllowedError:
281
            raise Unauthorized('Access denied')
282
        except NameError:
283
            raise ItemNotFound('Object does not exist')
284

    
285
def get_int_parameter(p):
286
    if p is not None:
287
        try:
288
            p = int(p)
289
        except ValueError:
290
            return None
291
        if p < 0:
292
            return None
293
    return p
294

    
295
def get_content_length(request):
296
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
297
    if content_length is None:
298
        raise LengthRequired('Missing or invalid Content-Length header')
299
    return content_length
300

    
301
def get_range(request, size):
302
    """Parse a Range header from the request.
303
    
304
    Either returns None, when the header is not existent or should be ignored,
305
    or a list of (offset, length) tuples - should be further checked.
306
    """
307
    
308
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
309
    if not ranges.startswith('bytes='):
310
        return None
311
    
312
    ret = []
313
    for r in (x.strip() for x in ranges[6:].split(',')):
314
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
315
        m = p.match(r)
316
        if not m:
317
            return None
318
        offset = m.group('offset')
319
        upto = m.group('upto')
320
        if offset == '' and upto == '':
321
            return None
322
        
323
        if offset != '':
324
            offset = int(offset)
325
            if upto != '':
326
                upto = int(upto)
327
                if offset > upto:
328
                    return None
329
                ret.append((offset, upto - offset + 1))
330
            else:
331
                ret.append((offset, size - offset))
332
        else:
333
            length = int(upto)
334
            ret.append((size - length, length))
335
    
336
    return ret
337

    
338
def get_content_range(request):
339
    """Parse a Content-Range header from the request.
340
    
341
    Either returns None, when the header is not existent or should be ignored,
342
    or an (offset, length, total) tuple - check as length, total may be None.
343
    Returns (None, None, None) if the provided range is '*/*'.
344
    """
345
    
346
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
347
    if not ranges:
348
        return None
349
    
350
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
351
    m = p.match(ranges)
352
    if not m:
353
        if ranges == 'bytes */*':
354
            return (None, None, None)
355
        return None
356
    offset = int(m.group('offset'))
357
    upto = m.group('upto')
358
    total = m.group('total')
359
    if upto != '':
360
        upto = int(upto)
361
    else:
362
        upto = None
363
    if total != '*':
364
        total = int(total)
365
    else:
366
        total = None
367
    if (upto is not None and offset > upto) or \
368
        (total is not None and offset >= total) or \
369
        (total is not None and upto is not None and upto >= total):
370
        return None
371
    
372
    if upto is None:
373
        length = None
374
    else:
375
        length = upto - offset + 1
376
    return (offset, length, total)
377

    
378
def get_sharing(request):
379
    """Parse an X-Object-Sharing header from the request.
380
    
381
    Raises BadRequest on error.
382
    """
383
    
384
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
385
    if permissions is None:
386
        return None
387
    
388
    ret = {}
389
    permissions = permissions.replace(' ', '')
390
    if permissions == '':
391
        return ret
392
    for perm in (x for x in permissions.split(';')):
393
        if perm.startswith('read='):
394
            ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
395
            if '' in ret['read']:
396
                ret['read'].remove('')
397
            if '*' in ret['read']:
398
                ret['read'] = ['*']
399
            if len(ret['read']) == 0:
400
                raise BadRequest('Bad X-Object-Sharing header value')
401
        elif perm.startswith('write='):
402
            ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
403
            if '' in ret['write']:
404
                ret['write'].remove('')
405
            if '*' in ret['write']:
406
                ret['write'] = ['*']
407
            if len(ret['write']) == 0:
408
                raise BadRequest('Bad X-Object-Sharing header value')
409
        else:
410
            raise BadRequest('Bad X-Object-Sharing header value')
411
    return ret
412

    
413
def get_public(request):
414
    """Parse an X-Object-Public header from the request.
415
    
416
    Raises BadRequest on error.
417
    """
418
    
419
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
420
    if public is None:
421
        return None
422
    
423
    public = public.replace(' ', '').lower()
424
    if public == 'true':
425
        return True
426
    elif public == 'false' or public == '':
427
        return False
428
    raise BadRequest('Bad X-Object-Public header value')
429

    
430
def raw_input_socket(request):
431
    """Return the socket for reading the rest of the request."""
432
    
433
    server_software = request.META.get('SERVER_SOFTWARE')
434
    if server_software and server_software.startswith('mod_python'):
435
        return request._req
436
    if 'wsgi.input' in request.environ:
437
        return request.environ['wsgi.input']
438
    raise ServiceUnavailable('Unknown server software')
439

    
440
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
441

    
442
def socket_read_iterator(request, length=0, blocksize=4096):
443
    """Return a maximum of blocksize data read from the socket in each iteration.
444
    
445
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
446
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
447
    """
448
    
449
    sock = raw_input_socket(request)
450
    if length < 0: # Chunked transfers
451
        # Small version (server does the dechunking).
452
        if request.environ.get('mod_wsgi.input_chunked', None):
453
            while length < MAX_UPLOAD_SIZE:
454
                data = sock.read(blocksize)
455
                if data == '':
456
                    return
457
                yield data
458
            raise BadRequest('Maximum size is reached')
459
        
460
        # Long version (do the dechunking).
461
        data = ''
462
        while length < MAX_UPLOAD_SIZE:
463
            # Get chunk size.
464
            if hasattr(sock, 'readline'):
465
                chunk_length = sock.readline()
466
            else:
467
                chunk_length = ''
468
                while chunk_length[-1:] != '\n':
469
                    chunk_length += sock.read(1)
470
                chunk_length.strip()
471
            pos = chunk_length.find(';')
472
            if pos >= 0:
473
                chunk_length = chunk_length[:pos]
474
            try:
475
                chunk_length = int(chunk_length, 16)
476
            except Exception, e:
477
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
478
            # Check if done.
479
            if chunk_length == 0:
480
                if len(data) > 0:
481
                    yield data
482
                return
483
            # Get the actual data.
484
            while chunk_length > 0:
485
                chunk = sock.read(min(chunk_length, blocksize))
486
                chunk_length -= len(chunk)
487
                if length > 0:
488
                    length += len(chunk)
489
                data += chunk
490
                if len(data) >= blocksize:
491
                    ret = data[:blocksize]
492
                    data = data[blocksize:]
493
                    yield ret
494
            sock.read(2) # CRLF
495
        raise BadRequest('Maximum size is reached')
496
    else:
497
        if length > MAX_UPLOAD_SIZE:
498
            raise BadRequest('Maximum size is reached')
499
        while length > 0:
500
            data = sock.read(min(length, blocksize))
501
            length -= len(data)
502
            yield data
503

    
504
class ObjectWrapper(object):
505
    """Return the object's data block-per-block in each iteration.
506
    
507
    Read from the object using the offset and length provided in each entry of the range list.
508
    """
509
    
510
    def __init__(self, ranges, sizes, hashmaps, boundary):
511
        self.ranges = ranges
512
        self.sizes = sizes
513
        self.hashmaps = hashmaps
514
        self.boundary = boundary
515
        self.size = sum(self.sizes)
516
        
517
        self.file_index = 0
518
        self.block_index = 0
519
        self.block_hash = -1
520
        self.block = ''
521
        
522
        self.range_index = -1
523
        self.offset, self.length = self.ranges[0]
524
    
525
    def __iter__(self):
526
        return self
527
    
528
    def part_iterator(self):
529
        if self.length > 0:
530
            # Get the file for the current offset.
531
            file_size = self.sizes[self.file_index]
532
            while self.offset >= file_size:
533
                self.offset -= file_size
534
                self.file_index += 1
535
                file_size = self.sizes[self.file_index]
536
            
537
            # Get the block for the current position.
538
            self.block_index = int(self.offset / backend.block_size)
539
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
540
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
541
                try:
542
                    self.block = backend.get_block(self.block_hash)
543
                except NameError:
544
                    raise ItemNotFound('Block does not exist')
545
            
546
            # Get the data from the block.
547
            bo = self.offset % backend.block_size
548
            bl = min(self.length, len(self.block) - bo)
549
            data = self.block[bo:bo + bl]
550
            self.offset += bl
551
            self.length -= bl
552
            return data
553
        else:
554
            raise StopIteration
555
    
556
    def next(self):
557
        if len(self.ranges) == 1:
558
            return self.part_iterator()
559
        if self.range_index == len(self.ranges):
560
            raise StopIteration
561
        try:
562
            if self.range_index == -1:
563
                raise StopIteration
564
            return self.part_iterator()
565
        except StopIteration:
566
            self.range_index += 1
567
            out = []
568
            if self.range_index < len(self.ranges):
569
                # Part header.
570
                self.offset, self.length = self.ranges[self.range_index]
571
                self.file_index = 0
572
                if self.range_index > 0:
573
                    out.append('')
574
                out.append('--' + self.boundary)
575
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
576
                out.append('Content-Transfer-Encoding: binary')
577
                out.append('')
578
                out.append('')
579
                return '\r\n'.join(out)
580
            else:
581
                # Footer.
582
                out.append('')
583
                out.append('--' + self.boundary + '--')
584
                out.append('')
585
                return '\r\n'.join(out)
586

    
587
def object_data_response(request, sizes, hashmaps, meta, public=False):
588
    """Get the HttpResponse object for replying with the object's data."""
589
    
590
    # Range handling.
591
    size = sum(sizes)
592
    ranges = get_range(request, size)
593
    if ranges is None:
594
        ranges = [(0, size)]
595
        ret = 200
596
    else:
597
        check = [True for offset, length in ranges if
598
                    length <= 0 or length > size or
599
                    offset < 0 or offset >= size or
600
                    offset + length > size]
601
        if len(check) > 0:
602
            raise RangeNotSatisfiable('Requested range exceeds object limits')
603
        ret = 206
604
        if_range = request.META.get('HTTP_IF_RANGE')
605
        if if_range:
606
            try:
607
                # Modification time has passed instead.
608
                last_modified = parse_http_date(if_range)
609
                if last_modified != meta['modified']:
610
                    ranges = [(0, size)]
611
                    ret = 200
612
            except ValueError:
613
                if if_range != meta['hash']:
614
                    ranges = [(0, size)]
615
                    ret = 200
616
    
617
    if ret == 206 and len(ranges) > 1:
618
        boundary = uuid.uuid4().hex
619
    else:
620
        boundary = ''
621
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
622
    response = HttpResponse(wrapper, status=ret)
623
    put_object_headers(response, meta, public)
624
    if ret == 206:
625
        if len(ranges) == 1:
626
            offset, length = ranges[0]
627
            response['Content-Length'] = length # Update with the correct length.
628
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
629
        else:
630
            del(response['Content-Length'])
631
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
632
    return response
633

    
634
def put_object_block(hashmap, data, offset):
635
    """Put one block of data at the given offset."""
636
    
637
    bi = int(offset / backend.block_size)
638
    bo = offset % backend.block_size
639
    bl = min(len(data), backend.block_size - bo)
640
    if bi < len(hashmap):
641
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
642
    else:
643
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
644
    return bl # Return ammount of data written.
645

    
646
def hashmap_hash(hashmap):
647
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
648
    
649
    def subhash(d):
650
        h = hashlib.new(backend.hash_algorithm)
651
        h.update(d)
652
        return h.digest()
653
    
654
    if len(hashmap) == 0:
655
        return hexlify(subhash(''))
656
    if len(hashmap) == 1:
657
        return hexlify(subhash(hashmap[0]))
658
    s = 2
659
    while s < len(hashmap):
660
        s = s * 2
661
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
662
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
663
    while len(h) > 1:
664
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
665
    return hexlify(h[0])
666

    
667
def update_response_headers(request, response):
668
    if request.serialization == 'xml':
669
        response['Content-Type'] = 'application/xml; charset=UTF-8'
670
    elif request.serialization == 'json':
671
        response['Content-Type'] = 'application/json; charset=UTF-8'
672
    elif not response['Content-Type']:
673
        response['Content-Type'] = 'text/plain; charset=UTF-8'
674
    
675
    if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
676
        response['Content-Length'] = len(response.content)
677
    
678
    if settings.TEST:
679
        response['Date'] = format_date_time(time())
680

    
681
def render_fault(request, fault):
682
    if settings.DEBUG or settings.TEST:
683
        fault.details = format_exc(fault)
684
    
685
    request.serialization = 'text'
686
    data = '\n'.join((fault.message, fault.details)) + '\n'
687
    response = HttpResponse(data, status=fault.code)
688
    update_response_headers(request, response)
689
    return response
690

    
691
def request_serialization(request, format_allowed=False):
692
    """Return the serialization format requested.
693
    
694
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
695
    """
696
    
697
    if not format_allowed:
698
        return 'text'
699
    
700
    format = request.GET.get('format')
701
    if format == 'json':
702
        return 'json'
703
    elif format == 'xml':
704
        return 'xml'
705
    
706
#     for item in request.META.get('HTTP_ACCEPT', '').split(','):
707
#         accept, sep, rest = item.strip().partition(';')
708
#         if accept == 'application/json':
709
#             return 'json'
710
#         elif accept == 'application/xml' or accept == 'text/xml':
711
#             return 'xml'
712
    
713
    return 'text'
714

    
715
def api_method(http_method=None, format_allowed=False):
716
    """Decorator function for views that implement an API method."""
717
    
718
    def decorator(func):
719
        @wraps(func)
720
        def wrapper(request, *args, **kwargs):
721
            try:
722
                if http_method and request.method != http_method:
723
                    raise BadRequest('Method not allowed.')
724
                
725
                # The args variable may contain up to (account, container, object).
726
                if len(args) > 1 and len(args[1]) > 256:
727
                    raise BadRequest('Container name too large.')
728
                if len(args) > 2 and len(args[2]) > 1024:
729
                    raise BadRequest('Object name too large.')
730
                
731
                # Fill in custom request variables.
732
                request.serialization = request_serialization(request, format_allowed)
733
                
734
                response = func(request, *args, **kwargs)
735
                update_response_headers(request, response)
736
                return response
737
            except Fault, fault:
738
                return render_fault(request, fault)
739
            except BaseException, e:
740
                logger.exception('Unexpected error: %s' % e)
741
                fault = ServiceUnavailable('Unexpected error')
742
                return render_fault(request, fault)
743
        return wrapper
744
    return decorator