Statistics
| Branch: | Tag: | Revision:

root / pithos / api / util.py @ 4adb68b8

History | View | Annotate | Download (8.5 kB)

1
#
2
# Copyright (c) 2011 Greek Research and Technology Network
3
#
4

    
5
from datetime import timedelta, tzinfo
6
from functools import wraps
7
from random import choice
8
from string import ascii_letters, digits
9
from time import time
10
from traceback import format_exc
11
from wsgiref.handlers import format_date_time
12

    
13
from django.conf import settings
14
from django.http import HttpResponse
15
from django.template.loader import render_to_string
16
from django.utils import simplejson as json
17

    
18
from pithos.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable
19
#from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata
20

    
21
import datetime
22
import dateutil.parser
23
import logging
24

    
25
import re
26
import calendar
27

    
28
# Part of newer Django versions.
29

    
30
__D = r'(?P<day>\d{2})'
31
__D2 = r'(?P<day>[ \d]\d)'
32
__M = r'(?P<mon>\w{3})'
33
__Y = r'(?P<year>\d{4})'
34
__Y2 = r'(?P<year>\d{2})'
35
__T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'
36
RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))
37
RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))
38
ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))
39

    
40
def parse_http_date(date):
41
    """
42
    Parses a date format as specified by HTTP RFC2616 section 3.3.1.
43

44
    The three formats allowed by the RFC are accepted, even if only the first
45
    one is still in widespread use.
46

47
    Returns an floating point number expressed in seconds since the epoch, in
48
    UTC.
49
    """
50
    # emails.Util.parsedate does the job for RFC1123 dates; unfortunately
51
    # RFC2616 makes it mandatory to support RFC850 dates too. So we roll
52
    # our own RFC-compliant parsing.
53
    for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:
54
        m = regex.match(date)
55
        if m is not None:
56
            break
57
    else:
58
        raise ValueError("%r is not in a valid HTTP date format" % date)
59
    try:
60
        year = int(m.group('year'))
61
        if year < 100:
62
            if year < 70:
63
                year += 2000
64
            else:
65
                year += 1900
66
        month = MONTHS.index(m.group('mon').lower()) + 1
67
        day = int(m.group('day'))
68
        hour = int(m.group('hour'))
69
        min = int(m.group('min'))
70
        sec = int(m.group('sec'))
71
        result = datetime.datetime(year, month, day, hour, min, sec)
72
        return calendar.timegm(result.utctimetuple())
73
    except Exception:
74
        raise ValueError("%r is not a valid date" % date)
75

    
76
def parse_http_date_safe(date):
77
    """
78
    Same as parse_http_date, but returns None if the input is invalid.
79
    """
80
    try:
81
        return parse_http_date(date)
82
    except Exception:
83
        pass
84

    
85
def get_object_meta(request):
86
    """
87
    Get all X-Object-Meta-* headers in a dict.
88
    """
89
    prefix = 'HTTP_X_OBJECT_META_'
90
    return dict([(k[len(prefix):].lower(), v) for k, v in request.META.iteritems() if k.startswith(prefix)])
91
    
92
def get_range(request):
93
    """
94
    Parse a Range header from the request.
95
    Either returns None, or an (offset, length) tuple.
96
    If no offset is defined offset equals 0.
97
    If no length is defined length is None.
98
    """
99
    
100
    range = request.GET.get('range')
101
    if not range:
102
        return None
103
    
104
    range = range.replace(' ', '')
105
    if not range.startswith('bytes='):
106
        return None
107
    
108
    parts = range.split('-')
109
    if len(parts) != 2:
110
        return None
111
    
112
    offset, length = parts
113
    if offset == '' and length == '':
114
        return None
115
    
116
    if offset != '':
117
        try:
118
            offset = int(offset)
119
        except ValueError:
120
            return None
121
    else:
122
        offset = 0
123
    
124
    if length != '':
125
        try:
126
            length = int(length)
127
        except ValueError:
128
            return None
129
    else:
130
        length = None
131
    
132
    return (offset, length)
133

    
134
# def get_vm(server_id):
135
#     """Return a VirtualMachine instance or raise ItemNotFound."""
136
#     
137
#     try:
138
#         server_id = int(server_id)
139
#         return VirtualMachine.objects.get(id=server_id)
140
#     except ValueError:
141
#         raise BadRequest('Invalid server ID.')
142
#     except VirtualMachine.DoesNotExist:
143
#         raise ItemNotFound('Server not found.')
144
# 
145
# def get_vm_meta(server_id, key):
146
#     """Return a VirtualMachineMetadata instance or raise ItemNotFound."""
147
#     
148
#     try:
149
#         server_id = int(server_id)
150
#         return VirtualMachineMetadata.objects.get(meta_key=key, vm=server_id)
151
#     except VirtualMachineMetadata.DoesNotExist:
152
#         raise ItemNotFound('Metadata key not found.')
153
# 
154
# def get_image(image_id):
155
#     """Return an Image instance or raise ItemNotFound."""
156
#     
157
#     try:
158
#         image_id = int(image_id)
159
#         return Image.objects.get(id=image_id)
160
#     except Image.DoesNotExist:
161
#         raise ItemNotFound('Image not found.')
162
# 
163
# def get_image_meta(image_id, key):
164
#     """Return a ImageMetadata instance or raise ItemNotFound."""
165
# 
166
#     try:
167
#         image_id = int(image_id)
168
#         return ImageMetadata.objects.get(meta_key=key, image=image_id)
169
#     except ImageMetadata.DoesNotExist:
170
#         raise ItemNotFound('Metadata key not found.')
171
# 
172
# 
173
# def get_request_dict(request):
174
#     """Returns data sent by the client as a python dict."""
175
#     
176
#     data = request.raw_post_data
177
#     if request.META.get('CONTENT_TYPE').startswith('application/json'):
178
#         try:
179
#             return json.loads(data)
180
#         except ValueError:
181
#             raise BadRequest('Invalid JSON data.')
182
#     else:
183
#         raise BadRequest('Unsupported Content-Type.')
184

    
185
def update_response_headers(request, response):
186
    if request.serialization == 'xml':
187
        response['Content-Type'] = 'application/xml; charset=UTF-8'
188
    elif request.serialization == 'json':
189
        response['Content-Type'] = 'application/json; charset=UTF-8'
190
    else:
191
        response['Content-Type'] = 'text/plain; charset=UTF-8'
192

    
193
    if settings.TEST:
194
        response['Date'] = format_date_time(time())
195

    
196
def render_fault(request, fault):
197
    if settings.DEBUG or settings.TEST:
198
        fault.details = format_exc(fault)
199
    
200
#     if request.serialization == 'xml':
201
#         data = render_to_string('fault.xml', {'fault': fault})
202
#     else:
203
#         d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}
204
#         data = json.dumps(d)
205
    
206
#     resp = HttpResponse(data, status=fault.code)
207
    resp = HttpResponse(status = fault.code)
208
    update_response_headers(request, resp)
209
    return resp
210

    
211
def request_serialization(request, format_allowed=False):
212
    """
213
    Return the serialization format requested.
214
       
215
    Valid formats are 'text' and 'json', 'xml' if `format_allowed` is True.
216
    """
217
    
218
    if not format_allowed:
219
        return 'text'
220
    
221
    format = request.GET.get('format')
222
    if format == 'json':
223
        return 'json'
224
    elif format == 'xml':
225
        return 'xml'
226
    
227
    # TODO: Do we care of Accept headers?
228
#     for item in request.META.get('HTTP_ACCEPT', '').split(','):
229
#         accept, sep, rest = item.strip().partition(';')
230
#         if accept == 'application/json':
231
#             return 'json'
232
#         elif accept == 'application/xml':
233
#             return 'xml'
234
    
235
    return 'text'
236

    
237
def api_method(http_method = None, format_allowed = False):
238
    """
239
    Decorator function for views that implement an API method.
240
    """
241
    
242
    def decorator(func):
243
        @wraps(func)
244
        def wrapper(request, *args, **kwargs):
245
            try:
246
                request.serialization = request_serialization(request, format_allowed)
247
                # TODO: Authenticate.
248
                # TODO: Return 401/404 when the account is not found.
249
                request.user = "test"
250
                # TODO: Check parameter sizes.
251
                if http_method and request.method != http_method:
252
                    raise BadRequest('Method not allowed.')
253
                
254
                resp = func(request, *args, **kwargs)
255
                update_response_headers(request, resp)
256
                return resp
257
            
258
            except Fault, fault:
259
                return render_fault(request, fault)
260
            except BaseException, e:
261
                logging.exception('Unexpected error: %s' % e)
262
                fault = ServiceUnavailable('Unexpected error')
263
                return render_fault(request, fault)
264
        return wrapper
265
    return decorator