Statistics
| Branch: | Tag: | Revision:

root / snf-pithos-app / pithos / api / util.py @ 4d15c94e

History | View | Annotate | Download (35.7 kB)

1 2e662088 Antony Chazapis
# Copyright 2011-2012 GRNET S.A. All rights reserved.
2 5635f9ef Antony Chazapis
# 
3 5635f9ef Antony Chazapis
# Redistribution and use in source and binary forms, with or
4 5635f9ef Antony Chazapis
# without modification, are permitted provided that the following
5 5635f9ef Antony Chazapis
# conditions are met:
6 5635f9ef Antony Chazapis
# 
7 5635f9ef Antony Chazapis
#   1. Redistributions of source code must retain the above
8 5635f9ef Antony Chazapis
#      copyright notice, this list of conditions and the following
9 5635f9ef Antony Chazapis
#      disclaimer.
10 5635f9ef Antony Chazapis
# 
11 5635f9ef Antony Chazapis
#   2. Redistributions in binary form must reproduce the above
12 5635f9ef Antony Chazapis
#      copyright notice, this list of conditions and the following
13 5635f9ef Antony Chazapis
#      disclaimer in the documentation and/or other materials
14 5635f9ef Antony Chazapis
#      provided with the distribution.
15 5635f9ef Antony Chazapis
# 
16 5635f9ef Antony Chazapis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 5635f9ef Antony Chazapis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 5635f9ef Antony Chazapis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 5635f9ef Antony Chazapis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 5635f9ef Antony Chazapis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 5635f9ef Antony Chazapis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 5635f9ef Antony Chazapis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 5635f9ef Antony Chazapis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 5635f9ef Antony Chazapis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 5635f9ef Antony Chazapis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 5635f9ef Antony Chazapis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 5635f9ef Antony Chazapis
# POSSIBILITY OF SUCH DAMAGE.
28 5635f9ef Antony Chazapis
# 
29 5635f9ef Antony Chazapis
# The views and conclusions contained in the software and
30 5635f9ef Antony Chazapis
# documentation are those of the authors and should not be
31 5635f9ef Antony Chazapis
# interpreted as representing official policies, either expressed
32 5635f9ef Antony Chazapis
# or implied, of GRNET S.A.
33 5635f9ef Antony Chazapis
34 b956618e Antony Chazapis
from functools import wraps
35 b956618e Antony Chazapis
from time import time
36 b956618e Antony Chazapis
from traceback import format_exc
37 b956618e Antony Chazapis
from wsgiref.handlers import format_date_time
38 0d4ea090 Antony Chazapis
from binascii import hexlify, unhexlify
39 e11087c2 Antony Chazapis
from datetime import datetime, tzinfo, timedelta
40 9fefc052 Antony Chazapis
from urllib import quote, unquote
41 b956618e Antony Chazapis
42 b956618e Antony Chazapis
from django.conf import settings
43 b956618e Antony Chazapis
from django.http import HttpResponse
44 6b6b6c1e Antony Chazapis
from django.template.loader import render_to_string
45 1993fea9 Antony Chazapis
from django.utils import simplejson as json
46 22dab079 Antony Chazapis
from django.utils.http import http_date, parse_etags
47 fb064032 Antony Chazapis
from django.utils.encoding import smart_unicode, smart_str
48 817890f2 Antony Chazapis
from django.core.files.uploadhandler import FileUploadHandler
49 817890f2 Antony Chazapis
from django.core.files.uploadedfile import UploadedFile
50 b956618e Antony Chazapis
51 6e147ecc Antony Chazapis
from synnefo.lib.parsedate import parse_http_date_safe, parse_http_date
52 dfdf4802 Sofia Papagiannaki
from synnefo.lib.astakos import get_user
53 5a96180b Antony Chazapis
54 297513ba Antony Chazapis
from pithos.api.faults import (Fault, NotModified, BadRequest, Unauthorized, Forbidden, ItemNotFound,
55 5df6c6d1 Antony Chazapis
                                Conflict, LengthRequired, PreconditionFailed, RequestEntityTooLarge,
56 08de868d Antony Chazapis
                                RangeNotSatisfiable, InternalServerError, NotImplemented)
57 bb4eafc6 Antony Chazapis
from pithos.api.short_url import encode_url
58 a7dff008 Antony Chazapis
from pithos.api.settings import (BACKEND_DB_MODULE, BACKEND_DB_CONNECTION,
59 a7dff008 Antony Chazapis
                                    BACKEND_BLOCK_MODULE, BACKEND_BLOCK_PATH,
60 1427ce62 Sofia Papagiannaki
                                    BACKEND_BLOCK_UMASK,
61 a7dff008 Antony Chazapis
                                    BACKEND_QUEUE_MODULE, BACKEND_QUEUE_CONNECTION,
62 dfdf4802 Sofia Papagiannaki
                                    BACKEND_QUOTA, BACKEND_VERSIONING,
63 dfdf4802 Sofia Papagiannaki
                                    AUTHENTICATION_URL, AUTHENTICATION_USERS,
64 dfdf4802 Sofia Papagiannaki
                                    SERVICE_TOKEN, COOKIE_NAME)
65 dfdf4802 Sofia Papagiannaki
66 39593b2b Giorgos Verigakis
from pithos.backends import connect_backend
67 5df6c6d1 Antony Chazapis
from pithos.backends.base import NotAllowedError, QuotaError
68 b956618e Antony Chazapis
69 b956618e Antony Chazapis
import logging
70 22dab079 Antony Chazapis
import re
71 cbfb6636 Sofia Papagiannaki
import hashlib
72 7bef5750 Antony Chazapis
import uuid
73 c48acbfd Sofia Papagiannaki
import decimal
74 8c793655 Antony Chazapis
75 b956618e Antony Chazapis
76 b956618e Antony Chazapis
logger = logging.getLogger(__name__)
77 b956618e Antony Chazapis
78 b956618e Antony Chazapis
79 e11087c2 Antony Chazapis
class UTC(tzinfo):
80 e11087c2 Antony Chazapis
   def utcoffset(self, dt):
81 e11087c2 Antony Chazapis
       return timedelta(0)
82 e11087c2 Antony Chazapis
83 e11087c2 Antony Chazapis
   def tzname(self, dt):
84 e11087c2 Antony Chazapis
       return 'UTC'
85 e11087c2 Antony Chazapis
86 e11087c2 Antony Chazapis
   def dst(self, dt):
87 e11087c2 Antony Chazapis
       return timedelta(0)
88 e11087c2 Antony Chazapis
89 c48acbfd Sofia Papagiannaki
def json_encode_decimal(obj):
90 c48acbfd Sofia Papagiannaki
    if isinstance(obj, decimal.Decimal):
91 c48acbfd Sofia Papagiannaki
        return str(obj)
92 c48acbfd Sofia Papagiannaki
    raise TypeError(repr(obj) + " is not JSON serializable")
93 c48acbfd Sofia Papagiannaki
94 e11087c2 Antony Chazapis
def isoformat(d):
95 e11087c2 Antony Chazapis
   """Return an ISO8601 date string that includes a timezone."""
96 e11087c2 Antony Chazapis
97 e11087c2 Antony Chazapis
   return d.replace(tzinfo=UTC()).isoformat()
98 e11087c2 Antony Chazapis
99 804e8fe7 Antony Chazapis
def rename_meta_key(d, old, new):
100 804e8fe7 Antony Chazapis
    if old not in d:
101 804e8fe7 Antony Chazapis
        return
102 804e8fe7 Antony Chazapis
    d[new] = d[old]
103 804e8fe7 Antony Chazapis
    del(d[old])
104 804e8fe7 Antony Chazapis
105 02c0c3fa Antony Chazapis
def printable_header_dict(d):
106 b956618e Antony Chazapis
    """Format a meta dictionary for printing out json/xml.
107 b956618e Antony Chazapis
    
108 804e8fe7 Antony Chazapis
    Convert all keys to lower case and replace dashes with underscores.
109 804e8fe7 Antony Chazapis
    Format 'last_modified' timestamp.
110 b956618e Antony Chazapis
    """
111 83dd59c5 Antony Chazapis
    
112 371d907a Antony Chazapis
    if 'last_modified' in d and d['last_modified']:
113 690747fe Antony Chazapis
        d['last_modified'] = isoformat(datetime.fromtimestamp(d['last_modified']))
114 b956618e Antony Chazapis
    return dict([(k.lower().replace('-', '_'), v) for k, v in d.iteritems()])
115 b956618e Antony Chazapis
116 02c0c3fa Antony Chazapis
def format_header_key(k):
117 58a6c894 Antony Chazapis
    """Convert underscores to dashes and capitalize intra-dash strings."""
118 b956618e Antony Chazapis
    return '-'.join([x.capitalize() for x in k.replace('_', '-').split('-')])
119 b956618e Antony Chazapis
120 02c0c3fa Antony Chazapis
def get_header_prefix(request, prefix):
121 02c0c3fa Antony Chazapis
    """Get all prefix-* request headers in a dict. Reformat keys with format_header_key()."""
122 83dd59c5 Antony Chazapis
    
123 b956618e Antony Chazapis
    prefix = 'HTTP_' + prefix.upper().replace('-', '_')
124 3ab38c43 Antony Chazapis
    # TODO: Document or remove '~' replacing.
125 3ab38c43 Antony Chazapis
    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)])
126 02c0c3fa Antony Chazapis
127 62bf8157 Antony Chazapis
def check_meta_headers(meta):
128 62bf8157 Antony Chazapis
    if len(meta) > 90:
129 62bf8157 Antony Chazapis
        raise BadRequest('Too many headers.')
130 62bf8157 Antony Chazapis
    for k, v in meta.iteritems():
131 62bf8157 Antony Chazapis
        if len(k) > 128:
132 62bf8157 Antony Chazapis
            raise BadRequest('Header name too large.')
133 62bf8157 Antony Chazapis
        if len(v) > 256:
134 62bf8157 Antony Chazapis
            raise BadRequest('Header value too large.')
135 62bf8157 Antony Chazapis
136 02c0c3fa Antony Chazapis
def get_account_headers(request):
137 02c0c3fa Antony Chazapis
    meta = get_header_prefix(request, 'X-Account-Meta-')
138 62bf8157 Antony Chazapis
    check_meta_headers(meta)
139 02c0c3fa Antony Chazapis
    groups = {}
140 02c0c3fa Antony Chazapis
    for k, v in get_header_prefix(request, 'X-Account-Group-').iteritems():
141 02c0c3fa Antony Chazapis
        n = k[16:].lower()
142 02c0c3fa Antony Chazapis
        if '-' in n or '_' in n:
143 02c0c3fa Antony Chazapis
            raise BadRequest('Bad characters in group name')
144 02c0c3fa Antony Chazapis
        groups[n] = v.replace(' ', '').split(',')
145 40d6b76d Antony Chazapis
        while '' in groups[n]:
146 02c0c3fa Antony Chazapis
            groups[n].remove('')
147 02c0c3fa Antony Chazapis
    return meta, groups
148 02c0c3fa Antony Chazapis
149 647a5f48 Antony Chazapis
def put_account_headers(response, meta, groups, policy):
150 f6c97079 Antony Chazapis
    if 'count' in meta:
151 f6c97079 Antony Chazapis
        response['X-Account-Container-Count'] = meta['count']
152 f6c97079 Antony Chazapis
    if 'bytes' in meta:
153 f6c97079 Antony Chazapis
        response['X-Account-Bytes-Used'] = meta['bytes']
154 d065f612 Antony Chazapis
    response['Last-Modified'] = http_date(int(meta['modified']))
155 b956618e Antony Chazapis
    for k in [x for x in meta.keys() if x.startswith('X-Account-Meta-')]:
156 e7b51248 Sofia Papagiannaki
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
157 83dd59c5 Antony Chazapis
    if 'until_timestamp' in meta:
158 83dd59c5 Antony Chazapis
        response['X-Account-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
159 02c0c3fa Antony Chazapis
    for k, v in groups.iteritems():
160 e7b51248 Sofia Papagiannaki
        k = smart_str(k, strings_only=True)
161 e7b51248 Sofia Papagiannaki
        k = format_header_key('X-Account-Group-' + k)
162 e7b51248 Sofia Papagiannaki
        v = smart_str(','.join(v), strings_only=True)
163 e7b51248 Sofia Papagiannaki
        response[k] = v
164 647a5f48 Antony Chazapis
    for k, v in policy.iteritems():
165 647a5f48 Antony Chazapis
        response[smart_str(format_header_key('X-Account-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
166 647a5f48 Antony Chazapis
167 02c0c3fa Antony Chazapis
def get_container_headers(request):
168 02c0c3fa Antony Chazapis
    meta = get_header_prefix(request, 'X-Container-Meta-')
169 62bf8157 Antony Chazapis
    check_meta_headers(meta)
170 3ab38c43 Antony Chazapis
    policy = dict([(k[19:].lower(), v.replace(' ', '')) for k, v in get_header_prefix(request, 'X-Container-Policy-').iteritems()])
171 3ab38c43 Antony Chazapis
    return meta, policy
172 b956618e Antony Chazapis
173 39593b2b Giorgos Verigakis
def put_container_headers(request, response, meta, policy):
174 f6c97079 Antony Chazapis
    if 'count' in meta:
175 f6c97079 Antony Chazapis
        response['X-Container-Object-Count'] = meta['count']
176 f6c97079 Antony Chazapis
    if 'bytes' in meta:
177 f6c97079 Antony Chazapis
        response['X-Container-Bytes-Used'] = meta['bytes']
178 58a6c894 Antony Chazapis
    response['Last-Modified'] = http_date(int(meta['modified']))
179 b956618e Antony Chazapis
    for k in [x for x in meta.keys() if x.startswith('X-Container-Meta-')]:
180 e7b51248 Sofia Papagiannaki
        response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
181 e7b51248 Sofia Papagiannaki
    l = [smart_str(x, strings_only=True) for x in meta['object_meta'] if x.startswith('X-Object-Meta-')]
182 e7b51248 Sofia Papagiannaki
    response['X-Container-Object-Meta'] = ','.join([x[14:] for x in l])
183 39593b2b Giorgos Verigakis
    response['X-Container-Block-Size'] = request.backend.block_size
184 39593b2b Giorgos Verigakis
    response['X-Container-Block-Hash'] = request.backend.hash_algorithm
185 83dd59c5 Antony Chazapis
    if 'until_timestamp' in meta:
186 83dd59c5 Antony Chazapis
        response['X-Container-Until-Timestamp'] = http_date(int(meta['until_timestamp']))
187 3ab38c43 Antony Chazapis
    for k, v in policy.iteritems():
188 e7b51248 Sofia Papagiannaki
        response[smart_str(format_header_key('X-Container-Policy-' + k), strings_only=True)] = smart_str(v, strings_only=True)
189 b956618e Antony Chazapis
190 02c0c3fa Antony Chazapis
def get_object_headers(request):
191 66ce2ca5 Antony Chazapis
    content_type = request.META.get('CONTENT_TYPE', None)
192 02c0c3fa Antony Chazapis
    meta = get_header_prefix(request, 'X-Object-Meta-')
193 62bf8157 Antony Chazapis
    check_meta_headers(meta)
194 b956618e Antony Chazapis
    if request.META.get('HTTP_CONTENT_ENCODING'):
195 b956618e Antony Chazapis
        meta['Content-Encoding'] = request.META['HTTP_CONTENT_ENCODING']
196 22dab079 Antony Chazapis
    if request.META.get('HTTP_CONTENT_DISPOSITION'):
197 22dab079 Antony Chazapis
        meta['Content-Disposition'] = request.META['HTTP_CONTENT_DISPOSITION']
198 b956618e Antony Chazapis
    if request.META.get('HTTP_X_OBJECT_MANIFEST'):
199 b956618e Antony Chazapis
        meta['X-Object-Manifest'] = request.META['HTTP_X_OBJECT_MANIFEST']
200 66ce2ca5 Antony Chazapis
    return content_type, meta, get_sharing(request), get_public(request)
201 b956618e Antony Chazapis
202 3ab38c43 Antony Chazapis
def put_object_headers(response, meta, restricted=False):
203 33b4e4a6 Antony Chazapis
    response['ETag'] = meta['checksum']
204 b956618e Antony Chazapis
    response['Content-Length'] = meta['bytes']
205 66ce2ca5 Antony Chazapis
    response['Content-Type'] = meta.get('type', 'application/octet-stream')
206 b956618e Antony Chazapis
    response['Last-Modified'] = http_date(int(meta['modified']))
207 3ab38c43 Antony Chazapis
    if not restricted:
208 4a1c29ea Antony Chazapis
        response['X-Object-Hash'] = meta['hash']
209 37bee317 Antony Chazapis
        response['X-Object-UUID'] = meta['uuid']
210 72c3ba3f Antony Chazapis
        response['X-Object-Modified-By'] = smart_str(meta['modified_by'], strings_only=True)
211 7bef5750 Antony Chazapis
        response['X-Object-Version'] = meta['version']
212 104626e3 Antony Chazapis
        response['X-Object-Version-Timestamp'] = http_date(int(meta['version_timestamp']))
213 7bef5750 Antony Chazapis
        for k in [x for x in meta.keys() if x.startswith('X-Object-Meta-')]:
214 e7b51248 Sofia Papagiannaki
            response[smart_str(k, strings_only=True)] = smart_str(meta[k], strings_only=True)
215 067cf1fc Antony Chazapis
        for k in ('Content-Encoding', 'Content-Disposition', 'X-Object-Manifest',
216 067cf1fc Antony Chazapis
                  'X-Object-Sharing', 'X-Object-Shared-By', 'X-Object-Allowed-To',
217 067cf1fc Antony Chazapis
                  'X-Object-Public'):
218 7bef5750 Antony Chazapis
            if k in meta:
219 e7b51248 Sofia Papagiannaki
                response[k] = smart_str(meta[k], strings_only=True)
220 7bef5750 Antony Chazapis
    else:
221 c9af0703 Antony Chazapis
        for k in ('Content-Encoding', 'Content-Disposition'):
222 7bef5750 Antony Chazapis
            if k in meta:
223 e8d003e8 Antony Chazapis
                response[k] = smart_str(meta[k], strings_only=True)
224 b956618e Antony Chazapis
225 8cb45c13 Antony Chazapis
def update_manifest_meta(request, v_account, meta):
226 8cb45c13 Antony Chazapis
    """Update metadata if the object has an X-Object-Manifest."""
227 8cb45c13 Antony Chazapis
    
228 8cb45c13 Antony Chazapis
    if 'X-Object-Manifest' in meta:
229 4a1c29ea Antony Chazapis
        etag = ''
230 8cb45c13 Antony Chazapis
        bytes = 0
231 8cb45c13 Antony Chazapis
        try:
232 6d817842 Antony Chazapis
            src_container, src_name = split_container_object_string('/' + meta['X-Object-Manifest'])
233 61efb530 Antony Chazapis
            objects = request.backend.list_objects(request.user_uniq, v_account,
234 39593b2b Giorgos Verigakis
                                src_container, prefix=src_name, virtual=False)
235 8cb45c13 Antony Chazapis
            for x in objects:
236 61efb530 Antony Chazapis
                src_meta = request.backend.get_object_meta(request.user_uniq,
237 91817b22 chazapis
                                        v_account, src_container, x[0], 'pithos', x[1])
238 33b4e4a6 Antony Chazapis
                etag += src_meta['checksum']
239 8cb45c13 Antony Chazapis
                bytes += src_meta['bytes']
240 8cb45c13 Antony Chazapis
        except:
241 8cb45c13 Antony Chazapis
            # Ignore errors.
242 8cb45c13 Antony Chazapis
            return
243 8cb45c13 Antony Chazapis
        meta['bytes'] = bytes
244 8cb45c13 Antony Chazapis
        md5 = hashlib.md5()
245 4a1c29ea Antony Chazapis
        md5.update(etag)
246 33b4e4a6 Antony Chazapis
        meta['checksum'] = md5.hexdigest().lower()
247 8cb45c13 Antony Chazapis
248 067cf1fc Antony Chazapis
def update_sharing_meta(request, permissions, v_account, v_container, v_object, meta):
249 cca6c617 Antony Chazapis
    if permissions is None:
250 cca6c617 Antony Chazapis
        return
251 067cf1fc Antony Chazapis
    allowed, perm_path, perms = permissions
252 cca6c617 Antony Chazapis
    if len(perms) == 0:
253 cca6c617 Antony Chazapis
        return
254 3436eeb0 Antony Chazapis
    ret = []
255 cca6c617 Antony Chazapis
    r = ','.join(perms.get('read', []))
256 3436eeb0 Antony Chazapis
    if r:
257 3436eeb0 Antony Chazapis
        ret.append('read=' + r)
258 cca6c617 Antony Chazapis
    w = ','.join(perms.get('write', []))
259 3436eeb0 Antony Chazapis
    if w:
260 3436eeb0 Antony Chazapis
        ret.append('write=' + w)
261 cca6c617 Antony Chazapis
    meta['X-Object-Sharing'] = '; '.join(ret)
262 cca6c617 Antony Chazapis
    if '/'.join((v_account, v_container, v_object)) != perm_path:
263 cca6c617 Antony Chazapis
        meta['X-Object-Shared-By'] = perm_path
264 61efb530 Antony Chazapis
    if request.user_uniq != v_account:
265 067cf1fc Antony Chazapis
        meta['X-Object-Allowed-To'] = allowed
266 3436eeb0 Antony Chazapis
267 e0f916bb Antony Chazapis
def update_public_meta(public, meta):
268 e0f916bb Antony Chazapis
    if not public:
269 e0f916bb Antony Chazapis
        return
270 bb4eafc6 Antony Chazapis
    meta['X-Object-Public'] = '/public/' + encode_url(public)
271 e0f916bb Antony Chazapis
272 b956618e Antony Chazapis
def validate_modification_preconditions(request, meta):
273 58a6c894 Antony Chazapis
    """Check that the modified timestamp conforms with the preconditions set."""
274 83dd59c5 Antony Chazapis
    
275 22dab079 Antony Chazapis
    if 'modified' not in meta:
276 22dab079 Antony Chazapis
        return # TODO: Always return?
277 22dab079 Antony Chazapis
    
278 b956618e Antony Chazapis
    if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')
279 b956618e Antony Chazapis
    if if_modified_since is not None:
280 b956618e Antony Chazapis
        if_modified_since = parse_http_date_safe(if_modified_since)
281 22dab079 Antony Chazapis
    if if_modified_since is not None and int(meta['modified']) <= if_modified_since:
282 3e5d2f38 Antony Chazapis
        raise NotModified('Resource has not been modified')
283 b956618e Antony Chazapis
    
284 b956618e Antony Chazapis
    if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')
285 b956618e Antony Chazapis
    if if_unmodified_since is not None:
286 b956618e Antony Chazapis
        if_unmodified_since = parse_http_date_safe(if_unmodified_since)
287 22dab079 Antony Chazapis
    if if_unmodified_since is not None and int(meta['modified']) > if_unmodified_since:
288 3e5d2f38 Antony Chazapis
        raise PreconditionFailed('Resource has been modified')
289 b956618e Antony Chazapis
290 22dab079 Antony Chazapis
def validate_matching_preconditions(request, meta):
291 58a6c894 Antony Chazapis
    """Check that the ETag conforms with the preconditions set."""
292 83dd59c5 Antony Chazapis
    
293 33b4e4a6 Antony Chazapis
    etag = meta['checksum']
294 33b4e4a6 Antony Chazapis
    if not etag:
295 33b4e4a6 Antony Chazapis
        etag = None
296 22dab079 Antony Chazapis
    
297 22dab079 Antony Chazapis
    if_match = request.META.get('HTTP_IF_MATCH')
298 a8326bef Antony Chazapis
    if if_match is not None:
299 4a1c29ea Antony Chazapis
        if etag is None:
300 a8326bef Antony Chazapis
            raise PreconditionFailed('Resource does not exist')
301 4a1c29ea Antony Chazapis
        if if_match != '*' and etag not in [x.lower() for x in parse_etags(if_match)]:
302 a8326bef Antony Chazapis
            raise PreconditionFailed('Resource ETag does not match')
303 22dab079 Antony Chazapis
    
304 22dab079 Antony Chazapis
    if_none_match = request.META.get('HTTP_IF_NONE_MATCH')
305 22dab079 Antony Chazapis
    if if_none_match is not None:
306 a8326bef Antony Chazapis
        # TODO: If this passes, must ignore If-Modified-Since header.
307 4a1c29ea Antony Chazapis
        if etag is not None:
308 4a1c29ea Antony Chazapis
            if if_none_match == '*' or etag in [x.lower() for x in parse_etags(if_none_match)]:
309 a8326bef Antony Chazapis
                # TODO: Continue if an If-Modified-Since header is present.
310 a8326bef Antony Chazapis
                if request.method in ('HEAD', 'GET'):
311 a8326bef Antony Chazapis
                    raise NotModified('Resource ETag matches')
312 a8326bef Antony Chazapis
                raise PreconditionFailed('Resource exists or ETag matches')
313 22dab079 Antony Chazapis
314 83dd59c5 Antony Chazapis
def split_container_object_string(s):
315 6d817842 Antony Chazapis
    if not len(s) > 0 or s[0] != '/':
316 6d817842 Antony Chazapis
        raise ValueError
317 6d817842 Antony Chazapis
    s = s[1:]
318 8cb45c13 Antony Chazapis
    pos = s.find('/')
319 22d7b01e Antony Chazapis
    if pos == -1 or pos == len(s) - 1:
320 83dd59c5 Antony Chazapis
        raise ValueError
321 8cb45c13 Antony Chazapis
    return s[:pos], s[(pos + 1):]
322 83dd59c5 Antony Chazapis
323 4d15c94e Sofia Papagiannaki
def copy_or_move_object(request, src_account, src_container, src_name, dest_account, dest_container, dest_name, move=False, delimiter=None):
324 58a6c894 Antony Chazapis
    """Copy or move an object."""
325 22dab079 Antony Chazapis
    
326 53cff70c Antony Chazapis
    if 'ignore_content_type' in request.GET and 'CONTENT_TYPE' in request.META:
327 53cff70c Antony Chazapis
        del(request.META['CONTENT_TYPE'])
328 66ce2ca5 Antony Chazapis
    content_type, meta, permissions, public = get_object_headers(request)
329 79bb41b7 Antony Chazapis
    src_version = request.META.get('HTTP_X_SOURCE_VERSION')
330 b956618e Antony Chazapis
    try:
331 b956618e Antony Chazapis
        if move:
332 61efb530 Antony Chazapis
            version_id = request.backend.move_object(request.user_uniq, src_account, src_container, src_name,
333 79bb41b7 Antony Chazapis
                                                        dest_account, dest_container, dest_name,
334 4d15c94e Sofia Papagiannaki
                                                        content_type, 'pithos', meta, False, permissions, delimiter)
335 b956618e Antony Chazapis
        else:
336 61efb530 Antony Chazapis
            version_id = request.backend.copy_object(request.user_uniq, src_account, src_container, src_name,
337 79bb41b7 Antony Chazapis
                                                        dest_account, dest_container, dest_name,
338 4d15c94e Sofia Papagiannaki
                                                        content_type, 'pithos', meta, False, permissions, src_version, delimiter)
339 cca6c617 Antony Chazapis
    except NotAllowedError:
340 297513ba Antony Chazapis
        raise Forbidden('Not allowed')
341 a8326bef Antony Chazapis
    except (NameError, IndexError):
342 b956618e Antony Chazapis
        raise ItemNotFound('Container or object does not exist')
343 3436eeb0 Antony Chazapis
    except ValueError:
344 3436eeb0 Antony Chazapis
        raise BadRequest('Invalid sharing header')
345 5df6c6d1 Antony Chazapis
    except QuotaError:
346 5df6c6d1 Antony Chazapis
        raise RequestEntityTooLarge('Quota exceeded')
347 e0f916bb Antony Chazapis
    if public is not None:
348 e0f916bb Antony Chazapis
        try:
349 61efb530 Antony Chazapis
            request.backend.update_object_public(request.user_uniq, dest_account, dest_container, dest_name, public)
350 e0f916bb Antony Chazapis
        except NotAllowedError:
351 297513ba Antony Chazapis
            raise Forbidden('Not allowed')
352 e0f916bb Antony Chazapis
        except NameError:
353 e0f916bb Antony Chazapis
            raise ItemNotFound('Object does not exist')
354 7dd293a0 Antony Chazapis
    return version_id
355 b956618e Antony Chazapis
356 1495b972 Antony Chazapis
def get_int_parameter(p):
357 83dd59c5 Antony Chazapis
    if p is not None:
358 58a6c894 Antony Chazapis
        try:
359 83dd59c5 Antony Chazapis
            p = int(p)
360 58a6c894 Antony Chazapis
        except ValueError:
361 58a6c894 Antony Chazapis
            return None
362 83dd59c5 Antony Chazapis
        if p < 0:
363 58a6c894 Antony Chazapis
            return None
364 83dd59c5 Antony Chazapis
    return p
365 58a6c894 Antony Chazapis
366 22dab079 Antony Chazapis
def get_content_length(request):
367 1495b972 Antony Chazapis
    content_length = get_int_parameter(request.META.get('CONTENT_LENGTH'))
368 1495b972 Antony Chazapis
    if content_length is None:
369 1495b972 Antony Chazapis
        raise LengthRequired('Missing or invalid Content-Length header')
370 22dab079 Antony Chazapis
    return content_length
371 22dab079 Antony Chazapis
372 22dab079 Antony Chazapis
def get_range(request, size):
373 58a6c894 Antony Chazapis
    """Parse a Range header from the request.
374 b956618e Antony Chazapis
    
375 22dab079 Antony Chazapis
    Either returns None, when the header is not existent or should be ignored,
376 22dab079 Antony Chazapis
    or a list of (offset, length) tuples - should be further checked.
377 b956618e Antony Chazapis
    """
378 83dd59c5 Antony Chazapis
    
379 22dab079 Antony Chazapis
    ranges = request.META.get('HTTP_RANGE', '').replace(' ', '')
380 22dab079 Antony Chazapis
    if not ranges.startswith('bytes='):
381 b956618e Antony Chazapis
        return None
382 b956618e Antony Chazapis
    
383 22dab079 Antony Chazapis
    ret = []
384 22dab079 Antony Chazapis
    for r in (x.strip() for x in ranges[6:].split(',')):
385 22dab079 Antony Chazapis
        p = re.compile('^(?P<offset>\d*)-(?P<upto>\d*)$')
386 22dab079 Antony Chazapis
        m = p.match(r)
387 22dab079 Antony Chazapis
        if not m:
388 22dab079 Antony Chazapis
            return None
389 22dab079 Antony Chazapis
        offset = m.group('offset')
390 22dab079 Antony Chazapis
        upto = m.group('upto')
391 22dab079 Antony Chazapis
        if offset == '' and upto == '':
392 b956618e Antony Chazapis
            return None
393 b956618e Antony Chazapis
        
394 22dab079 Antony Chazapis
        if offset != '':
395 22dab079 Antony Chazapis
            offset = int(offset)
396 22dab079 Antony Chazapis
            if upto != '':
397 b956618e Antony Chazapis
                upto = int(upto)
398 22dab079 Antony Chazapis
                if offset > upto:
399 22dab079 Antony Chazapis
                    return None
400 22dab079 Antony Chazapis
                ret.append((offset, upto - offset + 1))
401 22dab079 Antony Chazapis
            else:
402 22dab079 Antony Chazapis
                ret.append((offset, size - offset))
403 b956618e Antony Chazapis
        else:
404 22dab079 Antony Chazapis
            length = int(upto)
405 22dab079 Antony Chazapis
            ret.append((size - length, length))
406 22dab079 Antony Chazapis
    
407 22dab079 Antony Chazapis
    return ret
408 22dab079 Antony Chazapis
409 22dab079 Antony Chazapis
def get_content_range(request):
410 58a6c894 Antony Chazapis
    """Parse a Content-Range header from the request.
411 22dab079 Antony Chazapis
    
412 22dab079 Antony Chazapis
    Either returns None, when the header is not existent or should be ignored,
413 22dab079 Antony Chazapis
    or an (offset, length, total) tuple - check as length, total may be None.
414 22dab079 Antony Chazapis
    Returns (None, None, None) if the provided range is '*/*'.
415 22dab079 Antony Chazapis
    """
416 22dab079 Antony Chazapis
    
417 22dab079 Antony Chazapis
    ranges = request.META.get('HTTP_CONTENT_RANGE', '')
418 22dab079 Antony Chazapis
    if not ranges:
419 22dab079 Antony Chazapis
        return None
420 22dab079 Antony Chazapis
    
421 22dab079 Antony Chazapis
    p = re.compile('^bytes (?P<offset>\d+)-(?P<upto>\d*)/(?P<total>(\d+|\*))$')
422 22dab079 Antony Chazapis
    m = p.match(ranges)
423 22dab079 Antony Chazapis
    if not m:
424 22dab079 Antony Chazapis
        if ranges == 'bytes */*':
425 22dab079 Antony Chazapis
            return (None, None, None)
426 22dab079 Antony Chazapis
        return None
427 22dab079 Antony Chazapis
    offset = int(m.group('offset'))
428 22dab079 Antony Chazapis
    upto = m.group('upto')
429 22dab079 Antony Chazapis
    total = m.group('total')
430 22dab079 Antony Chazapis
    if upto != '':
431 22dab079 Antony Chazapis
        upto = int(upto)
432 b956618e Antony Chazapis
    else:
433 22dab079 Antony Chazapis
        upto = None
434 22dab079 Antony Chazapis
    if total != '*':
435 22dab079 Antony Chazapis
        total = int(total)
436 22dab079 Antony Chazapis
    else:
437 22dab079 Antony Chazapis
        total = None
438 70e526a0 Antony Chazapis
    if (upto is not None and offset > upto) or \
439 70e526a0 Antony Chazapis
        (total is not None and offset >= total) or \
440 70e526a0 Antony Chazapis
        (total is not None and upto is not None and upto >= total):
441 22dab079 Antony Chazapis
        return None
442 22dab079 Antony Chazapis
    
443 70e526a0 Antony Chazapis
    if upto is None:
444 22dab079 Antony Chazapis
        length = None
445 22dab079 Antony Chazapis
    else:
446 22dab079 Antony Chazapis
        length = upto - offset + 1
447 22dab079 Antony Chazapis
    return (offset, length, total)
448 b956618e Antony Chazapis
449 3436eeb0 Antony Chazapis
def get_sharing(request):
450 3436eeb0 Antony Chazapis
    """Parse an X-Object-Sharing header from the request.
451 3436eeb0 Antony Chazapis
    
452 3436eeb0 Antony Chazapis
    Raises BadRequest on error.
453 3436eeb0 Antony Chazapis
    """
454 3436eeb0 Antony Chazapis
    
455 3436eeb0 Antony Chazapis
    permissions = request.META.get('HTTP_X_OBJECT_SHARING')
456 cca6c617 Antony Chazapis
    if permissions is None:
457 3436eeb0 Antony Chazapis
        return None
458 3436eeb0 Antony Chazapis
    
459 0a7f1671 Antony Chazapis
    # TODO: Document or remove '~' replacing.
460 0a7f1671 Antony Chazapis
    permissions = permissions.replace('~', '')
461 0a7f1671 Antony Chazapis
    
462 3436eeb0 Antony Chazapis
    ret = {}
463 cca6c617 Antony Chazapis
    permissions = permissions.replace(' ', '')
464 cca6c617 Antony Chazapis
    if permissions == '':
465 cca6c617 Antony Chazapis
        return ret
466 cca6c617 Antony Chazapis
    for perm in (x for x in permissions.split(';')):
467 cca6c617 Antony Chazapis
        if perm.startswith('read='):
468 b0a2d1a6 Antony Chazapis
            ret['read'] = list(set([v.replace(' ','').lower() for v in perm[5:].split(',')]))
469 02c0c3fa Antony Chazapis
            if '' in ret['read']:
470 02c0c3fa Antony Chazapis
                ret['read'].remove('')
471 e8886082 Antony Chazapis
            if '*' in ret['read']:
472 e8886082 Antony Chazapis
                ret['read'] = ['*']
473 3436eeb0 Antony Chazapis
            if len(ret['read']) == 0:
474 3436eeb0 Antony Chazapis
                raise BadRequest('Bad X-Object-Sharing header value')
475 3436eeb0 Antony Chazapis
        elif perm.startswith('write='):
476 b0a2d1a6 Antony Chazapis
            ret['write'] = list(set([v.replace(' ','').lower() for v in perm[6:].split(',')]))
477 02c0c3fa Antony Chazapis
            if '' in ret['write']:
478 02c0c3fa Antony Chazapis
                ret['write'].remove('')
479 e8886082 Antony Chazapis
            if '*' in ret['write']:
480 e8886082 Antony Chazapis
                ret['write'] = ['*']
481 3436eeb0 Antony Chazapis
            if len(ret['write']) == 0:
482 3436eeb0 Antony Chazapis
                raise BadRequest('Bad X-Object-Sharing header value')
483 3436eeb0 Antony Chazapis
        else:
484 3436eeb0 Antony Chazapis
            raise BadRequest('Bad X-Object-Sharing header value')
485 21a8a6ff Antony Chazapis
    
486 21a8a6ff Antony Chazapis
    # Keep duplicates only in write list.
487 21a8a6ff Antony Chazapis
    dups = [x for x in ret.get('read', []) if x in ret.get('write', []) and x != '*']
488 21a8a6ff Antony Chazapis
    if dups:
489 21a8a6ff Antony Chazapis
        for x in dups:
490 21a8a6ff Antony Chazapis
            ret['read'].remove(x)
491 21a8a6ff Antony Chazapis
        if len(ret['read']) == 0:
492 21a8a6ff Antony Chazapis
            del(ret['read'])
493 21a8a6ff Antony Chazapis
    
494 3436eeb0 Antony Chazapis
    return ret
495 3436eeb0 Antony Chazapis
496 3ab38c43 Antony Chazapis
def get_public(request):
497 3ab38c43 Antony Chazapis
    """Parse an X-Object-Public header from the request.
498 3ab38c43 Antony Chazapis
    
499 3ab38c43 Antony Chazapis
    Raises BadRequest on error.
500 3ab38c43 Antony Chazapis
    """
501 3ab38c43 Antony Chazapis
    
502 3ab38c43 Antony Chazapis
    public = request.META.get('HTTP_X_OBJECT_PUBLIC')
503 3ab38c43 Antony Chazapis
    if public is None:
504 3ab38c43 Antony Chazapis
        return None
505 3ab38c43 Antony Chazapis
    
506 3ab38c43 Antony Chazapis
    public = public.replace(' ', '').lower()
507 3ab38c43 Antony Chazapis
    if public == 'true':
508 3ab38c43 Antony Chazapis
        return True
509 3ab38c43 Antony Chazapis
    elif public == 'false' or public == '':
510 3ab38c43 Antony Chazapis
        return False
511 3ab38c43 Antony Chazapis
    raise BadRequest('Bad X-Object-Public header value')
512 3ab38c43 Antony Chazapis
513 b956618e Antony Chazapis
def raw_input_socket(request):
514 58a6c894 Antony Chazapis
    """Return the socket for reading the rest of the request."""
515 83dd59c5 Antony Chazapis
    
516 b956618e Antony Chazapis
    server_software = request.META.get('SERVER_SOFTWARE')
517 fc1b2a75 Antony Chazapis
    if server_software and server_software.startswith('mod_python'):
518 b956618e Antony Chazapis
        return request._req
519 fc1b2a75 Antony Chazapis
    if 'wsgi.input' in request.environ:
520 fc1b2a75 Antony Chazapis
        return request.environ['wsgi.input']
521 08de868d Antony Chazapis
    raise NotImplemented('Unknown server software')
522 b956618e Antony Chazapis
523 07fea84b Antony Chazapis
MAX_UPLOAD_SIZE = 5 * (1024 * 1024 * 1024) # 5GB
524 b956618e Antony Chazapis
525 c032f34d Antony Chazapis
def socket_read_iterator(request, length=0, blocksize=4096):
526 58a6c894 Antony Chazapis
    """Return a maximum of blocksize data read from the socket in each iteration.
527 b956618e Antony Chazapis
    
528 22dab079 Antony Chazapis
    Read up to 'length'. If 'length' is negative, will attempt a chunked read.
529 b956618e Antony Chazapis
    The maximum ammount of data read is controlled by MAX_UPLOAD_SIZE.
530 b956618e Antony Chazapis
    """
531 83dd59c5 Antony Chazapis
    
532 c032f34d Antony Chazapis
    sock = raw_input_socket(request)
533 b956618e Antony Chazapis
    if length < 0: # Chunked transfers
534 c032f34d Antony Chazapis
        # Small version (server does the dechunking).
535 032dc768 Antony Chazapis
        if request.environ.get('mod_wsgi.input_chunked', None) or request.META['SERVER_SOFTWARE'].startswith('gunicorn'):
536 c032f34d Antony Chazapis
            while length < MAX_UPLOAD_SIZE:
537 c032f34d Antony Chazapis
                data = sock.read(blocksize)
538 c032f34d Antony Chazapis
                if data == '':
539 c032f34d Antony Chazapis
                    return
540 c032f34d Antony Chazapis
                yield data
541 c032f34d Antony Chazapis
            raise BadRequest('Maximum size is reached')
542 c032f34d Antony Chazapis
        
543 c032f34d Antony Chazapis
        # Long version (do the dechunking).
544 22dab079 Antony Chazapis
        data = ''
545 b956618e Antony Chazapis
        while length < MAX_UPLOAD_SIZE:
546 22dab079 Antony Chazapis
            # Get chunk size.
547 22dab079 Antony Chazapis
            if hasattr(sock, 'readline'):
548 22dab079 Antony Chazapis
                chunk_length = sock.readline()
549 22dab079 Antony Chazapis
            else:
550 22dab079 Antony Chazapis
                chunk_length = ''
551 22dab079 Antony Chazapis
                while chunk_length[-1:] != '\n':
552 22dab079 Antony Chazapis
                    chunk_length += sock.read(1)
553 22dab079 Antony Chazapis
                chunk_length.strip()
554 b956618e Antony Chazapis
            pos = chunk_length.find(';')
555 b956618e Antony Chazapis
            if pos >= 0:
556 b956618e Antony Chazapis
                chunk_length = chunk_length[:pos]
557 b956618e Antony Chazapis
            try:
558 b956618e Antony Chazapis
                chunk_length = int(chunk_length, 16)
559 b956618e Antony Chazapis
            except Exception, e:
560 b956618e Antony Chazapis
                raise BadRequest('Bad chunk size') # TODO: Change to something more appropriate.
561 22dab079 Antony Chazapis
            # Check if done.
562 b956618e Antony Chazapis
            if chunk_length == 0:
563 22dab079 Antony Chazapis
                if len(data) > 0:
564 22dab079 Antony Chazapis
                    yield data
565 b956618e Antony Chazapis
                return
566 22dab079 Antony Chazapis
            # Get the actual data.
567 b956618e Antony Chazapis
            while chunk_length > 0:
568 22dab079 Antony Chazapis
                chunk = sock.read(min(chunk_length, blocksize))
569 22dab079 Antony Chazapis
                chunk_length -= len(chunk)
570 623a0cf4 Sofia Papagiannaki
                if length > 0:
571 623a0cf4 Sofia Papagiannaki
                    length += len(chunk)
572 22dab079 Antony Chazapis
                data += chunk
573 22dab079 Antony Chazapis
                if len(data) >= blocksize:
574 22dab079 Antony Chazapis
                    ret = data[:blocksize]
575 22dab079 Antony Chazapis
                    data = data[blocksize:]
576 22dab079 Antony Chazapis
                    yield ret
577 22dab079 Antony Chazapis
            sock.read(2) # CRLF
578 32a437b1 Sofia Papagiannaki
        raise BadRequest('Maximum size is reached')
579 b956618e Antony Chazapis
    else:
580 b956618e Antony Chazapis
        if length > MAX_UPLOAD_SIZE:
581 32a437b1 Sofia Papagiannaki
            raise BadRequest('Maximum size is reached')
582 b956618e Antony Chazapis
        while length > 0:
583 b956618e Antony Chazapis
            data = sock.read(min(length, blocksize))
584 7b25e082 Antony Chazapis
            if not data:
585 7b25e082 Antony Chazapis
                raise BadRequest()
586 b956618e Antony Chazapis
            length -= len(data)
587 b956618e Antony Chazapis
            yield data
588 b956618e Antony Chazapis
589 817890f2 Antony Chazapis
class SaveToBackendHandler(FileUploadHandler):
590 817890f2 Antony Chazapis
    """Handle a file from an HTML form the django way."""
591 817890f2 Antony Chazapis
    
592 817890f2 Antony Chazapis
    def __init__(self, request=None):
593 817890f2 Antony Chazapis
        super(SaveToBackendHandler, self).__init__(request)
594 817890f2 Antony Chazapis
        self.backend = request.backend
595 817890f2 Antony Chazapis
    
596 817890f2 Antony Chazapis
    def put_data(self, length):
597 817890f2 Antony Chazapis
        if len(self.data) >= length:
598 817890f2 Antony Chazapis
            block = self.data[:length]
599 817890f2 Antony Chazapis
            self.file.hashmap.append(self.backend.put_block(block))
600 817890f2 Antony Chazapis
            self.md5.update(block)
601 817890f2 Antony Chazapis
            self.data = self.data[length:]
602 817890f2 Antony Chazapis
    
603 817890f2 Antony Chazapis
    def new_file(self, field_name, file_name, content_type, content_length, charset=None):
604 817890f2 Antony Chazapis
        self.md5 = hashlib.md5()        
605 817890f2 Antony Chazapis
        self.data = ''
606 817890f2 Antony Chazapis
        self.file = UploadedFile(name=file_name, content_type=content_type, charset=charset)
607 817890f2 Antony Chazapis
        self.file.size = 0
608 817890f2 Antony Chazapis
        self.file.hashmap = []
609 817890f2 Antony Chazapis
    
610 817890f2 Antony Chazapis
    def receive_data_chunk(self, raw_data, start):
611 817890f2 Antony Chazapis
        self.data += raw_data
612 817890f2 Antony Chazapis
        self.file.size += len(raw_data)
613 817890f2 Antony Chazapis
        self.put_data(self.request.backend.block_size)
614 817890f2 Antony Chazapis
        return None
615 817890f2 Antony Chazapis
    
616 817890f2 Antony Chazapis
    def file_complete(self, file_size):
617 817890f2 Antony Chazapis
        l = len(self.data)
618 817890f2 Antony Chazapis
        if l > 0:
619 817890f2 Antony Chazapis
            self.put_data(l)
620 817890f2 Antony Chazapis
        self.file.etag = self.md5.hexdigest().lower()
621 817890f2 Antony Chazapis
        return self.file
622 817890f2 Antony Chazapis
623 22dab079 Antony Chazapis
class ObjectWrapper(object):
624 58a6c894 Antony Chazapis
    """Return the object's data block-per-block in each iteration.
625 22dab079 Antony Chazapis
    
626 22dab079 Antony Chazapis
    Read from the object using the offset and length provided in each entry of the range list.
627 22dab079 Antony Chazapis
    """
628 22dab079 Antony Chazapis
    
629 39593b2b Giorgos Verigakis
    def __init__(self, backend, ranges, sizes, hashmaps, boundary):
630 39593b2b Giorgos Verigakis
        self.backend = backend
631 22dab079 Antony Chazapis
        self.ranges = ranges
632 8cb45c13 Antony Chazapis
        self.sizes = sizes
633 8cb45c13 Antony Chazapis
        self.hashmaps = hashmaps
634 22dab079 Antony Chazapis
        self.boundary = boundary
635 8cb45c13 Antony Chazapis
        self.size = sum(self.sizes)
636 22dab079 Antony Chazapis
        
637 8cb45c13 Antony Chazapis
        self.file_index = 0
638 8cb45c13 Antony Chazapis
        self.block_index = 0
639 8cb45c13 Antony Chazapis
        self.block_hash = -1
640 22dab079 Antony Chazapis
        self.block = ''
641 22dab079 Antony Chazapis
        
642 22dab079 Antony Chazapis
        self.range_index = -1
643 22dab079 Antony Chazapis
        self.offset, self.length = self.ranges[0]
644 22dab079 Antony Chazapis
    
645 22dab079 Antony Chazapis
    def __iter__(self):
646 22dab079 Antony Chazapis
        return self
647 22dab079 Antony Chazapis
    
648 22dab079 Antony Chazapis
    def part_iterator(self):
649 22dab079 Antony Chazapis
        if self.length > 0:
650 8cb45c13 Antony Chazapis
            # Get the file for the current offset.
651 8cb45c13 Antony Chazapis
            file_size = self.sizes[self.file_index]
652 8cb45c13 Antony Chazapis
            while self.offset >= file_size:
653 8cb45c13 Antony Chazapis
                self.offset -= file_size
654 8cb45c13 Antony Chazapis
                self.file_index += 1
655 8cb45c13 Antony Chazapis
                file_size = self.sizes[self.file_index]
656 8cb45c13 Antony Chazapis
            
657 8cb45c13 Antony Chazapis
            # Get the block for the current position.
658 39593b2b Giorgos Verigakis
            self.block_index = int(self.offset / self.backend.block_size)
659 8cb45c13 Antony Chazapis
            if self.block_hash != self.hashmaps[self.file_index][self.block_index]:
660 8cb45c13 Antony Chazapis
                self.block_hash = self.hashmaps[self.file_index][self.block_index]
661 22dab079 Antony Chazapis
                try:
662 39593b2b Giorgos Verigakis
                    self.block = self.backend.get_block(self.block_hash)
663 22dab079 Antony Chazapis
                except NameError:
664 22dab079 Antony Chazapis
                    raise ItemNotFound('Block does not exist')
665 8cb45c13 Antony Chazapis
            
666 22dab079 Antony Chazapis
            # Get the data from the block.
667 39593b2b Giorgos Verigakis
            bo = self.offset % self.backend.block_size
668 c9865fe1 Antony Chazapis
            bs = self.backend.block_size
669 45cf0bc8 Antony Chazapis
            if (self.block_index == len(self.hashmaps[self.file_index]) - 1 and
670 45cf0bc8 Antony Chazapis
                self.sizes[self.file_index] % self.backend.block_size):
671 c9865fe1 Antony Chazapis
                bs = self.sizes[self.file_index] % self.backend.block_size
672 c9865fe1 Antony Chazapis
            bl = min(self.length, bs - bo)
673 22dab079 Antony Chazapis
            data = self.block[bo:bo + bl]
674 22dab079 Antony Chazapis
            self.offset += bl
675 22dab079 Antony Chazapis
            self.length -= bl
676 22dab079 Antony Chazapis
            return data
677 22dab079 Antony Chazapis
        else:
678 22dab079 Antony Chazapis
            raise StopIteration
679 22dab079 Antony Chazapis
    
680 22dab079 Antony Chazapis
    def next(self):
681 22dab079 Antony Chazapis
        if len(self.ranges) == 1:
682 22dab079 Antony Chazapis
            return self.part_iterator()
683 22dab079 Antony Chazapis
        if self.range_index == len(self.ranges):
684 22dab079 Antony Chazapis
            raise StopIteration
685 22dab079 Antony Chazapis
        try:
686 22dab079 Antony Chazapis
            if self.range_index == -1:
687 22dab079 Antony Chazapis
                raise StopIteration
688 22dab079 Antony Chazapis
            return self.part_iterator()
689 22dab079 Antony Chazapis
        except StopIteration:
690 22dab079 Antony Chazapis
            self.range_index += 1
691 22dab079 Antony Chazapis
            out = []
692 22dab079 Antony Chazapis
            if self.range_index < len(self.ranges):
693 22dab079 Antony Chazapis
                # Part header.
694 22dab079 Antony Chazapis
                self.offset, self.length = self.ranges[self.range_index]
695 8cb45c13 Antony Chazapis
                self.file_index = 0
696 22dab079 Antony Chazapis
                if self.range_index > 0:
697 22dab079 Antony Chazapis
                    out.append('')
698 22dab079 Antony Chazapis
                out.append('--' + self.boundary)
699 22dab079 Antony Chazapis
                out.append('Content-Range: bytes %d-%d/%d' % (self.offset, self.offset + self.length - 1, self.size))
700 22dab079 Antony Chazapis
                out.append('Content-Transfer-Encoding: binary')
701 22dab079 Antony Chazapis
                out.append('')
702 22dab079 Antony Chazapis
                out.append('')
703 22dab079 Antony Chazapis
                return '\r\n'.join(out)
704 22dab079 Antony Chazapis
            else:
705 22dab079 Antony Chazapis
                # Footer.
706 22dab079 Antony Chazapis
                out.append('')
707 22dab079 Antony Chazapis
                out.append('--' + self.boundary + '--')
708 22dab079 Antony Chazapis
                out.append('')
709 22dab079 Antony Chazapis
                return '\r\n'.join(out)
710 22dab079 Antony Chazapis
711 8cb45c13 Antony Chazapis
def object_data_response(request, sizes, hashmaps, meta, public=False):
712 7bef5750 Antony Chazapis
    """Get the HttpResponse object for replying with the object's data."""
713 7bef5750 Antony Chazapis
    
714 7bef5750 Antony Chazapis
    # Range handling.
715 8cb45c13 Antony Chazapis
    size = sum(sizes)
716 7bef5750 Antony Chazapis
    ranges = get_range(request, size)
717 7bef5750 Antony Chazapis
    if ranges is None:
718 7bef5750 Antony Chazapis
        ranges = [(0, size)]
719 7bef5750 Antony Chazapis
        ret = 200
720 7bef5750 Antony Chazapis
    else:
721 7bef5750 Antony Chazapis
        check = [True for offset, length in ranges if
722 7bef5750 Antony Chazapis
                    length <= 0 or length > size or
723 7bef5750 Antony Chazapis
                    offset < 0 or offset >= size or
724 7bef5750 Antony Chazapis
                    offset + length > size]
725 7bef5750 Antony Chazapis
        if len(check) > 0:
726 6d1e6dce Sofia Papagiannaki
            raise RangeNotSatisfiable('Requested range exceeds object limits')
727 7bef5750 Antony Chazapis
        ret = 206
728 15d465b8 Antony Chazapis
        if_range = request.META.get('HTTP_IF_RANGE')
729 15d465b8 Antony Chazapis
        if if_range:
730 6d1e6dce Sofia Papagiannaki
            try:
731 15d465b8 Antony Chazapis
                # Modification time has passed instead.
732 6d1e6dce Sofia Papagiannaki
                last_modified = parse_http_date(if_range)
733 6d1e6dce Sofia Papagiannaki
                if last_modified != meta['modified']:
734 6d1e6dce Sofia Papagiannaki
                    ranges = [(0, size)]
735 6d1e6dce Sofia Papagiannaki
                    ret = 200
736 6d1e6dce Sofia Papagiannaki
            except ValueError:
737 33b4e4a6 Antony Chazapis
                if if_range != meta['checksum']:
738 6d1e6dce Sofia Papagiannaki
                    ranges = [(0, size)]
739 6d1e6dce Sofia Papagiannaki
                    ret = 200
740 7bef5750 Antony Chazapis
    
741 7bef5750 Antony Chazapis
    if ret == 206 and len(ranges) > 1:
742 7bef5750 Antony Chazapis
        boundary = uuid.uuid4().hex
743 7bef5750 Antony Chazapis
    else:
744 7bef5750 Antony Chazapis
        boundary = ''
745 39593b2b Giorgos Verigakis
    wrapper = ObjectWrapper(request.backend, ranges, sizes, hashmaps, boundary)
746 7bef5750 Antony Chazapis
    response = HttpResponse(wrapper, status=ret)
747 02c0c3fa Antony Chazapis
    put_object_headers(response, meta, public)
748 7bef5750 Antony Chazapis
    if ret == 206:
749 7bef5750 Antony Chazapis
        if len(ranges) == 1:
750 7bef5750 Antony Chazapis
            offset, length = ranges[0]
751 7bef5750 Antony Chazapis
            response['Content-Length'] = length # Update with the correct length.
752 7bef5750 Antony Chazapis
            response['Content-Range'] = 'bytes %d-%d/%d' % (offset, offset + length - 1, size)
753 7bef5750 Antony Chazapis
        else:
754 7bef5750 Antony Chazapis
            del(response['Content-Length'])
755 7bef5750 Antony Chazapis
            response['Content-Type'] = 'multipart/byteranges; boundary=%s' % (boundary,)
756 7bef5750 Antony Chazapis
    return response
757 7bef5750 Antony Chazapis
758 39593b2b Giorgos Verigakis
def put_object_block(request, hashmap, data, offset):
759 cb146cf9 Antony Chazapis
    """Put one block of data at the given offset."""
760 cb146cf9 Antony Chazapis
    
761 39593b2b Giorgos Verigakis
    bi = int(offset / request.backend.block_size)
762 39593b2b Giorgos Verigakis
    bo = offset % request.backend.block_size
763 39593b2b Giorgos Verigakis
    bl = min(len(data), request.backend.block_size - bo)
764 cb146cf9 Antony Chazapis
    if bi < len(hashmap):
765 39593b2b Giorgos Verigakis
        hashmap[bi] = request.backend.update_block(hashmap[bi], data[:bl], bo)
766 cb146cf9 Antony Chazapis
    else:
767 39593b2b Giorgos Verigakis
        hashmap.append(request.backend.put_block(('\x00' * bo) + data[:bl]))
768 cb146cf9 Antony Chazapis
    return bl # Return ammount of data written.
769 cb146cf9 Antony Chazapis
770 b3155065 Antony Chazapis
def hashmap_md5(backend, hashmap, size):
771 cddcf432 chazapis
    """Produce the MD5 sum from the data in the hashmap."""
772 cddcf432 chazapis
    
773 cddcf432 chazapis
    # TODO: Search backend for the MD5 of another object with the same hashmap and size...
774 cddcf432 chazapis
    md5 = hashlib.md5()
775 b3155065 Antony Chazapis
    bs = backend.block_size
776 cddcf432 chazapis
    for bi, hash in enumerate(hashmap):
777 b3155065 Antony Chazapis
        data = backend.get_block(hash) # Blocks come in padded.
778 cddcf432 chazapis
        if bi == len(hashmap) - 1:
779 c9865fe1 Antony Chazapis
            data = data[:size % bs]
780 c9865fe1 Antony Chazapis
        md5.update(data)
781 cddcf432 chazapis
    return md5.hexdigest().lower()
782 49133392 Antony Chazapis
783 af7bb62f Antony Chazapis
def simple_list_response(request, l):
784 6b6b6c1e Antony Chazapis
    if request.serialization == 'text':
785 6b6b6c1e Antony Chazapis
        return '\n'.join(l) + '\n'
786 6b6b6c1e Antony Chazapis
    if request.serialization == 'xml':
787 af7bb62f Antony Chazapis
        return render_to_string('items.xml', {'items': l})
788 6b6b6c1e Antony Chazapis
    if request.serialization == 'json':
789 6b6b6c1e Antony Chazapis
        return json.dumps(l)
790 6b6b6c1e Antony Chazapis
791 228de81b Antony Chazapis
def get_backend():
792 a7dff008 Antony Chazapis
    backend = connect_backend(db_module=BACKEND_DB_MODULE,
793 a7dff008 Antony Chazapis
                              db_connection=BACKEND_DB_CONNECTION,
794 a7dff008 Antony Chazapis
                              block_module=BACKEND_BLOCK_MODULE,
795 a7dff008 Antony Chazapis
                              block_path=BACKEND_BLOCK_PATH,
796 1427ce62 Sofia Papagiannaki
                              block_umask=BACKEND_BLOCK_UMASK,
797 a7dff008 Antony Chazapis
                              queue_module=BACKEND_QUEUE_MODULE,
798 a7dff008 Antony Chazapis
                              queue_connection=BACKEND_QUEUE_CONNECTION)
799 a7dff008 Antony Chazapis
    backend.default_policy['quota'] = BACKEND_QUOTA
800 a7dff008 Antony Chazapis
    backend.default_policy['versioning'] = BACKEND_VERSIONING
801 228de81b Antony Chazapis
    return backend
802 228de81b Antony Chazapis
803 9fefc052 Antony Chazapis
def update_request_headers(request):
804 9fefc052 Antony Chazapis
    # Handle URL-encoded keys and values.
805 88283e9e Antony Chazapis
    meta = dict([(k, v) for k, v in request.META.iteritems() if k.startswith('HTTP_')])
806 88283e9e Antony Chazapis
    for k, v in meta.iteritems():
807 88283e9e Antony Chazapis
        try:
808 88283e9e Antony Chazapis
            k.decode('ascii')
809 88283e9e Antony Chazapis
            v.decode('ascii')
810 88283e9e Antony Chazapis
        except UnicodeDecodeError:
811 88283e9e Antony Chazapis
            raise BadRequest('Bad character in headers.')
812 88283e9e Antony Chazapis
        if '%' in k or '%' in v:
813 88283e9e Antony Chazapis
            del(request.META[k])
814 88283e9e Antony Chazapis
            request.META[unquote(k)] = smart_unicode(unquote(v), strings_only=True)
815 9fefc052 Antony Chazapis
816 b956618e Antony Chazapis
def update_response_headers(request, response):
817 b956618e Antony Chazapis
    if request.serialization == 'xml':
818 b956618e Antony Chazapis
        response['Content-Type'] = 'application/xml; charset=UTF-8'
819 b956618e Antony Chazapis
    elif request.serialization == 'json':
820 b956618e Antony Chazapis
        response['Content-Type'] = 'application/json; charset=UTF-8'
821 22dab079 Antony Chazapis
    elif not response['Content-Type']:
822 b956618e Antony Chazapis
        response['Content-Type'] = 'text/plain; charset=UTF-8'
823 b980169d Antony Chazapis
    
824 9fefc052 Antony Chazapis
    if (not response.has_header('Content-Length') and
825 9fefc052 Antony Chazapis
        not (response.has_header('Content-Type') and
826 9fefc052 Antony Chazapis
             response['Content-Type'].startswith('multipart/byteranges'))):
827 b980169d Antony Chazapis
        response['Content-Length'] = len(response.content)
828 b980169d Antony Chazapis
    
829 9fefc052 Antony Chazapis
    # URL-encode unicode in headers.
830 9fefc052 Antony Chazapis
    meta = response.items()
831 9fefc052 Antony Chazapis
    for k, v in meta:
832 88283e9e Antony Chazapis
        if (k.startswith('X-Account-') or k.startswith('X-Container-') or
833 88283e9e Antony Chazapis
            k.startswith('X-Object-') or k.startswith('Content-')):
834 9fefc052 Antony Chazapis
            del(response[k])
835 88283e9e Antony Chazapis
            response[quote(k)] = quote(v, safe='/=,:@; ')
836 b956618e Antony Chazapis
837 b956618e Antony Chazapis
def render_fault(request, fault):
838 a7dff008 Antony Chazapis
    if isinstance(fault, InternalServerError) and settings.DEBUG:
839 b956618e Antony Chazapis
        fault.details = format_exc(fault)
840 1993fea9 Antony Chazapis
    
841 b956618e Antony Chazapis
    request.serialization = 'text'
842 08de868d Antony Chazapis
    data = fault.message + '\n'
843 08de868d Antony Chazapis
    if fault.details:
844 08de868d Antony Chazapis
        data += '\n' + fault.details
845 b956618e Antony Chazapis
    response = HttpResponse(data, status=fault.code)
846 b956618e Antony Chazapis
    update_response_headers(request, response)
847 b956618e Antony Chazapis
    return response
848 b956618e Antony Chazapis
849 b956618e Antony Chazapis
def request_serialization(request, format_allowed=False):
850 58a6c894 Antony Chazapis
    """Return the serialization format requested.
851 b956618e Antony Chazapis
    
852 b956618e Antony Chazapis
    Valid formats are 'text' and 'json', 'xml' if 'format_allowed' is True.
853 b956618e Antony Chazapis
    """
854 83dd59c5 Antony Chazapis
    
855 b956618e Antony Chazapis
    if not format_allowed:
856 b956618e Antony Chazapis
        return 'text'
857 b956618e Antony Chazapis
    
858 b956618e Antony Chazapis
    format = request.GET.get('format')
859 b956618e Antony Chazapis
    if format == 'json':
860 b956618e Antony Chazapis
        return 'json'
861 b956618e Antony Chazapis
    elif format == 'xml':
862 b956618e Antony Chazapis
        return 'xml'
863 b956618e Antony Chazapis
    
864 f9f15f92 Antony Chazapis
    for item in request.META.get('HTTP_ACCEPT', '').split(','):
865 f9f15f92 Antony Chazapis
        accept, sep, rest = item.strip().partition(';')
866 f9f15f92 Antony Chazapis
        if accept == 'application/json':
867 f9f15f92 Antony Chazapis
            return 'json'
868 f9f15f92 Antony Chazapis
        elif accept == 'application/xml' or accept == 'text/xml':
869 f9f15f92 Antony Chazapis
            return 'xml'
870 b956618e Antony Chazapis
    
871 b956618e Antony Chazapis
    return 'text'
872 b956618e Antony Chazapis
873 297513ba Antony Chazapis
def api_method(http_method=None, format_allowed=False, user_required=True):
874 58a6c894 Antony Chazapis
    """Decorator function for views that implement an API method."""
875 83dd59c5 Antony Chazapis
    
876 b956618e Antony Chazapis
    def decorator(func):
877 b956618e Antony Chazapis
        @wraps(func)
878 b956618e Antony Chazapis
        def wrapper(request, *args, **kwargs):
879 b956618e Antony Chazapis
            try:
880 b956618e Antony Chazapis
                if http_method and request.method != http_method:
881 b956618e Antony Chazapis
                    raise BadRequest('Method not allowed.')
882 dfdf4802 Sofia Papagiannaki
                
883 dfdf4802 Sofia Papagiannaki
                if user_required:
884 dfdf4802 Sofia Papagiannaki
                    token = None
885 dfdf4802 Sofia Papagiannaki
                    if request.method in ('HEAD', 'GET') and COOKIE_NAME in request.COOKIES:
886 dfdf4802 Sofia Papagiannaki
                        cookie_value = unquote(request.COOKIES.get(COOKIE_NAME, ''))
887 dfdf4802 Sofia Papagiannaki
                        if cookie_value and '|' in cookie_value:
888 dfdf4802 Sofia Papagiannaki
                            token = cookie_value.split('|', 1)[1]
889 52fc9c0c Sofia Papagiannaki
                    get_user(request, AUTHENTICATION_URL, AUTHENTICATION_USERS, token)
890 dfdf4802 Sofia Papagiannaki
                    if  getattr(request, 'user', None) is None:
891 dfdf4802 Sofia Papagiannaki
                        raise Unauthorized('Access denied')
892 2c22e4ac Antony Chazapis
                
893 b956618e Antony Chazapis
                # The args variable may contain up to (account, container, object).
894 b956618e Antony Chazapis
                if len(args) > 1 and len(args[1]) > 256:
895 b956618e Antony Chazapis
                    raise BadRequest('Container name too large.')
896 b956618e Antony Chazapis
                if len(args) > 2 and len(args[2]) > 1024:
897 b956618e Antony Chazapis
                    raise BadRequest('Object name too large.')
898 b956618e Antony Chazapis
                
899 9fefc052 Antony Chazapis
                # Format and check headers.
900 9fefc052 Antony Chazapis
                update_request_headers(request)
901 9fefc052 Antony Chazapis
                
902 b956618e Antony Chazapis
                # Fill in custom request variables.
903 b956618e Antony Chazapis
                request.serialization = request_serialization(request, format_allowed)
904 228de81b Antony Chazapis
                request.backend = get_backend()
905 07813fd6 Sofia Papagiannaki
                
906 b956618e Antony Chazapis
                response = func(request, *args, **kwargs)
907 b956618e Antony Chazapis
                update_response_headers(request, response)
908 b956618e Antony Chazapis
                return response
909 b956618e Antony Chazapis
            except Fault, fault:
910 b956618e Antony Chazapis
                return render_fault(request, fault)
911 b956618e Antony Chazapis
            except BaseException, e:
912 b956618e Antony Chazapis
                logger.exception('Unexpected error: %s' % e)
913 dfdf4802 Sofia Papagiannaki
                fault = InternalServerError('Unexpected error: %s' % e)
914 b956618e Antony Chazapis
                return render_fault(request, fault)
915 7b25e082 Antony Chazapis
            finally:
916 297513ba Antony Chazapis
                if getattr(request, 'backend', None) is not None:
917 d14fe290 Antony Chazapis
                    request.backend.close()
918 b956618e Antony Chazapis
        return wrapper
919 b956618e Antony Chazapis
    return decorator