Update default settings. Minor changes.
[pithos] / pithos / api / functions.py
1 #\r
2 # Copyright (c) 2011 Greek Research and Technology Network\r
3 #\r
4 \r
5 from django.http import HttpResponse\r
6 from django.template.loader import render_to_string\r
7 from django.utils import simplejson as json\r
8 from django.utils.http import http_date, parse_etags\r
9 \r
10 from pithos.api.compat import parse_http_date_safe\r
11 \r
12 from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, ItemNotFound, Conflict, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity\r
13 from pithos.api.util import get_meta, get_range, api_method\r
14 from pithos.backends.dummy import BackEnd\r
15 \r
16 import os\r
17 import datetime\r
18 import logging\r
19 \r
20 from settings import PROJECT_PATH\r
21 STORAGE_PATH = os.path.join(PROJECT_PATH, 'data')\r
22 \r
23 logger = logging.getLogger(__name__)\r
24 \r
25 @api_method('GET')\r
26 def authenticate(request):\r
27     # Normal Response Codes: 204\r
28     # Error Response Codes: serviceUnavailable (503),\r
29     #                       unauthorized (401),\r
30     #                       badRequest (400)\r
31     \r
32     x_auth_user = request.META.get('HTTP_X_AUTH_USER')\r
33     x_auth_key = request.META.get('HTTP_X_AUTH_KEY')\r
34     \r
35     if not x_auth_user or not x_auth_key:\r
36         raise BadRequest('Missing auth user or key.')\r
37     \r
38     response = HttpResponse(status = 204)\r
39     response['X-Auth-Token'] = '0000'\r
40     response['X-Storage-Url'] = os.path.join(request.build_absolute_uri(), 'demo')\r
41     return response\r
42 \r
43 def account_demux(request, v_account):\r
44     if request.method == 'HEAD':\r
45         return account_meta(request, v_account)\r
46     elif request.method == 'GET':\r
47         return container_list(request, v_account)\r
48     elif request.method == 'POST':\r
49         return account_update(request, v_account)\r
50     else:\r
51         return method_not_allowed(request)\r
52 \r
53 def container_demux(request, v_account, v_container):\r
54     if request.method == 'HEAD':\r
55         return container_meta(request, v_account, v_container)\r
56     elif request.method == 'GET':\r
57         return object_list(request, v_account, v_container)\r
58     elif request.method == 'PUT':\r
59         return container_create(request, v_account, v_container)\r
60     elif request.method == 'POST':\r
61         return container_update(request, v_account, v_container)\r
62     elif request.method == 'DELETE':\r
63         return container_delete(request, v_account, v_container)\r
64     else:\r
65         return method_not_allowed(request)\r
66 \r
67 def object_demux(request, v_account, v_container, v_object):\r
68     if request.method == 'HEAD':\r
69         return object_meta(request, v_account, v_container, v_object)\r
70     elif request.method == 'GET':\r
71         return object_read(request, v_account, v_container, v_object)\r
72     elif request.method == 'PUT':\r
73         return object_write(request, v_account, v_container, v_object)\r
74     elif request.method == 'COPY':\r
75         return object_copy(request, v_account, v_container, v_object)\r
76     elif request.method == 'POST':\r
77         return object_update(request, v_account, v_container, v_object)\r
78     elif request.method == 'DELETE':\r
79         return object_delete(request, v_account, v_container, v_object)\r
80     else:\r
81         return method_not_allowed(request)\r
82 \r
83 @api_method('HEAD')\r
84 def account_meta(request, v_account):\r
85     # Normal Response Codes: 204\r
86     # Error Response Codes: serviceUnavailable (503),\r
87     #                       unauthorized (401),\r
88     #                       badRequest (400)\r
89     \r
90     be = BackEnd(STORAGE_PATH)\r
91     try:\r
92         info = be.get_account_meta(request.user)\r
93     except NameError:\r
94         info = {'count': 0, 'bytes': 0}\r
95     \r
96     response = HttpResponse(status = 204)\r
97     response['X-Account-Container-Count'] = info['count']\r
98     response['X-Account-Bytes-Used'] = info['bytes']\r
99     for k in [x for x in info.keys() if x.startswith('X-Account-Meta-')]:\r
100         response[k.encode('utf-8')] = info[k].encode('utf-8')\r
101     \r
102     return response\r
103 \r
104 @api_method('POST')\r
105 def account_update(request, v_account):\r
106     # Normal Response Codes: 202\r
107     # Error Response Codes: serviceUnavailable (503),\r
108     #                       itemNotFound (404),\r
109     #                       unauthorized (401),\r
110     #                       badRequest (400)\r
111     \r
112     meta = get_meta(request, 'X-Account-Meta-')\r
113     \r
114     be = BackEnd(STORAGE_PATH)\r
115     be.update_account_meta(request.user, meta)\r
116     \r
117     return HttpResponse(status = 202)\r
118 \r
119 @api_method('GET', format_allowed = True)\r
120 def container_list(request, v_account):\r
121     # Normal Response Codes: 200, 204\r
122     # Error Response Codes: serviceUnavailable (503),\r
123     #                       itemNotFound (404),\r
124     #                       unauthorized (401),\r
125     #                       badRequest (400)\r
126     \r
127     marker = request.GET.get('marker')\r
128     limit = request.GET.get('limit')\r
129     if limit:\r
130         try:\r
131             limit = int(limit)\r
132         except ValueError:\r
133             limit = 10000\r
134     \r
135     be = BackEnd(STORAGE_PATH)\r
136     try:\r
137         containers = be.list_containers(request.user, marker, limit)\r
138     except NameError:\r
139         containers = []\r
140     \r
141     if request.serialization == 'text':\r
142         if len(containers) == 0:\r
143             # The cloudfiles python bindings expect 200 if json/xml.\r
144             return HttpResponse(status = 204)\r
145         return HttpResponse('\n'.join(containers), status = 200)\r
146     \r
147     # TODO: Do this with a backend parameter?\r
148     try:\r
149         containers = [be.get_container_meta(request.user, x) for x in containers]\r
150     except NameError:\r
151         raise ItemNotFound()\r
152     if request.serialization == 'xml':\r
153         data = render_to_string('containers.xml', {'account': request.user, 'containers': containers})\r
154     elif request.serialization  == 'json':\r
155         data = json.dumps(containers)\r
156     return HttpResponse(data, status = 200)\r
157 \r
158 @api_method('HEAD')\r
159 def container_meta(request, v_account, v_container):\r
160     # Normal Response Codes: 204\r
161     # Error Response Codes: serviceUnavailable (503),\r
162     #                       itemNotFound (404),\r
163     #                       unauthorized (401),\r
164     #                       badRequest (400)\r
165     \r
166     be = BackEnd(STORAGE_PATH)\r
167     try:\r
168         info = be.get_container_meta(request.user, v_container)\r
169     except NameError:\r
170         raise ItemNotFound()\r
171     \r
172     response = HttpResponse(status = 204)\r
173     response['X-Container-Object-Count'] = info['count']\r
174     response['X-Container-Bytes-Used'] = info['bytes']\r
175     for k in [x for x in info.keys() if x.startswith('X-Container-Meta-')]:\r
176         response[k.encode('utf-8')] = info[k].encode('utf-8')\r
177     \r
178     return response\r
179 \r
180 @api_method('PUT')\r
181 def container_create(request, v_account, v_container):\r
182     # Normal Response Codes: 201, 202\r
183     # Error Response Codes: serviceUnavailable (503),\r
184     #                       itemNotFound (404),\r
185     #                       unauthorized (401),\r
186     #                       badRequest (400)\r
187     \r
188     meta = get_meta(request, 'X-Container-Meta-')\r
189     \r
190     be = BackEnd(STORAGE_PATH)\r
191     try:\r
192         be.create_container(request.user, v_container)\r
193         ret = 201\r
194     except NameError:\r
195         ret = 202\r
196     \r
197     if len(meta) > 0:\r
198         be.update_container_meta(request.user, v_container, meta)\r
199     \r
200     return HttpResponse(status = ret)\r
201 \r
202 @api_method('POST')\r
203 def container_update(request, v_account, v_container):\r
204     # Normal Response Codes: 202\r
205     # Error Response Codes: serviceUnavailable (503),\r
206     #                       itemNotFound (404),\r
207     #                       unauthorized (401),\r
208     #                       badRequest (400)\r
209     \r
210     meta = get_meta(request, 'X-Container-Meta-')\r
211     \r
212     be = BackEnd(STORAGE_PATH)\r
213     try:\r
214         be.update_container_meta(request.user, v_container, meta)\r
215     except NameError:\r
216         raise ItemNotFound()\r
217     \r
218     return HttpResponse(status = 202)\r
219 \r
220 @api_method('DELETE')\r
221 def container_delete(request, v_account, v_container):\r
222     # Normal Response Codes: 204\r
223     # Error Response Codes: serviceUnavailable (503),\r
224     #                       conflict (409),\r
225     #                       itemNotFound (404),\r
226     #                       unauthorized (401),\r
227     #                       badRequest (400)\r
228     \r
229     be = BackEnd(STORAGE_PATH)\r
230     try:\r
231         be.delete_container(request.user, v_container)\r
232     except NameError:\r
233         raise ItemNotFound()\r
234     except IndexError:\r
235         raise Conflict()\r
236     return HttpResponse(status = 204)\r
237 \r
238 @api_method('GET', format_allowed = True)\r
239 def object_list(request, v_account, v_container):\r
240     # Normal Response Codes: 200, 204\r
241     # Error Response Codes: serviceUnavailable (503),\r
242     #                       itemNotFound (404),\r
243     #                       unauthorized (401),\r
244     #                       badRequest (400)\r
245     \r
246     path = request.GET.get('path')\r
247     prefix = request.GET.get('prefix')\r
248     delimiter = request.GET.get('delimiter')\r
249     \r
250     # TODO: Check if the cloudfiles python bindings expect the results with the prefix.\r
251     # Path overrides prefix and delimiter.\r
252     if path:\r
253         prefix = path\r
254         delimiter = '/'\r
255     # Naming policy.\r
256     if prefix and delimiter:\r
257         prefix = prefix + delimiter\r
258     if not prefix:\r
259         prefix = ''\r
260     \r
261     marker = request.GET.get('marker')\r
262     limit = request.GET.get('limit')\r
263     if limit:\r
264         try:\r
265             limit = int(limit)\r
266         except ValueError:\r
267             limit = 10000\r
268     \r
269     be = BackEnd(STORAGE_PATH)\r
270     try:\r
271         objects = be.list_objects(request.user, v_container, prefix, delimiter, marker, limit)\r
272     except NameError:\r
273         raise ItemNotFound()\r
274     \r
275     if request.serialization == 'text':\r
276         if len(objects) == 0:\r
277             # The cloudfiles python bindings expect 200 if json/xml.\r
278             return HttpResponse(status = 204)\r
279         return HttpResponse('\n'.join(objects), status = 200)\r
280     \r
281     # TODO: Do this with a backend parameter?\r
282     try:\r
283         objects = [be.get_object_meta(request.user, v_container, x) for x in objects]\r
284     except NameError:\r
285         raise ItemNotFound()\r
286     # Format dates.\r
287     for x in objects:\r
288         if x.has_key('last_modified'):\r
289             x['last_modified'] = datetime.datetime.fromtimestamp(x['last_modified']).isoformat()\r
290     if request.serialization == 'xml':\r
291         data = render_to_string('objects.xml', {'container': v_container, 'objects': objects})\r
292     elif request.serialization  == 'json':\r
293         data = json.dumps(objects)\r
294     return HttpResponse(data, status = 200)\r
295 \r
296 @api_method('HEAD')\r
297 def object_meta(request, v_account, v_container, v_object):\r
298     # Normal Response Codes: 204\r
299     # Error Response Codes: serviceUnavailable (503),\r
300     #                       itemNotFound (404),\r
301     #                       unauthorized (401),\r
302     #                       badRequest (400)\r
303     \r
304     be = BackEnd(STORAGE_PATH)\r
305     try:\r
306         info = be.get_object_meta(request.user, v_container, v_object)\r
307     except NameError:\r
308         raise ItemNotFound()\r
309     \r
310     response = HttpResponse(status = 204)\r
311     response['ETag'] = info['hash']\r
312     response['Content-Length'] = info['bytes']\r
313     response['Content-Type'] = info['content_type']\r
314     response['Last-Modified'] = http_date(info['last_modified'])\r
315     for k in [x for x in info.keys() if x.startswith('X-Object-Meta-')]:\r
316         response[k.encode('utf-8')] = info[k].encode('utf-8')\r
317     \r
318     return response\r
319 \r
320 @api_method('GET')\r
321 def object_read(request, v_account, v_container, v_object):\r
322     # Normal Response Codes: 200, 206\r
323     # Error Response Codes: serviceUnavailable (503),\r
324     #                       rangeNotSatisfiable (416),\r
325     #                       preconditionFailed (412),\r
326     #                       itemNotFound (404),\r
327     #                       unauthorized (401),\r
328     #                       badRequest (400),\r
329     #                       notModified (304)\r
330     \r
331     be = BackEnd(STORAGE_PATH)\r
332     try:\r
333         info = be.get_object_meta(request.user, v_container, v_object)\r
334     except NameError:\r
335         raise ItemNotFound()\r
336     \r
337     # TODO: Check if the cloudfiles python bindings expect hash/content_type/last_modified on range requests.\r
338     response = HttpResponse()\r
339     response['ETag'] = info['hash']\r
340     response['Content-Type'] = info['content_type']\r
341     response['Last-Modified'] = http_date(info['last_modified'])\r
342     \r
343     # Range handling.\r
344     range = get_range(request)\r
345     if range is not None:\r
346         offset, length = range\r
347         if length:\r
348             if offset + length > info['bytes']:\r
349                 raise RangeNotSatisfiable()\r
350         else:\r
351             if offset > info['bytes']:\r
352                 raise RangeNotSatisfiable()\r
353         if not length:\r
354             length = -1\r
355         \r
356         response['Content-Length'] = length        \r
357         response.status_code = 206\r
358     else:\r
359         offset = 0\r
360         length = -1\r
361         \r
362         response['Content-Length'] = info['bytes']\r
363         response.status_code = 200\r
364     \r
365     # Conditions (according to RFC2616 must be evaluated at the end).\r
366     # TODO: Check etag/date conditions.\r
367     if_match = request.META.get('HTTP_IF_MATCH')\r
368     if if_match is not None and if_match != '*':\r
369         if info['hash'] not in parse_etags(if_match):\r
370             raise PreconditionFailed()\r
371     \r
372     if_none_match = request.META.get('HTTP_IF_NONE_MATCH')\r
373     if if_none_match is not None:\r
374         if if_none_match == '*' or info['hash'] in parse_etags(if_none_match):\r
375             raise NotModified()\r
376     \r
377     if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE')\r
378     if if_modified_since is not None:\r
379         if_modified_since = parse_http_date_safe(if_modified_since)\r
380     if if_modified_since is not None and info['last_modified'] <= if_modified_since:\r
381         raise NotModified()\r
382 \r
383     if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE')\r
384     if if_unmodified_since is not None:\r
385         if_unmodified_since = parse_http_date_safe(if_unmodified_since)\r
386     if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since:\r
387         raise PreconditionFailed()\r
388     \r
389     try:\r
390         response.content = be.get_object(request.user, v_container, v_object, offset, length)\r
391     except NameError:\r
392         raise ItemNotFound()\r
393     \r
394     return response\r
395 \r
396 @api_method('PUT')\r
397 def object_write(request, v_account, v_container, v_object):\r
398     # Normal Response Codes: 201\r
399     # Error Response Codes: serviceUnavailable (503),\r
400     #                       unprocessableEntity (422),\r
401     #                       lengthRequired (411),\r
402     #                       itemNotFound (404),\r
403     #                       unauthorized (401),\r
404     #                       badRequest (400)\r
405     \r
406     be = BackEnd(STORAGE_PATH)\r
407     \r
408     copy_from = request.META.get('HTTP_X_COPY_FROM')\r
409     if copy_from:\r
410         parts = copy_from.split('/')\r
411         if len(parts) < 3 or parts[0] != '':\r
412             raise BadRequest('Bad X-Copy-From path.')\r
413         copy_container = parts[1]\r
414         copy_name = '/'.join(parts[2:])\r
415         \r
416         try:\r
417             info = be.get_object_meta(request.user, copy_container, copy_name)\r
418         except NameError:\r
419             raise ItemNotFound()\r
420         \r
421         content_length = request.META.get('CONTENT_LENGTH')\r
422         content_type = request.META.get('CONTENT_TYPE')\r
423         # TODO: Why is this required? Copy this ammount?\r
424         if not content_length:\r
425             raise LengthRequired()\r
426         if content_type:\r
427             info['content_type'] = content_type\r
428         \r
429         meta = get_meta(request, 'X-Object-Meta-')\r
430         info.update(meta)\r
431         \r
432         try:\r
433             be.copy_object(request.user, copy_container, copy_name, v_container, v_object)\r
434             be.update_object_meta(request.user, v_container, v_object, info)\r
435         except NameError:\r
436             raise ItemNotFound()\r
437         \r
438         response = HttpResponse(status = 201)\r
439     else:\r
440         content_length = request.META.get('CONTENT_LENGTH')\r
441         content_type = request.META.get('CONTENT_TYPE')\r
442         if not content_length or not content_type:\r
443             raise LengthRequired()\r
444         \r
445         info = {'content_type': content_type}\r
446         meta = get_meta(request, 'X-Object-Meta-')\r
447         info.update(meta)\r
448         \r
449         data = request.raw_post_data\r
450         try:\r
451             be.update_object(request.user, v_container, v_object, data)\r
452             be.update_object_meta(request.user, v_container, v_object, info)\r
453         except NameError:\r
454             raise ItemNotFound()\r
455         \r
456         # TODO: Check before update?\r
457         info = be.get_object_meta(request.user, v_container, v_object)\r
458         etag = request.META.get('HTTP_ETAG')\r
459         if etag:\r
460             etag = parse_etags(etag)[0] # TODO: Unescape properly.\r
461             if etag != info['hash']:\r
462                 be.delete_object(request.user, v_container, v_object)\r
463                 raise UnprocessableEntity()\r
464         \r
465         response = HttpResponse(status = 201)\r
466         response['ETag'] = info['hash']\r
467     \r
468     return response\r
469 \r
470 @api_method('COPY')\r
471 def object_copy(request, v_account, v_container, v_object):\r
472     # Normal Response Codes: 201\r
473     # Error Response Codes: serviceUnavailable (503),\r
474     #                       itemNotFound (404),\r
475     #                       unauthorized (401),\r
476     #                       badRequest (400)\r
477     \r
478     destination = request.META.get('HTTP_DESTINATION')\r
479     if not destination:\r
480         raise BadRequest('Missing Destination.');\r
481     \r
482     parts = destination.split('/')\r
483     if len(parts) < 3 or parts[0] != '':\r
484         raise BadRequest('Bad Destination path.')\r
485     dest_container = parts[1]\r
486     dest_name = '/'.join(parts[2:])\r
487     \r
488     be = BackEnd(STORAGE_PATH)\r
489     try:\r
490         info = be.get_object_meta(request.user, v_container, v_object)\r
491     except NameError:\r
492         raise ItemNotFound()\r
493     \r
494     content_type = request.META.get('CONTENT_TYPE')\r
495     if content_type:\r
496         info['content_type'] = content_type\r
497     meta = get_meta(request, 'X-Object-Meta-')\r
498     info.update(meta)\r
499     \r
500     try:\r
501         be.copy_object(request.user, v_container, v_object, dest_container, dest_name)\r
502         be.update_object_meta(request.user, dest_container, dest_name, info)\r
503     except NameError:\r
504         raise ItemNotFound()\r
505     \r
506     response = HttpResponse(status = 201)\r
507 \r
508 @api_method('POST')\r
509 def object_update(request, v_account, v_container, v_object):\r
510     # Normal Response Codes: 202\r
511     # Error Response Codes: serviceUnavailable (503),\r
512     #                       itemNotFound (404),\r
513     #                       unauthorized (401),\r
514     #                       badRequest (400)\r
515     \r
516     meta = get_meta(request, 'X-Object-Meta-')\r
517     \r
518     be = BackEnd(STORAGE_PATH)\r
519     try:\r
520         be.update_object_meta(request.user, v_container, v_object, meta)\r
521     except NameError:\r
522         raise ItemNotFound()\r
523     \r
524     return HttpResponse(status = 202)\r
525 \r
526 @api_method('DELETE')\r
527 def object_delete(request, v_account, v_container, v_object):\r
528     # Normal Response Codes: 204\r
529     # Error Response Codes: serviceUnavailable (503),\r
530     #                       itemNotFound (404),\r
531     #                       unauthorized (401),\r
532     #                       badRequest (400)\r
533     \r
534     be = BackEnd(STORAGE_PATH)\r
535     try:\r
536         be.delete_object(request.user, v_container, v_object)\r
537     except NameError:\r
538         raise ItemNotFound()\r
539     return HttpResponse(status = 204)\r
540 \r
541 @api_method()\r
542 def method_not_allowed(request):\r
543     raise BadRequest('Method not allowed.')\r