Revision 4adb68b8
/dev/null | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
from api.models import Container, Object, Metadata |
|
6 |
from django.contrib import admin |
|
7 |
|
|
8 |
admin.site.register(Container) |
|
9 |
admin.site.register(Object) |
|
10 |
admin.site.register(Metadata) |
/dev/null | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
def camelCase(s): |
|
6 |
return s[0].lower() + s[1:] |
|
7 |
|
|
8 |
|
|
9 |
class Fault(Exception): |
|
10 |
def __init__(self, message='', details='', name=''): |
|
11 |
Exception.__init__(self, message, details, name) |
|
12 |
self.message = message |
|
13 |
self.details = details |
|
14 |
self.name = name or camelCase(self.__class__.__name__) |
|
15 |
|
|
16 |
class NotModified(Fault): |
|
17 |
code = 304 |
|
18 |
|
|
19 |
class BadRequest(Fault): |
|
20 |
code = 400 |
|
21 |
|
|
22 |
class Unauthorized(Fault): |
|
23 |
code = 401 |
|
24 |
|
|
25 |
class ResizeNotAllowed(Fault): |
|
26 |
code = 403 |
|
27 |
|
|
28 |
class ItemNotFound(Fault): |
|
29 |
code = 404 |
|
30 |
|
|
31 |
class LengthRequired(Fault): |
|
32 |
code = 411 |
|
33 |
|
|
34 |
class PreconditionFailed(Fault): |
|
35 |
code = 412 |
|
36 |
|
|
37 |
class RangeNotSatisfiable(Fault): |
|
38 |
code = 416 |
|
39 |
|
|
40 |
class UnprocessableEntity(Fault): |
|
41 |
code = 422 |
|
42 |
|
|
43 |
class ServiceUnavailable(Fault): |
|
44 |
code = 503 |
/dev/null | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
from django.http import HttpResponse |
|
6 |
from django.template.loader import render_to_string |
|
7 |
from django.utils import simplejson as json |
|
8 |
from django.utils.http import http_date, parse_etags |
|
9 |
|
|
10 |
try: |
|
11 |
from django.utils.http import parse_http_date_safe |
|
12 |
except: |
|
13 |
from pithos.api.util import parse_http_date_safe |
|
14 |
|
|
15 |
from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity |
|
16 |
from pithos.api.util import get_object_meta, get_range, api_method |
|
17 |
|
|
18 |
from pithos.backends.dummy_debug import * |
|
19 |
|
|
20 |
import logging |
|
21 |
|
|
22 |
logging.basicConfig(level=logging.DEBUG) |
|
23 |
|
|
24 |
@api_method('GET') |
|
25 |
def authenticate(request): |
|
26 |
# Normal Response Codes: 204 |
|
27 |
# Error Response Codes: serviceUnavailable (503), |
|
28 |
# unauthorized (401), |
|
29 |
# badRequest (400) |
|
30 |
|
|
31 |
x_auth_user = request.META.get('HTTP_X_AUTH_USER') |
|
32 |
x_auth_key = request.META.get('HTTP_X_AUTH_KEY') |
|
33 |
|
|
34 |
if not x_auth_user or not x_auth_key: |
|
35 |
raise BadRequest('Missing auth user or key.') |
|
36 |
|
|
37 |
response = HttpResponse(status = 204) |
|
38 |
response['X-Auth-Token'] = 'eaaafd18-0fed-4b3a-81b4-663c99ec1cbb' |
|
39 |
# TODO: Do we support redirections? |
|
40 |
#response['X-Storage-Url'] = 'https://storage.grnet.gr/pithos/v1.0/<some reference>' |
|
41 |
return response |
|
42 |
|
|
43 |
def account_demux(request, v_account): |
|
44 |
if request.method == 'HEAD': |
|
45 |
return account_meta(request, v_account) |
|
46 |
elif request.method == 'GET': |
|
47 |
return container_list(request, v_account) |
|
48 |
else: |
|
49 |
return method_not_allowed(request) |
|
50 |
|
|
51 |
def container_demux(request, v_account, v_container): |
|
52 |
if request.method == 'HEAD': |
|
53 |
return container_meta(request, v_account, v_container) |
|
54 |
elif request.method == 'GET': |
|
55 |
return object_list(request, v_account, v_container) |
|
56 |
elif request.method == 'PUT': |
|
57 |
return container_create(request, v_account, v_container) |
|
58 |
elif request.method == 'DELETE': |
|
59 |
return container_delete(request, v_account, v_container) |
|
60 |
else: |
|
61 |
return method_not_allowed(request) |
|
62 |
|
|
63 |
def object_demux(request, v_account, v_container, v_object): |
|
64 |
if request.method == 'HEAD': |
|
65 |
return object_meta(request, v_account, v_container, v_object) |
|
66 |
elif request.method == 'GET': |
|
67 |
return object_read(request, v_account, v_container, v_object) |
|
68 |
elif request.method == 'PUT': |
|
69 |
return object_write(request, v_account, v_container, v_object) |
|
70 |
elif request.method == 'COPY': |
|
71 |
return object_copy(request, v_account, v_container, v_object) |
|
72 |
elif request.method == 'POST': |
|
73 |
return object_update(request, v_account, v_container, v_object) |
|
74 |
elif request.method == 'DELETE': |
|
75 |
return object_delete(request, v_account, v_container, v_object) |
|
76 |
else: |
|
77 |
return method_not_allowed(request) |
|
78 |
|
|
79 |
@api_method('HEAD') |
|
80 |
def account_meta(request, v_account): |
|
81 |
# Normal Response Codes: 204 |
|
82 |
# Error Response Codes: serviceUnavailable (503), |
|
83 |
# itemNotFound (404), |
|
84 |
# unauthorized (401), |
|
85 |
# badRequest (400) |
|
86 |
|
|
87 |
container_count, bytes_count = get_account_meta(request.user) |
|
88 |
|
|
89 |
response = HttpResponse(status = 204) |
|
90 |
response['X-Account-Container-Count'] = container_count |
|
91 |
response['X-Account-Total-Bytes-Used'] = bytes_count |
|
92 |
return response |
|
93 |
|
|
94 |
@api_method('GET', format_allowed = True) |
|
95 |
def container_list(request, v_account): |
|
96 |
# Normal Response Codes: 200, 204 |
|
97 |
# Error Response Codes: serviceUnavailable (503), |
|
98 |
# unauthorized (401), |
|
99 |
# badRequest (400) |
|
100 |
|
|
101 |
marker = request.GET.get('marker') |
|
102 |
limit = request.GET.get('limit') |
|
103 |
if limit: |
|
104 |
try: |
|
105 |
limit = int(limit) |
|
106 |
except ValueError: |
|
107 |
limit = None |
|
108 |
|
|
109 |
containers = list_containers(request.user, marker, limit) |
|
110 |
if len(containers) == 0: |
|
111 |
return HttpResponse(status = 204) |
|
112 |
|
|
113 |
if request.serialization == 'xml': |
|
114 |
data = render_to_string('containers.xml', {'account': request.user, 'containers': containers}) |
|
115 |
elif request.serialization == 'json': |
|
116 |
data = json.dumps(containers) |
|
117 |
else: |
|
118 |
data = '\n'.join(x['name'] for x in containers) |
|
119 |
|
|
120 |
return HttpResponse(data, status = 200) |
|
121 |
|
|
122 |
@api_method('HEAD') |
|
123 |
def container_meta(request, v_account, v_container): |
|
124 |
# Normal Response Codes: 204 |
|
125 |
# Error Response Codes: serviceUnavailable (503), |
|
126 |
# itemNotFound (404), |
|
127 |
# unauthorized (401), |
|
128 |
# badRequest (400) |
|
129 |
|
|
130 |
object_count, bytes_count = get_container_meta(request.user, v_container) |
|
131 |
|
|
132 |
response = HttpResponse(status = 204) |
|
133 |
response['X-Container-Object-Count'] = object_count |
|
134 |
response['X-Container-Bytes-Used'] = bytes_count |
|
135 |
return response |
|
136 |
|
|
137 |
@api_method('PUT') |
|
138 |
def container_create(request, v_account, v_container): |
|
139 |
# Normal Response Codes: 201, 202 |
|
140 |
# Error Response Codes: serviceUnavailable (503), |
|
141 |
# itemNotFound (404), |
|
142 |
# unauthorized (401), |
|
143 |
# badRequest (400) |
|
144 |
|
|
145 |
if create_container(request.user, v_container): |
|
146 |
return HttpResponse(status = 201) |
|
147 |
else: |
|
148 |
return HttpResponse(status = 202) |
|
149 |
|
|
150 |
@api_method('DELETE') |
|
151 |
def container_delete(request, v_account, v_container): |
|
152 |
# Normal Response Codes: 204 |
|
153 |
# Error Response Codes: serviceUnavailable (503), |
|
154 |
# itemNotFound (404), |
|
155 |
# unauthorized (401), |
|
156 |
# badRequest (400) |
|
157 |
|
|
158 |
object_count, bytes_count = get_container_meta(request.user, v_container) |
|
159 |
if object_count > 0: |
|
160 |
return HttpResponse(status = 409) |
|
161 |
|
|
162 |
delete_container(request.user, v_container) |
|
163 |
return HttpResponse(status = 204) |
|
164 |
|
|
165 |
@api_method('GET', format_allowed = True) |
|
166 |
def object_list(request, v_account, v_container): |
|
167 |
# Normal Response Codes: 200, 204 |
|
168 |
# Error Response Codes: serviceUnavailable (503), |
|
169 |
# itemNotFound (404), |
|
170 |
# unauthorized (401), |
|
171 |
# badRequest (400) |
|
172 |
|
|
173 |
path = request.GET.get('path') |
|
174 |
prefix = request.GET.get('prefix') |
|
175 |
delimiter = request.GET.get('delimiter') |
|
176 |
logging.debug("path: %s", path) |
|
177 |
|
|
178 |
# Path overrides prefix and delimiter. |
|
179 |
if path: |
|
180 |
prefix = path |
|
181 |
delimiter = '/' |
|
182 |
# Naming policy. |
|
183 |
if prefix and delimiter: |
|
184 |
prefix = prefix + delimiter |
|
185 |
|
|
186 |
marker = request.GET.get('marker') |
|
187 |
limit = request.GET.get('limit') |
|
188 |
if limit: |
|
189 |
try: |
|
190 |
limit = int(limit) |
|
191 |
except ValueError: |
|
192 |
limit = None |
|
193 |
|
|
194 |
objects = list_objects(request.user, v_container, prefix, delimiter, marker, limit) |
|
195 |
if len(objects) == 0: |
|
196 |
return HttpResponse(status = 204) |
|
197 |
|
|
198 |
if request.serialization == 'xml': |
|
199 |
data = render_to_string('objects.xml', {'container': v_container, 'objects': objects}) |
|
200 |
elif request.serialization == 'json': |
|
201 |
data = json.dumps(objects) |
|
202 |
else: |
|
203 |
data = '\n'.join(x['name'] for x in objects) |
|
204 |
|
|
205 |
return HttpResponse(data, status = 200) |
|
206 |
|
|
207 |
@api_method('HEAD') |
|
208 |
def object_meta(request, v_account, v_container, v_object): |
|
209 |
# Normal Response Codes: 204 |
|
210 |
# Error Response Codes: serviceUnavailable (503), |
|
211 |
# itemNotFound (404), |
|
212 |
# unauthorized (401), |
|
213 |
# badRequest (400) |
|
214 |
|
|
215 |
info = get_object_meta(request.user, v_container, v_object) |
|
216 |
|
|
217 |
response = HttpResponse(status = 204) |
|
218 |
response['ETag'] = info['hash'] |
|
219 |
response['Content-Length'] = info['bytes'] |
|
220 |
response['Content-Type'] = info['content_type'] |
|
221 |
response['Last-Modified'] = http_date(info['last_modified']) |
|
222 |
for k, v in info['meta'].iteritems(): |
|
223 |
response['X-Object-Meta-%s' % k.capitalize()] = v |
|
224 |
|
|
225 |
return response |
|
226 |
|
|
227 |
@api_method('GET') |
|
228 |
def object_read(request, v_account, v_container, v_object): |
|
229 |
# Normal Response Codes: 200, 206 |
|
230 |
# Error Response Codes: serviceUnavailable (503), |
|
231 |
# rangeNotSatisfiable (416), |
|
232 |
# preconditionFailed (412), |
|
233 |
# itemNotFound (404), |
|
234 |
# unauthorized (401), |
|
235 |
# badRequest (400), |
|
236 |
# notModified (304) |
|
237 |
|
|
238 |
info = get_object_meta(request.user, v_container, v_object) |
|
239 |
|
|
240 |
response = HttpResponse() |
|
241 |
response['ETag'] = info['hash'] |
|
242 |
response['Content-Type'] = info['content_type'] |
|
243 |
response['Last-Modified'] = http_date(info['last_modified']) |
|
244 |
|
|
245 |
# Range handling. |
|
246 |
range = get_range(request) |
|
247 |
if range is not None: |
|
248 |
offset, length = range |
|
249 |
if not length: |
|
250 |
length = 0 |
|
251 |
if offset + length > info['bytes']: |
|
252 |
raise RangeNotSatisfiable() |
|
253 |
|
|
254 |
response['Content-Length'] = length |
|
255 |
response.status_code = 206 |
|
256 |
else: |
|
257 |
offset = 0 |
|
258 |
length = 0 |
|
259 |
|
|
260 |
response['Content-Length'] = info['bytes'] |
|
261 |
response.status_code = 200 |
|
262 |
|
|
263 |
# Conditions (according to RFC2616 must be evaluated at the end). |
|
264 |
if_match = request.META.get('HTTP_IF_MATCH') |
|
265 |
if if_match is not None and if_match != '*': |
|
266 |
if info['hash'] not in parse_etags(if_match): |
|
267 |
raise PreconditionFailed() |
|
268 |
|
|
269 |
if_none_match = request.META.get('HTTP_IF_NONE_MATCH') |
|
270 |
# if if_none_match is not None: |
|
271 |
# if if_none_match = '*' or info['hash'] in parse_etags(if_none_match): |
|
272 |
# raise NotModified() |
|
273 |
|
|
274 |
if_modified_since = request.META.get('HTTP_IF_MODIFIED_SINCE') |
|
275 |
if if_modified_since is not None: |
|
276 |
if_modified_since = parse_http_date_safe(if_modified_since) |
|
277 |
if if_modified_since is not None and info['last_modified'] <= if_modified_since: |
|
278 |
raise NotModified() |
|
279 |
|
|
280 |
if_unmodified_since = request.META.get('HTTP_IF_UNMODIFIED_SINCE') |
|
281 |
if if_unmodified_since is not None: |
|
282 |
if_unmodified_since = parse_http_date_safe(if_unmodified_since) |
|
283 |
if if_unmodified_since is not None and info['last_modified'] > if_unmodified_since: |
|
284 |
raise PreconditionFailed() |
|
285 |
|
|
286 |
response.content = get_object_data(request.user, v_container, v_object, offset, length) |
|
287 |
return response |
|
288 |
|
|
289 |
@api_method('PUT') |
|
290 |
def object_write(request, v_account, v_container, v_object): |
|
291 |
# Normal Response Codes: 201 |
|
292 |
# Error Response Codes: serviceUnavailable (503), |
|
293 |
# unprocessableEntity (422), |
|
294 |
# lengthRequired (411), |
|
295 |
# itemNotFound (404), |
|
296 |
# unauthorized (401), |
|
297 |
# badRequest (400) |
|
298 |
|
|
299 |
copy_from = request.META.get('HTTP_X_COPY_FROM') |
|
300 |
if copy_from: |
|
301 |
parts = copy_from.split('/') |
|
302 |
if len(parts) < 3 or parts[0] != '': |
|
303 |
raise BadRequest('Bad X-Copy-From path.') |
|
304 |
copy_container = parts[1] |
|
305 |
copy_name = '/'.join(parts[2:]) |
|
306 |
|
|
307 |
info = get_object_meta(request.user, copy_container, copy_name) |
|
308 |
|
|
309 |
content_length = request.META.get('CONTENT_LENGTH') |
|
310 |
content_type = request.META.get('CONTENT_TYPE') |
|
311 |
if not content_length: |
|
312 |
raise LengthRequired() |
|
313 |
if content_type: |
|
314 |
info['content_type'] = content_type |
|
315 |
|
|
316 |
meta = get_object_meta(request) |
|
317 |
for k, v in meta.iteritems(): |
|
318 |
info['meta'][k] = v |
|
319 |
|
|
320 |
copy_object(request.user, copy_container, copy_name, v_container, v_object) |
|
321 |
update_object_meta(request.user, v_container, v_object, info) |
|
322 |
|
|
323 |
response = HttpResponse(status = 201) |
|
324 |
else: |
|
325 |
content_length = request.META.get('CONTENT_LENGTH') |
|
326 |
content_type = request.META.get('CONTENT_TYPE') |
|
327 |
if not content_length or not content_type: |
|
328 |
raise LengthRequired() |
|
329 |
|
|
330 |
meta = get_object_meta(request) |
|
331 |
info = {'bytes': content_length, 'content_type': content_type, 'meta': meta} |
|
332 |
|
|
333 |
etag = request.META.get('HTTP_ETAG') |
|
334 |
if etag: |
|
335 |
etag = parse_etags(etag)[0] # TODO: Unescape properly. |
|
336 |
info['hash'] = etag |
|
337 |
|
|
338 |
data = request.read() |
|
339 |
# TODO: Hash function. |
|
340 |
# etag = hash(data) |
|
341 |
# if info.get('hash') and info['hash'] != etag: |
|
342 |
# raise UnprocessableEntity() |
|
343 |
|
|
344 |
update_object_data(request.user, v_container, v_name, info, data) |
|
345 |
|
|
346 |
response = HttpResponse(status = 201) |
|
347 |
# response['ETag'] = etag |
|
348 |
|
|
349 |
return response |
|
350 |
|
|
351 |
@api_method('COPY') |
|
352 |
def object_copy(request, v_account, v_container, v_object): |
|
353 |
# Normal Response Codes: 201 |
|
354 |
# Error Response Codes: serviceUnavailable (503), |
|
355 |
# itemNotFound (404), |
|
356 |
# unauthorized (401), |
|
357 |
# badRequest (400) |
|
358 |
|
|
359 |
destination = request.META.get('HTTP_DESTINATION') |
|
360 |
if not destination: |
|
361 |
raise BadRequest('Missing Destination.'); |
|
362 |
|
|
363 |
parts = destination.split('/') |
|
364 |
if len(parts) < 3 or parts[0] != '': |
|
365 |
raise BadRequest('Bad Destination path.') |
|
366 |
dest_container = parts[1] |
|
367 |
dest_name = '/'.join(parts[2:]) |
|
368 |
|
|
369 |
info = get_object_meta(request.user, v_container, v_object) |
|
370 |
|
|
371 |
content_type = request.META.get('CONTENT_TYPE') |
|
372 |
if content_type: |
|
373 |
info['content_type'] = content_type |
|
374 |
|
|
375 |
meta = get_object_meta(request) |
|
376 |
for k, v in meta.iteritems(): |
|
377 |
info['meta'][k] = v |
|
378 |
|
|
379 |
copy_object(request.user, v_container, v_object, dest_container, dest_name) |
|
380 |
update_object_meta(request.user, dest_container, dest_name, info) |
|
381 |
|
|
382 |
response = HttpResponse(status = 201) |
|
383 |
|
|
384 |
@api_method('POST') |
|
385 |
def object_update(request, v_account, v_container, v_object): |
|
386 |
# Normal Response Codes: 202 |
|
387 |
# Error Response Codes: serviceUnavailable (503), |
|
388 |
# itemNotFound (404), |
|
389 |
# unauthorized (401), |
|
390 |
# badRequest (400) |
|
391 |
|
|
392 |
meta = get_object_meta(request) |
|
393 |
|
|
394 |
update_object_meta(request.user, v_container, v_object, meta) |
|
395 |
return HttpResponse(status = 202) |
|
396 |
|
|
397 |
@api_method('DELETE') |
|
398 |
def object_delete(request, v_account, v_container, v_object): |
|
399 |
# Normal Response Codes: 204 |
|
400 |
# Error Response Codes: serviceUnavailable (503), |
|
401 |
# itemNotFound (404), |
|
402 |
# unauthorized (401), |
|
403 |
# badRequest (400) |
|
404 |
|
|
405 |
delete_object(request.user, v_container, v_object) |
|
406 |
return HttpResponse(status = 204) |
|
407 |
|
|
408 |
@api_method() |
|
409 |
def method_not_allowed(request): |
|
410 |
raise BadRequest('Method not allowed.') |
/dev/null | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
from django.db import models |
|
6 |
|
|
7 |
class Container(models.Model): |
|
8 |
account = models.CharField(max_length = 256) |
|
9 |
name = models.CharField(max_length = 256) |
|
10 |
date_created = models.DateTimeField(auto_now_add = True) |
|
11 |
|
|
12 |
def __unicode__(self): |
|
13 |
return self.name |
|
14 |
|
|
15 |
class Object(models.Model): |
|
16 |
container = models.ForeignKey(Container) |
|
17 |
name = models.CharField(max_length = 1024) |
|
18 |
length = models.IntegerField() |
|
19 |
type = models.CharField(max_length = 256) |
|
20 |
hash = models.CharField(max_length = 256) |
|
21 |
data = models.FileField(upload_to = 'data', max_length = 256) |
|
22 |
date_created = models.DateTimeField(auto_now_add = True) |
|
23 |
date_modified = models.DateTimeField(auto_now = True) |
|
24 |
|
|
25 |
def __unicode__(self): |
|
26 |
return self.name |
|
27 |
|
|
28 |
class Metadata(models.Model): |
|
29 |
object = models.ForeignKey(Object) |
|
30 |
name = models.CharField(max_length = 256) |
|
31 |
value = models.CharField(max_length = 1024) |
|
32 |
date_created = models.DateTimeField(auto_now_add = True) |
|
33 |
date_modified = models.DateTimeField(auto_now = True) |
/dev/null | ||
---|---|---|
1 |
{% spaceless %} |
|
2 |
<?xml version="1.0" encoding="UTF-8"?> |
|
3 |
|
|
4 |
<account name="{{ account }}"> |
|
5 |
{% for container in containers %} |
|
6 |
<container> |
|
7 |
<name>{{ container.name }}</name> |
|
8 |
<count>{{ container.count }}</count> |
|
9 |
<bytes>{{ container.bytes }}</bytes> |
|
10 |
</container> |
|
11 |
{% endfor %} |
|
12 |
</account> |
|
13 |
{% endspaceless %} |
/dev/null | ||
---|---|---|
1 |
{% spaceless %} |
|
2 |
<?xml version="1.0" encoding="UTF-8"?> |
|
3 |
|
|
4 |
<container name="{{ container }}"> |
|
5 |
{% for object in objects %} |
|
6 |
<object> |
|
7 |
<name>{{ object.name }}</name> |
|
8 |
<hash>{{ object.hash }}</hash> |
|
9 |
<bytes>{{ object.bytes }}</bytes> |
|
10 |
<content_type>{{ object.content_type }}</content_type> |
|
11 |
<last_modified>{{ object.last_modified }}</last_modified> |
|
12 |
</object> |
|
13 |
{% endfor %} |
|
14 |
</container> |
|
15 |
{% endspaceless %} |
/dev/null | ||
---|---|---|
1 |
""" |
|
2 |
This file demonstrates two different styles of tests (one doctest and one |
|
3 |
unittest). These will both pass when you run "manage.py test". |
|
4 |
|
|
5 |
Replace these with more appropriate tests for your application. |
|
6 |
""" |
|
7 |
|
|
8 |
from django.test import TestCase |
|
9 |
|
|
10 |
class SimpleTest(TestCase): |
|
11 |
def test_basic_addition(self): |
|
12 |
""" |
|
13 |
Tests that 1 + 1 always equals 2. |
|
14 |
""" |
|
15 |
self.failUnlessEqual(1 + 1, 2) |
|
16 |
|
|
17 |
__test__ = {"doctest": """ |
|
18 |
Another way to test that 1 + 1 is equal to 2. |
|
19 |
|
|
20 |
>>> 1 + 1 == 2 |
|
21 |
True |
|
22 |
"""} |
|
23 |
|
/dev/null | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
from django.conf.urls.defaults import * |
|
6 |
|
|
7 |
# TODO: This only works when in this order. |
|
8 |
# TODO: Define which characters can be used in each "path" component. |
|
9 |
urlpatterns = patterns('pithos.api.functions', |
|
10 |
(r'^$', 'authenticate'), |
|
11 |
(r'^(?P<v_account>.+?)/(?P<v_container>.+?)/(?P<v_object>.+?)$', 'object_demux'), |
|
12 |
(r'^(?P<v_account>.+?)/(?P<v_container>.+?)$', 'container_demux'), |
|
13 |
(r'^(?P<v_account>.+?)$', 'account_demux') |
|
14 |
) |
/dev/null | ||
---|---|---|
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 |
/dev/null | ||
---|---|---|
1 |
# Create your views here. |
/dev/null | ||
---|---|---|
1 |
import os |
|
2 |
import sqlite3 |
|
3 |
import json |
|
4 |
|
|
5 |
basepath = '/Users/butters/src/pithos/backends/content' #full path |
|
6 |
if not os.path.exists(basepath): |
|
7 |
os.makedirs(basepath) |
|
8 |
db = '/'.join([basepath, 'db']) |
|
9 |
con = sqlite3.connect(db) |
|
10 |
# Create tables |
|
11 |
print 'Creating tables....' |
|
12 |
sql = '''create table if not exists objects(name varchar(2560))''' |
|
13 |
print sql |
|
14 |
con.execute(sql) |
|
15 |
# Save (commit) the changes |
|
16 |
con.commit() |
|
17 |
|
|
18 |
def create_container(name): |
|
19 |
""" creates a new container with the given name |
|
20 |
if it doesn't exists under the basepath """ |
|
21 |
fullname = '/'.join([basepath, name]) |
|
22 |
if not os.path.exists(fullname): |
|
23 |
os.chdir(basepath) |
|
24 |
os.mkdir(name) |
|
25 |
else: |
|
26 |
raise NameError('Container already exists') |
|
27 |
return |
|
28 |
|
|
29 |
def delete_container(name): |
|
30 |
""" deletes the container with the given name |
|
31 |
if it exists under the basepath """ |
|
32 |
fullname = '/'.join([basepath, name]) |
|
33 |
if not os.path.exists(fullname): |
|
34 |
raise NameError('Container does not exist') |
|
35 |
if not list_objects(name): |
|
36 |
raise Error('Container is not empty') |
|
37 |
else: |
|
38 |
os.chdir(basepath) |
|
39 |
os.rmdir(name) |
|
40 |
return |
|
41 |
|
|
42 |
def get_container_meta(name): |
|
43 |
""" returns a dictionary with the container metadata """ |
|
44 |
fullname = '/'.join([basepath, name]) |
|
45 |
if not os.path.exists(fullname): |
|
46 |
raise NameError('Container does not exist') |
|
47 |
contents = os.listdir(fullname) |
|
48 |
count = len(contents) |
|
49 |
size = sum(os.path.getsize('/'.join([basepath, name, objectname])) for objectname in contents) |
|
50 |
return {'name': name, 'count': count, 'bytes': size} |
|
51 |
|
|
52 |
def list_containers(): |
|
53 |
return os.listdir(basepath) |
|
54 |
|
|
55 |
def list_objects(container, prefix='', delimiter=None): |
|
56 |
dir = '/'.join([basepath, container]) |
|
57 |
if not os.path.exists(dir): |
|
58 |
raise NameError('Container does not exist') |
|
59 |
search_str = '' |
|
60 |
if prefix: |
|
61 |
search_str = '/'.join([search_str, prefix]) |
|
62 |
#if delimiter: |
|
63 |
if None: |
|
64 |
search_str = ''.join(['%', search_str, '%', delimiter]) |
|
65 |
print search_str |
|
66 |
c = con.execute('select * from objects where name like ''?'' order by name', (search_str,)) |
|
67 |
else: |
|
68 |
search_str = ''.join(['%', search_str, '%']) |
|
69 |
print search_str |
|
70 |
c = con.execute('select * from objects where name like ''?'' order by name', (search_str,)) |
|
71 |
l = [] |
|
72 |
for row in c.fetchall(): |
|
73 |
s = '' |
|
74 |
print row[0] |
|
75 |
rest = str(row[0]).split(prefix)[1] |
|
76 |
print rest |
|
77 |
#if delimiter: |
|
78 |
# rest = rest.partition(delimiter)[0] |
|
79 |
#print rest |
|
80 |
folders = rest.split('/')[:-1] |
|
81 |
for folder in folders: |
|
82 |
path = ''.join([s, folder, '/']) |
|
83 |
if path not in l: |
|
84 |
l.append(path) |
|
85 |
s = ''.join([s, folder, '/']) |
|
86 |
l.append(rest) |
|
87 |
return l |
|
88 |
|
|
89 |
def get_object_meta(container, name): |
|
90 |
dir = '/'.join([basepath, container]) |
|
91 |
if not os.path.exists(dir): |
|
92 |
raise NameError('Container does not exist') |
|
93 |
else: |
|
94 |
os.chdir(dir) |
|
95 |
location = __get_object_linkinfo('/'.join([container, name])) |
|
96 |
location = '.'.join([location, 'meta']) |
|
97 |
f = open(location, 'r') |
|
98 |
data = json.load(f) |
|
99 |
f.close() |
|
100 |
return data |
|
101 |
|
|
102 |
def get_object_data(container, name, offset=0, length=-1): |
|
103 |
dir = '/'.join([basepath, container]) |
|
104 |
if not os.path.exists(dir): |
|
105 |
raise NameError('Container does not exist') |
|
106 |
else: |
|
107 |
os.chdir(dir) |
|
108 |
location = __get_object_linkinfo('/'.join([container, name])) |
|
109 |
f = open(location, 'r') |
|
110 |
if offset: |
|
111 |
f.seek(offset) |
|
112 |
data = f.read(length) |
|
113 |
f.close() |
|
114 |
return data |
|
115 |
|
|
116 |
def update_object(container, name, data): |
|
117 |
dir = '/'.join([basepath, container]) |
|
118 |
if not os.path.exists(dir): |
|
119 |
raise NameError('Container does not exist') |
|
120 |
try: |
|
121 |
location = __get_object_linkinfo('/'.join([container, name])) |
|
122 |
except NameError: |
|
123 |
# new object |
|
124 |
location = str(__save_linkinfo('/'.join([container, name]))) |
|
125 |
print ':'.join(['Creating new location', location]) |
|
126 |
__store_data(location, container, data) |
|
127 |
return |
|
128 |
|
|
129 |
def update_object_meta(container, name, meta): |
|
130 |
dir = '/'.join([basepath, container]) |
|
131 |
if not os.path.exists(dir): |
|
132 |
raise NameError('Container does not exist') |
|
133 |
try: |
|
134 |
location = __get_object_linkinfo('/'.join([container, name])) |
|
135 |
except NameError: |
|
136 |
# new object |
|
137 |
location = str(__save_linkinfo('/'.join([container, name]))) |
|
138 |
print ':'.join(['Creating new location', location]) |
|
139 |
__store_metadata(location, container, meta) |
|
140 |
return |
|
141 |
|
|
142 |
def copy_object(src_container, src_name, dest_container, dest_name, meta): |
|
143 |
fullname = '/'.join([basepath, dest_container]) |
|
144 |
if not os.path.exists(fullname): |
|
145 |
raise NameError('Destination container does not exist') |
|
146 |
update_object(dest_container, dest_name, get_object_data(src_container, src_name)) |
|
147 |
src_object_meta = get_object_meta(src_container, src_name) |
|
148 |
if (type(src_object_meta) == types.DictType): |
|
149 |
distinct_keys = [k for k in src_object_meta.keys() if k not in meta.keys()] |
|
150 |
for k in distinct_keys: |
|
151 |
meta[k] = src_object_meta[k] |
|
152 |
update_object_meta(dest_container, dest_name, meta) |
|
153 |
else: |
|
154 |
update_object_meta(dest_container, dest_name, meta) |
|
155 |
return |
|
156 |
|
|
157 |
def delete_object(container, name): |
|
158 |
return |
|
159 |
|
|
160 |
def __store_metadata(location, container, meta): |
|
161 |
dir = '/'.join([basepath, container]) |
|
162 |
if not os.path.exists(dir): |
|
163 |
raise NameError('Container does not exist') |
|
164 |
else: |
|
165 |
os.chdir(dir) |
|
166 |
location = '.'.join([location, 'meta']) |
|
167 |
f = open(location, 'w') |
|
168 |
data = json.dumps(meta) |
|
169 |
f.write(data) |
|
170 |
f.close() |
|
171 |
|
|
172 |
def __store_data(location, container, data): |
|
173 |
dir = '/'.join([basepath, container]) |
|
174 |
if not os.path.exists(dir): |
|
175 |
raise NameError('Container does not exist') |
|
176 |
else: |
|
177 |
os.chdir(dir) |
|
178 |
f = open(location, 'w') |
|
179 |
f.write(data) |
|
180 |
f.close() |
|
181 |
|
|
182 |
def __get_object_linkinfo(name): |
|
183 |
c = con.execute('select rowid from objects where name=''?''', (name,)) |
|
184 |
row = c.fetchone() |
|
185 |
if row: |
|
186 |
return str(row[0]) |
|
187 |
else: |
|
188 |
raise NameError('Object not found') |
|
189 |
|
|
190 |
def __save_linkinfo(name): |
|
191 |
id = con.execute('insert into objects(name) values(?)', (name,)).lastrowid |
|
192 |
con.commit() |
|
193 |
return id |
|
194 |
|
|
195 |
if __name__ == '__main__': |
|
196 |
dirname = 'papagian' |
|
197 |
#create_container(dirname) |
|
198 |
#assert os.path.exists(dirname) |
|
199 |
#assert os.path.isdir(dirname) |
|
200 |
|
|
201 |
#print get_container_meta(dirname) |
|
202 |
|
|
203 |
#update_object_meta(dirname, 'photos/animals/dog.jpg', {'name':'dog.jpg'}) |
|
204 |
#update_object_meta(dirname, 'photos/animals/dog.jpg', {'name':'dog.jpg', 'type':'image', 'size':400}) |
|
205 |
#print get_object_meta(dirname, 'photos/animals/dog.jpg') |
|
206 |
|
|
207 |
#f = open('dummy.py') |
|
208 |
#data = f.read() |
|
209 |
#update_object(dirname, 'photos/animals/dog.jpg', data) |
|
210 |
#update_object(dirname, 'photos/animals/cat.jpg', data) |
|
211 |
#update_object(dirname, 'photos/animals/thumbs/cat.jpg', data) |
|
212 |
#update_object(dirname, 'photos/fruits/banana.jpg', data) |
|
213 |
|
|
214 |
#print list_objects(dirname, 'photos/animals'); |
|
215 |
|
|
216 |
copy_object(dirname, 'photos/animals/dog.jpg', 'photos/animals/dog2.jpg') |
|
217 |
copy_object(dirname, 'photos/animals/dg.jpg', 'photos/animals/dog2.jpg') |
|
218 |
|
/dev/null | ||
---|---|---|
1 |
""" |
|
2 |
Dummy backend with debugging output |
|
3 |
|
|
4 |
A backend with no functionality other than producing debugging output. |
|
5 |
""" |
|
6 |
|
|
7 |
import logging |
|
8 |
|
|
9 |
def binary_search_name(a, x, lo = 0, hi = None): |
|
10 |
""" |
|
11 |
Search a sorted array of dicts for the value of the key 'name'. |
|
12 |
Raises ValueError if the value is not found. |
|
13 |
|
|
14 |
a -- the array |
|
15 |
x -- the value to search for |
|
16 |
""" |
|
17 |
if hi is None: |
|
18 |
hi = len(a) |
|
19 |
while lo < hi: |
|
20 |
mid = (lo + hi) // 2 |
|
21 |
midval = a[mid]['name'] |
|
22 |
if midval < x: |
|
23 |
lo = mid + 1 |
|
24 |
elif midval > x: |
|
25 |
hi = mid |
|
26 |
else: |
|
27 |
return mid |
|
28 |
raise ValueError() |
|
29 |
|
|
30 |
def get_account_meta(account): |
|
31 |
logging.debug("get_account_meta: %s %s", account, name) |
|
32 |
return {'count': 13, 'bytes': 3148237468} |
|
33 |
|
|
34 |
def create_container(account, name): |
|
35 |
""" |
|
36 |
Returns True if the container was created, False if it already exists. |
|
37 |
""" |
|
38 |
logging.debug("create_container: %s %s", account, name) |
|
39 |
return True |
|
40 |
|
|
41 |
def delete_container(account, name): |
|
42 |
logging.debug("delete_container: %s %s", account, name) |
|
43 |
return |
|
44 |
|
|
45 |
def get_container_meta(account, name): |
|
46 |
logging.debug("get_container_meta: %s %s", account, name) |
|
47 |
return {'count': 22, 'bytes': 245} |
|
48 |
|
|
49 |
def list_containers(account, marker = None, limit = 10000): |
|
50 |
logging.debug("list_containers: %s %s %s", account, marker, limit) |
|
51 |
|
|
52 |
containers = [ |
|
53 |
{'name': '1', 'count': 2, 'bytes': 123}, |
|
54 |
{'name': '2', 'count': 22, 'bytes': 245}, |
|
55 |
{'name': '3', 'count': 222, 'bytes': 83745}, |
|
56 |
{'name': 'four', 'count': 2222, 'bytes': 274365} |
|
57 |
] |
|
58 |
|
|
59 |
start = 0 |
|
60 |
if marker: |
|
61 |
try: |
|
62 |
start = binary_search_name(containers, marker) + 1 |
|
63 |
except ValueError: |
|
64 |
pass |
|
65 |
if not limit or limit > 10000: |
|
66 |
limit = 10000 |
|
67 |
|
|
68 |
return containers[start:start + limit] |
|
69 |
|
|
70 |
def list_objects(account, container, prefix = None, delimiter = None, marker = None, limit = 10000): |
|
71 |
logging.debug("list_objects: %s %s %s %s %s %s", account, container, prefix, delimiter, marker, limit) |
|
72 |
|
|
73 |
objects = [ |
|
74 |
{'name': 'other', 'hash': 'dfgs', 'bytes': 0, 'content_type': 'application/directory', 'last_modified': 23453454}, |
|
75 |
{'name': 'other/something', 'hash': 'vkajf', 'bytes': 234, 'content_type': 'application/octet-stream', 'last_modified': 878434562}, |
|
76 |
{'name': 'photos', 'hash': 'kajdsn', 'bytes': 0, 'content_type': 'application/directory', 'last_modified': 1983274}, |
|
77 |
{'name': 'photos/asdf', 'hash': 'jadsfkj', 'bytes': 0, 'content_type': 'application/directory', 'last_modified': 378465873}, |
|
78 |
{'name': 'photos/asdf/test', 'hash': 'sudfhius', 'bytes': 37284, 'content_type': 'text/plain', 'last_modified': 93674212}, |
|
79 |
{'name': 'photos/me.jpg', 'hash': 'sdgsdfgsf', 'bytes': 534, 'content_type': 'image/jpeg', 'last_modified': 262345345}, |
|
80 |
{'name': 'photos/text.txt', 'hash': 'asdfasd', 'bytes': 34243, 'content_type': 'text/plain', 'last_modified': 45345345} |
|
81 |
] |
|
82 |
|
|
83 |
if prefix or delimiter: |
|
84 |
if prefix: |
|
85 |
objects = [x for x in objects if x['name'].startswith(prefix)] |
|
86 |
if delimiter: |
|
87 |
pseudo_objects = {} |
|
88 |
for x in objects: |
|
89 |
pseudo_name = x['name'][len(prefix):] |
|
90 |
i = pseudo_name.find(delimiter) |
|
91 |
if i != -1: |
|
92 |
pseudo_name = pseudo_name[:i] |
|
93 |
# TODO: Virtual directories. |
|
94 |
if pseudo_name not in pseudo_objects: |
|
95 |
pseudo_objects[pseudo_name] = x |
|
96 |
objects = sorted(pseudo_objects.values(), key=lambda o: o['name']) |
|
97 |
|
|
98 |
start = 0 |
|
99 |
if marker: |
|
100 |
try: |
|
101 |
start = binary_search_name(objects, marker) + 1 |
|
102 |
except ValueError: |
|
103 |
pass |
|
104 |
if not limit or limit > 10000: |
|
105 |
limit = 10000 |
|
106 |
|
|
107 |
return objects[start:start + limit] |
|
108 |
|
|
109 |
def get_object_meta(account, container, name): |
|
110 |
logging.debug("get_object_meta: %s %s %s", account, container, name) |
|
111 |
meta = {'meat': 'bacon', 'fruit': 'apple'} |
|
112 |
return {'hash': 'asdfasd', 'bytes': 34243, 'content_type': 'text/plain', 'last_modified': 45345345, 'meta': meta} |
|
113 |
|
|
114 |
def update_object_meta(account, container, name, meta): |
|
115 |
logging.debug("update_object_meta: %s %s %s %s", account, container, name, meta) |
|
116 |
for k, v in meta.iteritems(): |
|
117 |
pass |
|
118 |
return |
|
119 |
|
|
120 |
def get_object_data(account, container, name, offset=0, length=0): |
|
121 |
logging.debug("get_object_data: %s %s %s %s %s", account, container, name, offset, length) |
|
122 |
return '' |
|
123 |
|
|
124 |
def update_object_data(account, container, name, meta, data): |
|
125 |
logging.debug("update_object_data: %s %s %s %s %s", account, container, name, meta, data) |
|
126 |
return |
|
127 |
|
|
128 |
def copy_object(account, container, name, new_container, new_name): |
|
129 |
logging.debug("copy_object: %s %s %s %s %s", account, container, name, new_container, new_name) |
|
130 |
return |
|
131 |
|
|
132 |
def delete_object(account, container, name): |
|
133 |
logging.debug("delete_object: %s %s %s", account, container, name) |
|
134 |
return |
/dev/null | ||
---|---|---|
1 |
#!/usr/bin/env python |
|
2 |
from django.core.management import execute_manager |
|
3 |
try: |
|
4 |
import settings # Assumed to be in the same directory. |
|
5 |
except ImportError: |
|
6 |
import sys |
|
7 |
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) |
|
8 |
sys.exit(1) |
|
9 |
|
|
10 |
if __name__ == "__main__": |
|
11 |
execute_manager(settings) |
b/pithos/api/admin.py | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
from api.models import Container, Object, Metadata |
|
6 |
from django.contrib import admin |
|
7 |
|
|
8 |
admin.site.register(Container) |
|
9 |
admin.site.register(Object) |
|
10 |
admin.site.register(Metadata) |
b/pithos/api/faults.py | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
def camelCase(s): |
|
6 |
return s[0].lower() + s[1:] |
|
7 |
|
|
8 |
|
|
9 |
class Fault(Exception): |
|
10 |
def __init__(self, message='', details='', name=''): |
|
11 |
Exception.__init__(self, message, details, name) |
|
12 |
self.message = message |
|
13 |
self.details = details |
|
14 |
self.name = name or camelCase(self.__class__.__name__) |
|
15 |
|
|
16 |
class NotModified(Fault): |
|
17 |
code = 304 |
|
18 |
|
|
19 |
class BadRequest(Fault): |
|
20 |
code = 400 |
|
21 |
|
|
22 |
class Unauthorized(Fault): |
|
23 |
code = 401 |
|
24 |
|
|
25 |
class ResizeNotAllowed(Fault): |
|
26 |
code = 403 |
|
27 |
|
|
28 |
class ItemNotFound(Fault): |
|
29 |
code = 404 |
|
30 |
|
|
31 |
class LengthRequired(Fault): |
|
32 |
code = 411 |
|
33 |
|
|
34 |
class PreconditionFailed(Fault): |
|
35 |
code = 412 |
|
36 |
|
|
37 |
class RangeNotSatisfiable(Fault): |
|
38 |
code = 416 |
|
39 |
|
|
40 |
class UnprocessableEntity(Fault): |
|
41 |
code = 422 |
|
42 |
|
|
43 |
class ServiceUnavailable(Fault): |
|
44 |
code = 503 |
b/pithos/api/functions.py | ||
---|---|---|
1 |
# |
|
2 |
# Copyright (c) 2011 Greek Research and Technology Network |
|
3 |
# |
|
4 |
|
|
5 |
from django.http import HttpResponse |
|
6 |
from django.template.loader import render_to_string |
|
7 |
from django.utils import simplejson as json |
|
8 |
from django.utils.http import http_date, parse_etags |
|
9 |
|
|
10 |
try: |
|
11 |
from django.utils.http import parse_http_date_safe |
|
12 |
except: |
|
13 |
from pithos.api.util import parse_http_date_safe |
|
14 |
|
|
15 |
from pithos.api.faults import Fault, NotModified, BadRequest, Unauthorized, LengthRequired, PreconditionFailed, RangeNotSatisfiable, UnprocessableEntity |
|
16 |
from pithos.api.util import get_object_meta, get_range, api_method |
|
17 |
|
|
18 |
from pithos.backends.dummy_debug import * |
|
19 |
|
|
20 |
import logging |
|
21 |
|
|
22 |
logging.basicConfig(level=logging.DEBUG) |
|
23 |
|
|
24 |
@api_method('GET') |
|
25 |
def authenticate(request): |
|
26 |
# Normal Response Codes: 204 |
|
27 |
# Error Response Codes: serviceUnavailable (503), |
|
28 |
# unauthorized (401), |
|
29 |
# badRequest (400) |
|
30 |
|
|
31 |
x_auth_user = request.META.get('HTTP_X_AUTH_USER') |
|
32 |
x_auth_key = request.META.get('HTTP_X_AUTH_KEY') |
|
33 |
|
|
34 |
if not x_auth_user or not x_auth_key: |
|
35 |
raise BadRequest('Missing auth user or key.') |
|
36 |
|
|
37 |
response = HttpResponse(status = 204) |
|
38 |
response['X-Auth-Token'] = 'eaaafd18-0fed-4b3a-81b4-663c99ec1cbb' |
|
39 |
# TODO: Do we support redirections? |
|
40 |
#response['X-Storage-Url'] = 'https://storage.grnet.gr/pithos/v1.0/<some reference>' |
|
41 |
return response |
|
42 |
|
|
43 |
def account_demux(request, v_account): |
|
44 |
if request.method == 'HEAD': |
|
45 |
return account_meta(request, v_account) |
|
46 |
elif request.method == 'GET': |
|
47 |
return container_list(request, v_account) |
|
48 |
else: |
|
49 |
return method_not_allowed(request) |
|
50 |
|
|
51 |
def container_demux(request, v_account, v_container): |
|
52 |
if request.method == 'HEAD': |
|
53 |
return container_meta(request, v_account, v_container) |
|
54 |
elif request.method == 'GET': |
|
55 |
return object_list(request, v_account, v_container) |
|
56 |
elif request.method == 'PUT': |
|
57 |
return container_create(request, v_account, v_container) |
|
58 |
elif request.method == 'DELETE': |
|
59 |
return container_delete(request, v_account, v_container) |
|
60 |
else: |
|
61 |
return method_not_allowed(request) |
|
62 |
|
|
63 |
def object_demux(request, v_account, v_container, v_object): |
|
64 |
if request.method == 'HEAD': |
|
65 |
return object_meta(request, v_account, v_container, v_object) |
|
66 |
elif request.method == 'GET': |
|
67 |
return object_read(request, v_account, v_container, v_object) |
|
68 |
elif request.method == 'PUT': |
|
69 |
return object_write(request, v_account, v_container, v_object) |
|
70 |
elif request.method == 'COPY': |
|
71 |
return object_copy(request, v_account, v_container, v_object) |
|
72 |
elif request.method == 'POST': |
|
73 |
return object_update(request, v_account, v_container, v_object) |
|
74 |
elif request.method == 'DELETE': |
|
75 |
return object_delete(request, v_account, v_container, v_object) |
|
76 |
else: |
|
77 |
return method_not_allowed(request) |
|
78 |
|
|
79 |
@api_method('HEAD') |
|
80 |
def account_meta(request, v_account): |
|
81 |
# Normal Response Codes: 204 |
|
82 |
# Error Response Codes: serviceUnavailable (503), |
|
83 |
# itemNotFound (404), |
|
84 |
# unauthorized (401), |
|
85 |
# badRequest (400) |
|
86 |
|
|
87 |
container_count, bytes_count = get_account_meta(request.user) |
|
88 |
|
|
89 |
response = HttpResponse(status = 204) |
|
90 |
response['X-Account-Container-Count'] = container_count |
|
91 |
response['X-Account-Total-Bytes-Used'] = bytes_count |
|
92 |
return response |
|
93 |
|
|
94 |
@api_method('GET', format_allowed = True) |
|
95 |
def container_list(request, v_account): |
|
96 |
# Normal Response Codes: 200, 204 |
|
97 |
# Error Response Codes: serviceUnavailable (503), |
|
98 |
# unauthorized (401), |
|
99 |
# badRequest (400) |
|
100 |
|
|
101 |
marker = request.GET.get('marker') |
|
102 |
limit = request.GET.get('limit') |
|
103 |
if limit: |
|
104 |
try: |
|
105 |
limit = int(limit) |
|
106 |
except ValueError: |
|
107 |
limit = None |
|
108 |
|
|
109 |
containers = list_containers(request.user, marker, limit) |
|
110 |
if len(containers) == 0: |
|
111 |
return HttpResponse(status = 204) |
|
112 |
|
|
113 |
if request.serialization == 'xml': |
|
114 |
data = render_to_string('containers.xml', {'account': request.user, 'containers': containers}) |
|
115 |
elif request.serialization == 'json': |
|
116 |
data = json.dumps(containers) |
|
117 |
else: |
|
118 |
data = '\n'.join(x['name'] for x in containers) |
|
119 |
|
|
120 |
return HttpResponse(data, status = 200) |
|
121 |
|
|
122 |
@api_method('HEAD') |
|
123 |
def container_meta(request, v_account, v_container): |
|
124 |
# Normal Response Codes: 204 |
|
125 |
# Error Response Codes: serviceUnavailable (503), |
|
126 |
# itemNotFound (404), |
|
127 |
# unauthorized (401), |
|
128 |
# badRequest (400) |
|
129 |
|
|
130 |
object_count, bytes_count = get_container_meta(request.user, v_container) |
|
131 |
|
|
132 |
response = HttpResponse(status = 204) |
Also available in: Unified diff