import os import sys from datetime import datetime import logging from functools import partial import unicodedata from bottle import Bottle, request, static_file, redirect, abort, response import bottle logger = logging.getLogger('server') botlog = logging.getLogger('bottle') botlog.setLevel(logging.INFO) botlog.addHandler(logging.StreamHandler(sys.stdout)) bottle._stderr = lambda x: botlog.info(x.strip()) from .techrec import Rec, RecDB from .processqueue import get_process_queue from .forge import create_mp3 from .config_manager import get_config def date_read(s): return datetime.fromtimestamp(int(s)) def date_write(dt): return dt.strftime('%s') def rec_sanitize(rec): d = rec.serialize() d['starttime'] = date_write(d['starttime']) d['endtime'] = date_write(d['endtime']) return d class DateApp(Bottle): ''' This application will expose some date-related functions; it is intended to be used when you need to know the server's time on the browser ''' def __init__(self): Bottle.__init__(self) self.route('/help', callback=self.help) self.route('/date', callback=self.date) self.route('/custom', callback=self.custom) def date(self): n = datetime.now() return { 'unix': n.strftime('%s'), 'isoformat': n.isoformat(), 'ctime': n.ctime() } def custom(self): n = datetime.now() if 'strftime' not in request.query: abort(400, 'Need argument "strftime"') response.content_type = 'text/plain' return n.strftime(request.query['strftime']) def help(self): response.content_type = 'text/plain' return \ '/date : get JSON dict containing multiple formats of now()\n' + \ '/custom?strftime=FORMAT : get now().strftime(FORMAT)' class RecAPI(Bottle): def __init__(self, app): Bottle.__init__(self) self._route() self._app = app self.db = RecDB(get_config()['DB_URI']) def _route(self): self.post('/create', callback=self.create) self.post('/delete', callback=self.delete) self.post('/update/', callback=self.update) self.post('/generate', callback=self.generate) self.get('/help', callback=self.help) self.get('/', callback=self.help) self.get('/get/search', callback=self.search) self.get('/get/ongoing', callback=self.get_ongoing) self.get('/get/archive', callback=self.get_archive) self.get('/jobs', callback=self.running_jobs) self.get('/jobs/', callback=self.check_job) def create(self): req = dict(request.POST.decode().allitems()) ret = {} logger.debug("Create request %s " % req) now = datetime.now() start = date_read(req['starttime']) if 'starttime' in req else now name = req['name'] if 'name' in req else u"" end = date_read(req['endtime']) if 'endtime' in req else now rec = Rec(name=name, starttime=start, endtime=end) ret = self.db.add(rec) return self.rec_msg("Nuova registrazione creata! (id:%d)" % ret.id, rec=rec_sanitize(rec)) def delete(self): req = dict(request.POST.decode().allitems()) logging.info("Server: request delete %s " % (req)) if 'id' not in req: return self.rec_err("No valid ID") if self.db.delete(req["id"]): return self.rec_msg("DELETE OK") else: return self.rec_err("DELETE error: %s" % (self.db.get_err())) def update(self, recid): req = dict(request.POST.decode().allitems()) newrec = {} now = datetime.now() if 'starttime' not in req: newrec['starttime'] = now else: newrec['starttime'] = date_read(req['starttime']) if "endtime" not in req: newrec['endtime'] = now else: newrec['endtime'] = date_read(req['endtime']) if 'name' in req: newrec["name"] = req['name'] try: logger.info("prima di update") result_rec = self.db.update(recid, newrec) logger.info("dopo update") except Exception as exc: return self.rec_err("Errore Aggiornamento", exception=exc) return self.rec_msg("Aggiornamento completato!", rec=rec_sanitize(result_rec)) def generate(self): # prendiamo la rec in causa recid = dict(request.POST.decode().allitems())['id'] rec = self.db._search(_id=recid)[0] if rec.filename is not None and os.path.exists(rec.filename): return {'status': 'ready', 'message': 'The file has already been generated at %s' % rec.filename, 'rec': rec } if get_config()['FORGE_MAX_DURATION'] > 0 and \ (rec.endtime - rec.starttime).total_seconds() > \ get_config()['FORGE_MAX_DURATION']: response.status = 400 return {'status': 'error', 'message': 'The requested recording is too long' + ' (%d seconds)' % (rec.endtime - rec.starttime).total_seconds() } rec.filename = get_config()['AUDIO_OUTPUT_FORMAT'] % { 'time': rec.starttime.strftime('%y%m%d_%H%M'), # kept for retrocompatibility, should be dropped 'endtime': rec.endtime.strftime('%H%M'), 'startdt': rec.starttime.strftime('%y%m%d_%H%M'), 'enddt': rec.endtime.strftime('%y%m%d_%H%M'), 'name': ''.join(filter(lambda c: c.isalpha(), unicodedata.normalize('NFKD', rec.name).encode('ascii', 'ignore').decode('ascii'))), } self.db.get_session(rec).commit() job_id = self._app.pq.submit( create_mp3, start=rec.starttime, end=rec.endtime, outfile=os.path.join(get_config()['AUDIO_OUTPUT'], rec.filename), options={ 'title': rec.name, 'license_uri': get_config()['TAG_LICENSE_URI'], 'extra_tags': get_config()['TAG_EXTRA'] } ) logger.debug("SUBMITTED: %d" % job_id) return self.rec_msg("Aggiornamento completato!", job_id=job_id, result='/output/' + rec.filename, rec=rec_sanitize(rec)) def check_job(self, job_id): try: job = self._app.pq.check_job(job_id) except ValueError: abort(400, 'job_id not valid') def ret(status): return {'job_status': status, 'job_id': job_id} if job is True: return ret('DONE') if job is False: abort(404, 'No such job has ever been spawned') else: if job.ready(): try: res = job.get() return res except Exception as exc: r = ret('FAILED') r['exception'] = str(exc) import traceback tb = traceback.format_exc() logger.warning(tb) if get_config()['DEBUG']: r['exception'] = "%s: %s" % (str(exc), tb) r['traceback'] = tb return r return ret('WIP') def running_jobs(self): res = {} res['last_job_id'] = self._app.pq.last_job_id res['running'] = self._app.pq.jobs.keys() return res def search(self, args=None): req = dict() req.update(request.GET.allitems()) logger.debug("Search request: %s" % (req)) values = self.db._search(**req) from pprint import pprint logger.debug("Returned Values %s" % pprint([r.serialize() for r in values])) ret = {} for rec in values: ret[rec.id] = rec_sanitize(rec) logging.info("Return: %s" % ret) return ret def get_ongoing(self): return {rec.id: rec_sanitize(rec) for rec in self.db.get_ongoing()} def get_archive(self): return {rec.id: rec_sanitize(rec) for rec in self.db.get_archive_recent()} # @route('/help') def help(self): return "

help


\

/get, /get/, /get/

\

Get Info about rec identified by ID

\ \

/search, /search/, /search//

\

Search rec that match key/value (or get all)

\ \

/delete/

\

Delete rec identified by ID

\

/update

\

Not implemented.

" # JSON UTILS def rec_msg(self, msg, status=True, **kwargs): d = {"message": msg, "status": status} d.update(kwargs) return d def rec_err(self, msg, **kwargs): return self.rec_msg(msg, status=False, **kwargs) class RecServer: def __init__(self): self._app = Bottle() self._app.pq = get_process_queue() self._route() self.db = RecDB(get_config()['DB_URI']) def _route(self): # Static part of the site self._app.route('/output/', callback=lambda filepath: static_file(filepath, root=get_config()['AUDIO_OUTPUT'], download=True)) self._app.route('/static/', callback=lambda filepath: static_file(filepath, root=get_config()['STATIC_FILES'])) self._app.route('/', callback=lambda: redirect('/new.html')) self._app.route('/new.html', callback=partial(static_file, 'new.html', root=get_config()['STATIC_PAGES'])) self._app.route('/old.html', callback=partial(static_file, 'old.html', root=get_config()['STATIC_PAGES'])) self._app.route('/archive.html', callback=partial(static_file, 'archive.html', root=get_config()['STATIC_PAGES'])) class DebugAPI(Bottle): ''' This application is useful for testing the webserver itself ''' def __init__(self): Bottle.__init__(self) self.route('/sleep/:milliseconds', callback=self.sleep) self.route('/cpusleep/:howmuch', callback=self.cpusleep) self.route('/big/:exponent', callback=self.big) def sleep(self, milliseconds): import time time.sleep(int(milliseconds)/1000.0) return 'ok' def cpusleep(self, howmuch): out = '' for i in xrange(int(howmuch) * (10**3)): if i % 11234 == 0: out += 'a' return out def big(self, exponent): ''' returns a 2**n -1 string ''' for i in xrange(int(exponent)): yield str(i) * (2 ** i) def help(self): response.content_type = 'text/plain' return ''' /sleep/ : sleep, than say "ok" /cpusleep/ : busysleep, than say "ok" /big/ : returns a 2**n -1 byte content ''' class PasteLoggingServer(bottle.PasteServer): def run(self, handler): # pragma: no cover from paste import httpserver from paste.translogger import TransLogger handler = TransLogger(handler, **self.options['translogger_opts']) del self.options['translogger_opts'] httpserver.serve(handler, host=self.host, port=str(self.port), **self.options) bottle.server_names['pastelog'] = PasteLoggingServer def main_cmd(*args): """meant to be called from argparse""" c = RecServer() c._app.mount('/date', DateApp()) c._app.mount('/api', RecAPI(c._app)) if get_config()['DEBUG']: c._app.mount('/debug', DebugAPI()) server = get_config()['WSGI_SERVER'] if server == 'pastelog': from paste.translogger import TransLogger get_config()['WSGI_SERVER_OPTIONS']['translogger_opts'] = \ get_config()['TRANSLOGGER_OPTS'] c._app.run(server=server, host=get_config()['HOST'], port=get_config()['PORT'], debug=get_config()['DEBUG'], quiet=True, # this is to hide access.log style messages **get_config()['WSGI_SERVER_OPTIONS'] ) if __name__ == '__main__': from cli import common_pre common_pre() logger.warn("Usage of server.py is deprecated; use cli.py") main_cmd() # vim: set ts=4 sw=4 et ai ft=python: