Move to a better file organization
[pithos] / pithos / api / util.py
1 #\r
2 # Copyright (c) 2011 Greek Research and Technology Network\r
3 #\r
4 \r
5 from datetime import timedelta, tzinfo\r
6 from functools import wraps\r
7 from random import choice\r
8 from string import ascii_letters, digits\r
9 from time import time\r
10 from traceback import format_exc\r
11 from wsgiref.handlers import format_date_time\r
12 \r
13 from django.conf import settings\r
14 from django.http import HttpResponse\r
15 from django.template.loader import render_to_string\r
16 from django.utils import simplejson as json\r
17 \r
18 from pithos.api.faults import Fault, BadRequest, ItemNotFound, ServiceUnavailable\r
19 #from synnefo.db.models import SynnefoUser, Image, ImageMetadata, VirtualMachine, VirtualMachineMetadata\r
20 \r
21 import datetime\r
22 import dateutil.parser\r
23 import logging\r
24 \r
25 import re\r
26 import calendar\r
27 \r
28 # Part of newer Django versions.\r
29 \r
30 __D = r'(?P<day>\d{2})'\r
31 __D2 = r'(?P<day>[ \d]\d)'\r
32 __M = r'(?P<mon>\w{3})'\r
33 __Y = r'(?P<year>\d{4})'\r
34 __Y2 = r'(?P<year>\d{2})'\r
35 __T = r'(?P<hour>\d{2}):(?P<min>\d{2}):(?P<sec>\d{2})'\r
36 RFC1123_DATE = re.compile(r'^\w{3}, %s %s %s %s GMT$' % (__D, __M, __Y, __T))\r
37 RFC850_DATE = re.compile(r'^\w{6,9}, %s-%s-%s %s GMT$' % (__D, __M, __Y2, __T))\r
38 ASCTIME_DATE = re.compile(r'^\w{3} %s %s %s %s$' % (__M, __D2, __T, __Y))\r
39 \r
40 def parse_http_date(date):\r
41     """\r
42     Parses a date format as specified by HTTP RFC2616 section 3.3.1.\r
43 \r
44     The three formats allowed by the RFC are accepted, even if only the first\r
45     one is still in widespread use.\r
46 \r
47     Returns an floating point number expressed in seconds since the epoch, in\r
48     UTC.\r
49     """\r
50     # emails.Util.parsedate does the job for RFC1123 dates; unfortunately\r
51     # RFC2616 makes it mandatory to support RFC850 dates too. So we roll\r
52     # our own RFC-compliant parsing.\r
53     for regex in RFC1123_DATE, RFC850_DATE, ASCTIME_DATE:\r
54         m = regex.match(date)\r
55         if m is not None:\r
56             break\r
57     else:\r
58         raise ValueError("%r is not in a valid HTTP date format" % date)\r
59     try:\r
60         year = int(m.group('year'))\r
61         if year < 100:\r
62             if year < 70:\r
63                 year += 2000\r
64             else:\r
65                 year += 1900\r
66         month = MONTHS.index(m.group('mon').lower()) + 1\r
67         day = int(m.group('day'))\r
68         hour = int(m.group('hour'))\r
69         min = int(m.group('min'))\r
70         sec = int(m.group('sec'))\r
71         result = datetime.datetime(year, month, day, hour, min, sec)\r
72         return calendar.timegm(result.utctimetuple())\r
73     except Exception:\r
74         raise ValueError("%r is not a valid date" % date)\r
75 \r
76 def parse_http_date_safe(date):\r
77     """\r
78     Same as parse_http_date, but returns None if the input is invalid.\r
79     """\r
80     try:\r
81         return parse_http_date(date)\r
82     except Exception:\r
83         pass\r
84 \r
85 def get_object_meta(request):\r
86     """\r
87     Get all X-Object-Meta-* headers in a dict.\r
88     """\r
89     prefix = 'HTTP_X_OBJECT_META_'\r
90     return dict([(k[len(prefix):].lower(), v) for k, v in request.META.iteritems() if k.startswith(prefix)])\r
91     \r
92 def get_range(request):\r
93     """\r
94     Parse a Range header from the request.\r
95     Either returns None, or an (offset, length) tuple.\r
96     If no offset is defined offset equals 0.\r
97     If no length is defined length is None.\r
98     """\r
99     \r
100     range = request.GET.get('range')\r
101     if not range:\r
102         return None\r
103     \r
104     range = range.replace(' ', '')\r
105     if not range.startswith('bytes='):\r
106         return None\r
107     \r
108     parts = range.split('-')\r
109     if len(parts) != 2:\r
110         return None\r
111     \r
112     offset, length = parts\r
113     if offset == '' and length == '':\r
114         return None\r
115     \r
116     if offset != '':\r
117         try:\r
118             offset = int(offset)\r
119         except ValueError:\r
120             return None\r
121     else:\r
122         offset = 0\r
123     \r
124     if length != '':\r
125         try:\r
126             length = int(length)\r
127         except ValueError:\r
128             return None\r
129     else:\r
130         length = None\r
131     \r
132     return (offset, length)\r
133 \r
134 # def get_vm(server_id):\r
135 #     """Return a VirtualMachine instance or raise ItemNotFound."""\r
136 #     \r
137 #     try:\r
138 #         server_id = int(server_id)\r
139 #         return VirtualMachine.objects.get(id=server_id)\r
140 #     except ValueError:\r
141 #         raise BadRequest('Invalid server ID.')\r
142 #     except VirtualMachine.DoesNotExist:\r
143 #         raise ItemNotFound('Server not found.')\r
144\r
145 # def get_vm_meta(server_id, key):\r
146 #     """Return a VirtualMachineMetadata instance or raise ItemNotFound."""\r
147 #     \r
148 #     try:\r
149 #         server_id = int(server_id)\r
150 #         return VirtualMachineMetadata.objects.get(meta_key=key, vm=server_id)\r
151 #     except VirtualMachineMetadata.DoesNotExist:\r
152 #         raise ItemNotFound('Metadata key not found.')\r
153\r
154 # def get_image(image_id):\r
155 #     """Return an Image instance or raise ItemNotFound."""\r
156 #     \r
157 #     try:\r
158 #         image_id = int(image_id)\r
159 #         return Image.objects.get(id=image_id)\r
160 #     except Image.DoesNotExist:\r
161 #         raise ItemNotFound('Image not found.')\r
162\r
163 # def get_image_meta(image_id, key):\r
164 #     """Return a ImageMetadata instance or raise ItemNotFound."""\r
165\r
166 #     try:\r
167 #         image_id = int(image_id)\r
168 #         return ImageMetadata.objects.get(meta_key=key, image=image_id)\r
169 #     except ImageMetadata.DoesNotExist:\r
170 #         raise ItemNotFound('Metadata key not found.')\r
171\r
172\r
173 # def get_request_dict(request):\r
174 #     """Returns data sent by the client as a python dict."""\r
175 #     \r
176 #     data = request.raw_post_data\r
177 #     if request.META.get('CONTENT_TYPE').startswith('application/json'):\r
178 #         try:\r
179 #             return json.loads(data)\r
180 #         except ValueError:\r
181 #             raise BadRequest('Invalid JSON data.')\r
182 #     else:\r
183 #         raise BadRequest('Unsupported Content-Type.')\r
184 \r
185 def update_response_headers(request, response):\r
186     if request.serialization == 'xml':\r
187         response['Content-Type'] = 'application/xml; charset=UTF-8'\r
188     elif request.serialization == 'json':\r
189         response['Content-Type'] = 'application/json; charset=UTF-8'\r
190     else:\r
191         response['Content-Type'] = 'text/plain; charset=UTF-8'\r
192 \r
193     if settings.TEST:\r
194         response['Date'] = format_date_time(time())\r
195 \r
196 def render_fault(request, fault):\r
197     if settings.DEBUG or settings.TEST:\r
198         fault.details = format_exc(fault)\r
199     \r
200 #     if request.serialization == 'xml':\r
201 #         data = render_to_string('fault.xml', {'fault': fault})\r
202 #     else:\r
203 #         d = {fault.name: {'code': fault.code, 'message': fault.message, 'details': fault.details}}\r
204 #         data = json.dumps(d)\r
205     \r
206 #     resp = HttpResponse(data, status=fault.code)\r
207     resp = HttpResponse(status = fault.code)\r
208     update_response_headers(request, resp)\r
209     return resp\r
210 \r
211 def request_serialization(request, format_allowed=False):\r
212     """\r
213     Return the serialization format requested.\r
214        \r
215     Valid formats are 'text' and 'json', 'xml' if `format_allowed` is True.\r
216     """\r
217     \r
218     if not format_allowed:\r
219         return 'text'\r
220     \r
221     format = request.GET.get('format')\r
222     if format == 'json':\r
223         return 'json'\r
224     elif format == 'xml':\r
225         return 'xml'\r
226     \r
227     # TODO: Do we care of Accept headers?\r
228 #     for item in request.META.get('HTTP_ACCEPT', '').split(','):\r
229 #         accept, sep, rest = item.strip().partition(';')\r
230 #         if accept == 'application/json':\r
231 #             return 'json'\r
232 #         elif accept == 'application/xml':\r
233 #             return 'xml'\r
234     \r
235     return 'text'\r
236 \r
237 def api_method(http_method = None, format_allowed = False):\r
238     """\r
239     Decorator function for views that implement an API method.\r
240     """\r
241     \r
242     def decorator(func):\r
243         @wraps(func)\r
244         def wrapper(request, *args, **kwargs):\r
245             try:\r
246                 request.serialization = request_serialization(request, format_allowed)\r
247                 # TODO: Authenticate.\r
248                 # TODO: Return 401/404 when the account is not found.\r
249                 request.user = "test"\r
250                 # TODO: Check parameter sizes.\r
251                 if http_method and request.method != http_method:\r
252                     raise BadRequest('Method not allowed.')\r
253                 \r
254                 resp = func(request, *args, **kwargs)\r
255                 update_response_headers(request, resp)\r
256                 return resp\r
257             \r
258             except Fault, fault:\r
259                 return render_fault(request, fault)\r
260             except BaseException, e:\r
261                 logging.exception('Unexpected error: %s' % e)\r
262                 fault = ServiceUnavailable('Unexpected error')\r
263                 return render_fault(request, fault)\r
264         return wrapper\r
265     return decorator\r