oxcd/oxcd/server.py

219 lines
7.1 KiB
Python

# encoding: utf-8
# vi:si:et:sw=4:sts=4:ts=4
from __future__ import with_statement, division
import sys
import inspect
import os
import json
from urlparse import urlparse
import datetime
import zlib
from twisted.web.resource import Resource
from twisted.web.static import File
from twisted.web.util import Redirect
from twisted.web.error import NoResource
from twisted.web.server import NOT_DONE_YET
from version import __version__
from gziprequest import GzipFile, GzipRequest
def trim(docstring):
if not docstring:
return ''
# Convert tabs to spaces (following the normal Python rules)
# and split into a list of lines:
lines = docstring.expandtabs().splitlines()
# Determine minimum indentation (first line doesn't count):
indent = sys.maxint
for line in lines[1:]:
stripped = line.lstrip()
if stripped:
indent = min(indent, len(line) - len(stripped))
# Remove indentation (first line is special):
trimmed = [lines[0].strip()]
if indent < sys.maxint:
for line in lines[1:]:
trimmed.append(line[indent:].rstrip())
# Strip off trailing and leading blank lines:
while trimmed and not trimmed[-1]:
trimmed.pop()
while trimmed and not trimmed[0]:
trimmed.pop(0)
# Return a single string:
return '\n'.join(trimmed)
def _to_json(python_object):
if isinstance(python_object, datetime.datetime):
return python_object.strftime('%Y-%m-%dT%H:%M:%SZ')
raise TypeError(u'%s %s is not JSON serializable' % (repr(python_object), type(python_object)))
def json_response(data=None, status=200, text='ok'):
if not data:
data = {}
return {'status': {'code': status, 'text': text}, 'data': data}
class ApiActions(dict):
properties = {}
def __init__(self):
def api(backend, site, data):
'''
returns list of all known api actions
param data {
docs: bool
}
if docs is true, action properties contain docstrings
return {
status: {'code': int, 'text': string},
data: {
actions: {
'api': {
cache: true,
doc: 'recursion'
},
'hello': {
cache: true,
..
}
...
}
}
}
'''
docs = data.get('docs', False)
code = data.get('code', False)
_actions = self.keys()
_actions.sort()
actions = {}
for a in _actions:
actions[a] = self.properties[a]
if docs:
actions[a]['doc'] = self.doc(a)
if code:
actions[a]['code'] = self.code(a)
response = json_response({'actions': actions})
return response
self.register(api)
def doc(self, f):
return trim(self[f].__doc__)
def code(self, name):
f = self[name]
if name != 'api' and hasattr(f, 'func_closure') and f.func_closure:
f = f.func_closure[0].cell_contents
info = f.func_code.co_firstlineno
return info, trim(inspect.getsource(f))
def register(self, method, action=None, cache=True):
if not action:
action = method.func_name
self[action] = method
self.properties[action] = {'cache': cache}
def unregister(self, action):
if action in self:
del self[action]
def render(self, backend, site, action, data):
if action in self:
result = self[action](backend, site, data)
else:
result = json_response(status=404, text='not found')
#print result
return json.dumps(result, default=_to_json)
actions = ApiActions()
class Server(Resource):
def __init__(self, base, backend):
self.base = base
self.backend = backend
Resource.__init__(self)
def static_path(self, path):
return os.path.join(self.base, 'static', path)
def get_site(self, request):
headers = request.getAllHeaders()
#print headers
if 'origin' in headers:
request.headers['Access-Control-Allow-Origin'] = headers['origin']
site = headers['origin']
elif 'referer' in headers:
u = urlparse(headers['referer'])
site = u.scheme + '://' + u.hostname
else:
site = 'http://' + headers['host']
return site
def getChild(self, name, request):
if name in ('icon.png', 'favicon.ico'):
f = File(self.static_path('png/icon16.png'))
f.isLeaf = True
return f
if request.path == '/api/':
return self
if request.path == '/library.xml':
accept_encoding = request.getHeader('accept-encoding') or ''
if 'gzip' in accept_encoding:
f = GzipFile(self.backend.xml, 'application/xml')
else:
f = File(self.backend.xml, 'application/xml')
f.isLeaf = True
return f
if request.path.startswith('/track/'):
track_id = request.path.split('/')[-1].split('.')[0]
path = self.backend.track(track_id)
if path:
if os.path.exists(path):
request.headers['Access-Control-Allow-Origin'] = '*'
f = File(path, 'audio/mpeg')
f.isLeaf = True
return f
return NoResource()
path = request.path
path = path[1:]
if not path:
path = 'index.html'
path = self.static_path(path)
f = File(path)
if not os.path.isdir(path):
f.isLeaf = True
return f
def render_POST(self, request):
request.headers['Server'] = 'oxcd/%s' % __version__
site = self.get_site(request)
#print "POST", request.args
if 'action' in request.args:
if 'data' in request.args:
data = json.loads(request.args['data'][0])
else:
data = {}
action = request.args['action'][0]
accept_encoding = request.getHeader('accept-encoding') or ''
if 'gzip' in accept_encoding:
request = GzipRequest(request)
#FIXME: this should be done async
request.write(actions.render(self.backend, site, action, data))
request.finish()
return NOT_DONE_YET
def render_GET(self, request):
request.headers['Server'] = 'oxcd/%s' % __version__
if request.path.startswith('/api'):
path = self.static_path('api.html')
else:
path = self.static_path('index.html')
with open(path) as f:
data = f.read()
request.headers['Content-Type'] = 'text/html'
site = self.get_site(request)
data = data.replace('$name', site)
return data