# Copyright (C) 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 .
import logging
import mimetypes
from os.path import split
from sugar_network import static, db
from sugar_network.toolkit.router import route, fallbackroute, Blob, ACL
from sugar_network.toolkit import coroutine
_logger = logging.getLogger('model.routes')
class VolumeRoutes(db.Routes):
@route('GET', ['context', None], cmd='feed',
mime_type='application/json')
def feed(self, request, distro):
context = self.volume['context'].get(request.guid)
releases = self.volume['release']
versions = []
impls, __ = releases.find(context=context.guid,
not_layer='deleted', **request)
for impl in impls:
version = impl.properties([
'guid', 'ctime', 'layer', 'author', 'tags',
'version', 'stability', 'license', 'notes',
])
if context['dependencies']:
requires = version.setdefault('requires', {})
for i in context['dependencies']:
requires.setdefault(i, {})
version['data'] = data = impl.meta('data')
for key in ('mtime', 'seqno', 'blob'):
if key in data:
del data[key]
versions.append(version)
result = {'releases': versions}
if distro:
aliases = context['aliases'].get(distro)
if aliases and 'binary' in aliases:
result['packages'] = aliases['binary']
return result
class FrontRoutes(object):
def __init__(self):
self._pooler = _Pooler()
@route('GET', mime_type='text/html')
def hello(self):
return _HELLO_HTML
@route('OPTIONS')
def options(self, request, response):
if request.environ['HTTP_ORIGIN']:
response['Access-Control-Allow-Methods'] = \
request.environ['HTTP_ACCESS_CONTROL_REQUEST_METHOD']
response['Access-Control-Allow-Headers'] = \
request.environ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']
else:
response['Allow'] = 'GET, HEAD, POST, PUT, DELETE'
response.content_length = 0
@route('GET', cmd='subscribe', mime_type='text/event-stream')
def subscribe(self, request=None, response=None, ping=False, **condition):
"""Subscribe to Server-Sent Events."""
if request is not None and not condition:
condition = request
if response is not None:
response.content_type = 'text/event-stream'
response['Cache-Control'] = 'no-cache'
return self._pull_events(request, ping, condition)
@route('POST', cmd='broadcast',
mime_type='application/json', acl=ACL.LOCAL)
def broadcast(self, event=None, request=None):
if request is not None:
event = request.content
_logger.debug('Broadcast event: %r', event)
self._pooler.notify_all(event)
@fallbackroute('GET', ['static'])
def get_static(self, request):
path = static.path(*request.path[1:])
if not mimetypes.inited:
mimetypes.init()
mime_type = mimetypes.types_map.get('.' + path.rsplit('.', 1)[-1])
return Blob({
'blob': path,
'filename': split(path)[-1],
'mime_type': mime_type,
})
@route('GET', ['robots.txt'], mime_type='text/plain')
def robots(self, request, response):
return 'User-agent: *\nDisallow: /\n'
@route('GET', ['favicon.ico'])
def favicon(self, request, response):
return Blob({
'blob': static.path('favicon.ico'),
'mime_type': 'image/x-icon',
})
def _pull_events(self, request, ping, condition):
_logger.debug('Start subscription, total=%s', self._pooler.waiters + 1)
if ping:
# XXX The whole commands' kwargs handling should be redesigned
if 'ping' in condition:
condition.pop('ping')
# If non-greenlet application needs only to initiate
# a subscription and do not stuck in waiting for the first event,
# it should pass `ping` argument to return fake event to unblock
# `GET /?cmd=subscribe` call.
yield {'event': 'pong'}
rfile = None
if request is not None:
rfile = request.content_stream
if rfile is not None:
coroutine.spawn(self._waiter_for_closing, rfile)
while True:
event = self._pooler.wait()
if not isinstance(event, dict):
if event is rfile:
break
else:
continue
for key, value in condition.items():
if value.startswith('!'):
if event.get(key) == value[1:]:
break
elif event.get(key) != value:
break
else:
yield event
_logger.debug('Stop subscription, total=%s', self._pooler.waiters)
def _waiter_for_closing(self, rfile):
try:
coroutine.select([rfile.fileno()], [], [])
finally:
self._pooler.notify_all(rfile)
class _Pooler(object):
"""One-producer-to-many-consumers events delivery."""
def __init__(self):
self._value = None
self._waiters = 0
self._ready = coroutine.Event()
self._open = coroutine.Event()
self._open.set()
@property
def waiters(self):
return self._waiters
def wait(self):
self._open.wait()
self._waiters += 1
try:
self._ready.wait()
finally:
self._waiters -= 1
if self._waiters == 0:
self._ready.clear()
self._open.set()
return self._value
def notify_all(self, value=None):
self._open.wait()
if not self._waiters:
return
self._open.clear()
self._value = value
self._ready.set()
_HELLO_HTML = """\
Welcome to Sugar Network API!
Visit the
Sugar Labs Wiki to learn how it can be used.
"""