diff options
author | Aleksey Lim <alsroot@sugarlabs.org> | 2013-07-27 17:25:48 (GMT) |
---|---|---|
committer | Aleksey Lim <alsroot@sugarlabs.org> | 2013-07-27 17:25:48 (GMT) |
commit | fa35499bcd89260b436c3c9f03c887661983c14d (patch) | |
tree | 7401fbbbbefce5885bb1fb5b52bbc17f8c0dfe72 /sugar_network/db/routes.py | |
parent | 35d777deda94fbc78324c447e16bb7abf0b11434 (diff) |
Polish implementation
Diffstat (limited to 'sugar_network/db/routes.py')
-rw-r--r-- | sugar_network/db/routes.py | 381 |
1 files changed, 381 insertions, 0 deletions
diff --git a/sugar_network/db/routes.py b/sugar_network/db/routes.py new file mode 100644 index 0000000..e61199a --- /dev/null +++ b/sugar_network/db/routes.py @@ -0,0 +1,381 @@ +# Copyright (C) 2011-2013 Aleksey Lim +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import sys +import time +import json +import types +import hashlib +import logging +from contextlib import contextmanager +from os.path import exists + +from sugar_network import toolkit +from sugar_network.db.metadata import BlobProperty, StoredProperty, LIST_TYPES +from sugar_network.toolkit.router import Blob, ACL, route +from sugar_network.toolkit import http, enforce + + +_GUID_RE = re.compile('[a-zA-Z0-9_+-.]+$') + +_logger = logging.getLogger('db.routes') + + +class Routes(object): + + def __init__(self, volume): + self.volume = volume + + @route('POST', [None], + acl=ACL.AUTH, mime_type='application/json') + def create(self, request): + with self._post(request, ACL.CREATE) as (directory, doc): + event = {} + self.on_create(request, doc.props, event) + if 'guid' not in doc.props: + doc.props['guid'] = toolkit.uuid() + doc.guid = doc.props['guid'] + directory.create(doc.props, event) + return doc.guid + + @route('GET', [None], + arguments={'offset': int, 'limit': int, 'reply': ('guid',)}, + mime_type='application/json') + def find(self, request, reply): + self._preget(request) + documents, total = self.volume[request.resource].find(**request) + result = [self._get_props(i, request, reply) for i in documents] + return {'total': total, 'result': result} + + @route('GET', [None, None], cmd='exists', + mime_type='application/json') + def exists(self, request): + directory = self.volume[request.resource] + return directory.exists(request.guid) + + @route('PUT', [None, None], + acl=ACL.AUTH | ACL.AUTHOR) + def update(self, request): + with self._post(request, ACL.WRITE) as (directory, doc): + if not doc.props: + return + event = {} + self.on_update(request, doc.props, event) + directory.update(doc.guid, doc.props, event) + + @route('PUT', [None, None, None], + acl=ACL.AUTH | ACL.AUTHOR) + def update_prop(self, request, url=None): + if url: + value = Blob({'url': url}) + elif request.content is None: + value = request.content_stream + else: + value = request.content + request.content = {request.prop: value} + self.update(request) + + @route('DELETE', [None, None], + acl=ACL.AUTH | ACL.AUTHOR) + def delete(self, request): + self.volume[request.resource].delete(request.guid) + + @route('GET', [None, None], arguments={'reply': ('guid',)}, + mime_type='application/json') + def get(self, request, reply): + if not reply: + reply = [] + for prop in self.volume[request.resource].metadata.values(): + if prop.acl & ACL.READ and not (prop.acl & ACL.LOCAL): + reply.append(prop.name) + self._preget(request) + doc = self.volume[request.resource].get(request.guid) + return self._get_props(doc, request, reply) + + @route('GET', [None, None, None], mime_type='application/json') + def get_prop(self, request): + directory = self.volume[request.resource] + prop = directory.metadata[request.prop] + doc = directory.get(request.guid) + doc.request = request + + prop.assert_access(ACL.READ) + + if isinstance(prop, StoredProperty): + value = doc.get(prop.name, request.accept_language) + value = prop.on_get(doc, value) + if value is None: + value = prop.default + return value + else: + meta = prop.on_get(doc, doc.meta(prop.name)) + enforce(meta is not None and ('blob' in meta or 'url' in meta), + http.NotFound, 'BLOB does not exist') + return meta + + @route('HEAD', [None, None, None]) + def get_prop_meta(self, request, response): + directory = self.volume[request.resource] + prop = directory.metadata[request.prop] + doc = directory.get(request.guid) + doc.request = request + + prop.assert_access(ACL.READ) + + if isinstance(prop, StoredProperty): + meta = doc.meta(prop.name) + value = meta.pop('value') + response.content_length = len(json.dumps(value)) + else: + meta = prop.on_get(doc, doc.meta(prop.name)) + enforce(meta is not None and ('blob' in meta or 'url' in meta), + http.NotFound, 'BLOB does not exist') + if 'blob' in meta: + meta.pop('blob') + meta['url'] = '/'.join([request.static_prefix] + request.path) + response.content_length = meta['blob_size'] + + response.meta.update(meta) + response.last_modified = meta['mtime'] + + @route('PUT', [None, None], cmd='useradd', + arguments={'role': 0}, acl=ACL.AUTH | ACL.AUTHOR) + def useradd(self, request, user, role): + enforce(user, "Argument 'user' is not specified") + directory = self.volume[request.resource] + authors = directory.get(request.guid)['author'] + self._useradd(authors, user, role) + directory.update(request.guid, {'author': authors}) + + @route('PUT', [None, None], cmd='userdel', acl=ACL.AUTH | ACL.AUTHOR) + def userdel(self, request, user): + enforce(user, "Argument 'user' is not specified") + enforce(user != request.principal, 'Cannot remove yourself') + directory = self.volume[request.resource] + authors = directory.get(request.guid)['author'] + enforce(user in authors, 'No such user') + del authors[user] + directory.update(request.guid, {'author': authors}) + + def on_create(self, request, props, event): + if 'guid' in props: + # TODO Temporal security hole, see TODO + guid = props['guid'] + enforce(not self.volume[request.resource].exists(guid), + '%s already exists', guid) + enforce(_GUID_RE.match(guid) is not None, + 'Malformed %s GUID', guid) + + ts = int(time.time()) + props['ctime'] = ts + props['mtime'] = ts + + if request.principal: + authors = props['author'] = {} + self._useradd(authors, request.principal, ACL.ORIGINAL) + + def on_update(self, request, props, event): + props['mtime'] = int(time.time()) + + def after_post(self, doc): + pass + + @contextmanager + def _post(self, request, access): + content = request.content or {} + enforce(isinstance(content, dict), 'Invalid value') + + directory = self.volume[request.resource] + if request.guid: + doc = directory.get(request.guid) + else: + doc = directory.document_class(None, {}) + doc.request = request + blobs = [] + + for name, value in content.items(): + prop = directory.metadata[name] + if isinstance(prop, BlobProperty): + prop.assert_access(ACL.CREATE if + access == ACL.WRITE and doc.meta(name) is None + else access) + if value is None: + value = {'blob': None} + elif isinstance(value, dict): + enforce('url' in value, + 'Key %r is not specified in %r blob property', + 'url', name) + value = {'url': value['url']} + else: + value = _read_blob(request, prop, value) + blobs.append(value['blob']) + else: + prop.assert_access(access) + if prop.localized and isinstance(value, basestring): + value = {request.accept_language[0]: value} + try: + value = _typecast_prop_value(prop.typecast, value) + except Exception, error: + error = 'Value %r for %r property is invalid: %s' % \ + (value, prop.name, error) + toolkit.exception(error) + raise RuntimeError(error) + doc[name] = value + + if access == ACL.CREATE: + for name, prop in directory.metadata.items(): + if not isinstance(prop, BlobProperty) and \ + name not in content and \ + (prop.default is not None or prop.on_set is not None): + doc[name] = prop.default + + try: + for name, value in doc.props.items(): + prop = directory.metadata[name] + if prop.on_set is not None: + doc.props[name] = prop.on_set(doc, value) + yield directory, doc + finally: + for path in blobs: + if exists(path): + os.unlink(path) + + self.after_post(doc) + + def _preget(self, request): + reply = request['reply'] + if not reply: + request['reply'] = ('guid',) + else: + directory = self.volume[request.resource] + for prop in reply: + directory.metadata[prop].assert_access(ACL.READ) + + def _get_props(self, doc, request, props): + result = {} + metadata = doc.metadata + doc.request = request + for name in props: + prop = metadata[name] + value = prop.on_get(doc, doc.get(name, request.accept_language)) + if value is None: + value = prop.default + elif isinstance(value, Blob): + value = value.get('url') + if value is None: + value = '/'.join(['', metadata.name, doc.guid, name]) + if value.startswith('/'): + value = request.static_prefix + value + result[name] = value + return result + + def _useradd(self, authors, user, role): + props = {} + + users = self.volume['user'] + if users.exists(user): + props['name'] = users.get(user)['name'] + role |= ACL.INSYSTEM + else: + role &= ~ACL.INSYSTEM + props['role'] = role & (ACL.INSYSTEM | ACL.ORIGINAL) + + if user in authors: + authors[user].update(props) + else: + if authors: + top = max(authors.values(), key=lambda x: x['order']) + props['order'] = top['order'] + 1 + else: + props['order'] = 0 + authors[user] = props + + +def _read_blob(request, prop, value): + digest = hashlib.sha1() + dst = toolkit.NamedTemporaryFile(delete=False) + + try: + if isinstance(value, basestring): + digest.update(value) + dst.write(value) + else: + size = request.content_length or sys.maxint + while size > 0: + chunk = value.read(min(size, toolkit.BUFFER_SIZE)) + if not chunk: + break + dst.write(chunk) + size -= len(chunk) + digest.update(chunk) + except Exception: + os.unlink(dst.name) + raise + finally: + dst.close() + + return {'blob': dst.name, + 'digest': digest.hexdigest(), + 'mime_type': request.content_type or prop.mime_type, + } + + +def _typecast_prop_value(typecast, value): + if typecast is None: + return value + enforce(value is not None, ValueError, 'Property value cannot be None') + + def cast(typecast, value): + if isinstance(typecast, types.FunctionType): + return typecast(value) + elif typecast is unicode: + return value.encode('utf-8') + elif typecast is str: + return str(value) + elif typecast is int: + return int(value) + elif typecast is float: + return float(value) + elif typecast is bool: + return bool(value) + elif typecast is dict: + return dict(value) + else: + raise ValueError('Unknown typecast') + + if type(typecast) in LIST_TYPES: + if typecast: + first = iter(typecast).next() + else: + first = None + if first is not None and type(first) is not type and \ + type(first) not in LIST_TYPES: + value = cast(type(first), value) + enforce(value in typecast, ValueError, + "Value %r is not in '%s' list", + value, ', '.join([str(i) for i in typecast])) + else: + enforce(len(typecast) <= 1, ValueError, + 'List values should contain values of the same type') + if type(value) not in LIST_TYPES: + value = (value,) + typecast, = typecast or [str] + value = tuple([_typecast_prop_value(typecast, i) for i in value]) + else: + value = cast(typecast, value) + + return value |