Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 804e8fe7

History | View | Annotate | Download (27.2 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
    if 'hash' not in meta:
232
        return # TODO: Always return?
233
    
234
    if_match = request.META.get('HTTP_IF_MATCH')
235
    if if_match is not None and if_match != '*':
236
        if meta['hash'] not in [x.lower() for x in parse_etags(if_match)]:
237
            raise PreconditionFailed('Resource Etag does not match')
238
    
239
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
240
    if if_none_match is not None:
241
        if if_none_match == '*' or meta['hash'] in [x.lower() for x in parse_etags(if_none_match)]:
242
            raise NotModified('Resource Etag matches')
243

    
244
def split_container_object_string(s):
245
    if not len(s) > 0 or s[0] != '/':
246
        raise ValueError
247
    s = s[1:]
248
    pos = s.find('/')
249
    if pos == -1:
250
        raise ValueError
251
    return s[:pos], s[(pos + 1):]
252

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

    
279
def get_int_parameter(p):
280
    if p is not None:
281
        try:
282
            p = int(p)
283
        except ValueError:
284
            return None
285
        if p < 0:
286
            return None
287
    return p
288

    
289
def get_content_length(request):
290
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
291
    if content_length is None:
292
        raise LengthRequired('Missing or invalid Content-Length header')
293
    return content_length
294

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

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

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

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

    
424
def raw_input_socket(request):
425
    """Return the socket for reading the rest of the request."""
426
    
427
    server_software = request.META.get('SERVER_SOFTWARE')
428
    if server_software and server_software.startswith('mod_python'):
429
        return request._req
430
    if 'wsgi.input' in request.environ:
431
        return request.environ['wsgi.input']
432
    raise ServiceUnavailable('Unknown server software')
433

    
434
MAX_UPLOAD_SIZE = 10 * (1024 * 1024) # 10MB
435

    
436
def socket_read_iterator(sock, length=0, blocksize=4096):
437
    """Return a maximum of blocksize data read from the socket in each iteration.
438
    
439
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
440
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
441
    """
442
    
443
    if length < 0: # Chunked transfers
444
        data = ''
445
        while length < MAX_UPLOAD_SIZE:
446
            # Get chunk size.
447
            if hasattr(sock, 'readline'):
448
                chunk_length = sock.readline()
449
            else:
450
                chunk_length = ''
451
                while chunk_length[-1:] != '\n':
452
                    chunk_length += sock.read(1)
453
                chunk_length.strip()
454
            pos = chunk_length.find(';')
455
            if pos >= 0:
456
                chunk_length = chunk_length[:pos]
457
            try:
458
                chunk_length = int(chunk_length, 16)
459
            except Exception, e:
460
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
461
            # Check if done.
462
            if chunk_length == 0:
463
                if len(data) > 0:
464
                    yield data
465
                return
466
            # Get the actual data.
467
            while chunk_length > 0:
468
                chunk = sock.read(min(chunk_length, blocksize))
469
                chunk_length -= len(chunk)
470
                if length > 0:
471
                    length += len(chunk)
472
                data += chunk
473
                if len(data) >= blocksize:
474
                    ret = data[:blocksize]
475
                    data = data[blocksize:]
476
                    yield ret
477
            sock.read(2) # CRLF
478
        raise BadRequest('Maximum size is reached')
479
    else:
480
        if length > MAX_UPLOAD_SIZE:
481
            raise BadRequest('Maximum size is reached')
482
        while length > 0:
483
            data = sock.read(min(length, blocksize))
484
            length -= len(data)
485
            yield data
486

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

    
570
def object_data_response(request, sizes, hashmaps, meta, public=False):
571
    """Get the HttpResponse object for replying with the object's data."""
572
    
573
    # Range handling.
574
    size = sum(sizes)
575
    ranges = get_range(request, size)
576
    if ranges is None:
577
        ranges = [(0, size)]
578
        ret = 200
579
    else:
580
        check = [True for offset, length in ranges if
581
                    length <= 0 or length > size or
582
                    offset < 0 or offset >= size or
583
                    offset + length > size]
584
        if len(check) > 0:
585
            raise RangeNotSatisfiable('Requested range exceeds object limits')
586
        ret = 206
587
        if_range = request.META.get('HTTP_IF_RANGE')
588
        if if_range:
589
            try:
590
                # Modification time has passed instead.
591
                last_modified = parse_http_date(if_range)
592
                if last_modified != meta['modified']:
593
                    ranges = [(0, size)]
594
                    ret = 200
595
            except ValueError:
596
                if if_range != meta['hash']:
597
                    ranges = [(0, size)]
598
                    ret = 200
599
    
600
    if ret == 206 and len(ranges) > 1:
601
        boundary = uuid.uuid4().hex
602
    else:
603
        boundary = ''
604
    wrapper = ObjectWrapper(ranges, sizes, hashmaps, boundary)
605
    response = HttpResponse(wrapper, status=ret)
606
    put_object_headers(response, meta, public)
607
    if ret == 206:
608
        if len(ranges) == 1:
609
            offset, length = ranges[0]
610
            response['Content-Length'] = length # Update with the correct length.
611
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
612
        else:
613
            del(response['Content-Length'])
614
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
615
    return response
616

    
617
def put_object_block(hashmap, data, offset):
618
    """Put one block of data at the given offset."""
619
    
620
    bi = int(offset / backend.block_size)
621
    bo = offset % backend.block_size
622
    bl = min(len(data), backend.block_size - bo)
623
    if bi < len(hashmap):
624
        hashmap[bi] = backend.update_block(hashmap[bi], data[:bl], bo)
625
    else:
626
        hashmap.append(backend.put_block(('\x00' * bo) + data[:bl]))
627
    return bl # Return ammount of data written.
628

    
629
def hashmap_hash(hashmap):
630
    """Produce the root hash, treating the hashmap as a Merkle-like tree."""
631
    
632
    def subhash(d):
633
        h = hashlib.new(backend.hash_algorithm)
634
        h.update(d)
635
        return h.digest()
636
    
637
    if len(hashmap) == 0:
638
        return hexlify(subhash(''))
639
    if len(hashmap) == 1:
640
        return hexlify(subhash(hashmap[0]))
641
    s = 2
642
    while s < len(hashmap):
643
        s = s * 2
644
    h = hashmap + ([('\x00' * len(hashmap[0]))] * (s - len(hashmap)))
645
    h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
646
    while len(h) > 1:
647
        h = [subhash(h[x] + (h[x + 1] if x + 1 < len(h) else '')) for x in range(0, len(h), 2)]
648
    return hexlify(h[0])
649

    
650
def update_response_headers(request, response):
651
    if request.serialization == 'xml':
652
        response['Content-Type'] = 'application/xml; charset=UTF-8'
653
    elif request.serialization == 'json':
654
        response['Content-Type'] = 'application/json; charset=UTF-8'
655
    elif not response['Content-Type']:
656
        response['Content-Type'] = 'text/plain; charset=UTF-8'
657
    
658
    if not response.has_header('Content-Length') and not (response.has_header('Content-Type') and response['Content-Type'].startswith('multipart/byteranges')):
659
        response['Content-Length'] = len(response.content)
660
    
661
    if settings.TEST:
662
        response['Date'] = format_date_time(time())
663

    
664
def render_fault(request, fault):
665
    if settings.DEBUG or settings.TEST:
666
        fault.details = format_exc(fault)
667
    
668
    request.serialization = 'text'
669
    data = '\n'.join((fault.message, fault.details)) + '\n'
670
    response = HttpResponse(data, status=fault.code)
671
    update_response_headers(request, response)
672
    return response
673

    
674
def request_serialization(request, format_allowed=False):
675
    """Return the serialization format requested.
676
    
677
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
678
    """
679
    
680
    if not format_allowed:
681
        return 'text'
682
    
683
    format = request.GET.get('format')
684
    if format == 'json':
685
        return 'json'
686
    elif format == 'xml':
687
        return 'xml'
688
    
689
#     for item in request.META.get('HTTP_ACCEPT', '').split(','):
690
#         accept, sep, rest = item.strip().partition(';')
691
#         if accept == 'application/json':
692
#             return 'json'
693
#         elif accept == 'application/xml' or accept == 'text/xml':
694
#             return 'xml'
695
    
696
    return 'text'
697

    
698
def api_method(http_method=None, format_allowed=False):
699
    """Decorator function for views that implement an API method."""
700
    
701
    def decorator(func):
702
        @wraps(func)
703
        def wrapper(request, *args, **kwargs):
704
            try:
705
                if http_method and request.method != http_method:
706
                    raise BadRequest('Method not allowed.')
707
                
708
                # The args variable may contain up to (account, container, object).
709
                if len(args) > 1 and len(args[1]) > 256:
710
                    raise BadRequest('Container name too large.')
711
                if len(args) > 2 and len(args[2]) > 1024:
712
                    raise BadRequest('Object name too large.')
713
                
714
                # Fill in custom request variables.
715
                request.serialization = request_serialization(request, format_allowed)
716
                
717
                response = func(request, *args, **kwargs)
718
                update_response_headers(request, response)
719
                return response
720
            except Fault, fault:
721
                return render_fault(request, fault)
722
            except BaseException, e:
723
                logger.exception('Unexpected error: %s' % e)
724
                fault = ServiceUnavailable('Unexpected error')
725
                return render_fault(request, fault)
726
        return wrapper
727
    return decorator