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
|