# technical import sys import os from threading import Thread from importlib import resources import datauri import time # server stuff from bottle import Bottle, static_file, request, response, FormsDict, redirect, BaseRequest, abort import waitress from jinja2.exceptions import TemplateNotFound # doreah toolkit from doreah.logging import log from doreah import auth # rest of the project from . import database from .database.jinjaview import JinjaDBConnection from .images import resolve_track_image, resolve_artist_image from .malojauri import uri_to_internal, remove_identical from .pkg_global.conf import malojaconfig, data_dir from .jinjaenv.context import jinja_environment from .apis import init_apis, apikeystore from .dev.profiler import profile ###### ### TECHNICAL SETTINGS ##### PORT = malojaconfig["PORT"] HOST = malojaconfig["HOST"] THREADS = 16 BaseRequest.MEMFILE_MAX = 15 * 1024 * 1024 #STATICFOLDER = importlib.resources.path(__name__,"web/static") webserver = Bottle() ###### ### ERRORS ##### @webserver.error(400) @webserver.error(403) @webserver.error(404) @webserver.error(405) @webserver.error(408) @webserver.error(500) @webserver.error(503) @webserver.error(505) def customerror(error): error_code = error.status_code error_desc = error.status traceback = error.traceback body = error.body or "" traceback = traceback.strip() if traceback is not None else "No Traceback" adminmode = request.cookies.get("adminmode") == "true" and auth.check(request) template = jinja_environment.get_template('error.jinja') return template.render( error_code=error_code, error_desc=error_desc, traceback=traceback, error_full_desc=body, adminmode=adminmode, ) ###### ### REGISTERING ENDPOINTS ##### aliases = { "admin": "admin_overview", "manual": "admin_manual", "setup": "admin_setup", "issues": "admin_issues" } ### API auth.authapi.mount(server=webserver) init_apis(webserver) # redirects for backwards compatibility @webserver.get("/api/s/") @webserver.post("/api/s/") def deprecated_api_s(pth): redirect("/apis/" + pth + "?" + request.query_string,308) @webserver.get("/api/") @webserver.post("/api/") def deprecated_api(pth): redirect("/apis/mlj_1/" + pth + "?" + request.query_string,308) ### STATIC @webserver.route("/image") def dynamic_image(): keys = FormsDict.decode(request.query) if keys['type'] == 'track': result = resolve_track_image(keys['id']) elif keys['type'] == 'artist': result = resolve_artist_image(keys['id']) if result is None or result['value'] in [None,'']: return "" if result['type'] == 'raw': # data uris are directly served as image because a redirect to a data uri # doesnt work duri = datauri.DataURI(result['value']) response.content_type = duri.mimetype return duri.data if result['type'] == 'url': redirect(result['value'],307) @webserver.route("/images/") @webserver.route("/images/") @webserver.route("/images/") @webserver.route("/images/") def static_image(pth): ext = pth.split(".")[-1] small_pth = pth + "-small" if os.path.exists(data_dir['images'](small_pth)): resp = static_file(small_pth,root=data_dir['images']()) else: try: from pyvips import Image thumb = Image.thumbnail(data_dir['images'](pth),300) thumb.webpsave(data_dir['images'](small_pth)) resp = static_file(small_pth,root=data_dir['images']()) except Exception: resp = static_file(pth,root=data_dir['images']()) #response = static_file("images/" + pth,root="") resp.set_header("Cache-Control", "public, max-age=86400") resp.set_header("Content-Type", "image/" + ext) return resp @webserver.route("/login") def login(): return auth.get_login_page() # old @webserver.route("/.") @webserver.route("/media/.") def static(name,ext): assert ext in ["txt","ico","jpeg","jpg","png","less","js","ttf","css"] with resources.files('maloja') / 'web' / 'static' as staticfolder: response = static_file(ext + "/" + name + "." + ext,root=staticfolder) response.set_header("Cache-Control", "public, max-age=3600") return response # new, direct reference @webserver.route("/static/") def static(path): with resources.files('maloja') / 'web' / 'static' as staticfolder: response = static_file(path,root=staticfolder) response.set_header("Cache-Control", "public, max-age=3600") return response # static files not supplied by the package @webserver.get("/static_custom//") def static_custom(category,path): rootpath = { 'css':data_dir['css']() } response = static_file(path,root=rootpath[category]) response.set_header("Cache-Control", "public, max-age=3600") return response ### DYNAMIC @profile def jinja_page(name): if name in aliases: redirect(aliases[name]) keys = remove_identical(FormsDict.decode(request.query)) adminmode = request.cookies.get("adminmode") == "true" and auth.check(request) with JinjaDBConnection() as conn: loc_context = { "dbc":conn, "adminmode":adminmode, "apikey":request.cookies.get("apikey") if adminmode else None, "apikeys":apikeystore, "_urikeys":keys, #temporary! } loc_context["filterkeys"], loc_context["limitkeys"], loc_context["delimitkeys"], loc_context["amountkeys"], loc_context["specialkeys"] = uri_to_internal(keys) try: template = jinja_environment.get_template(name + '.jinja') res = template.render(**loc_context) except TemplateNotFound: abort(404,f"Not found: '{name}'") except (ValueError, IndexError): abort(404,"This Artist or Track does not exist") if malojaconfig["DEV_MODE"]: jinja_environment.cache.clear() return res @webserver.route("/") @auth.authenticated def jinja_page_private(name): return jinja_page(name) @webserver.route("/") def jinja_page_public(name): return jinja_page(name) @webserver.route("") @webserver.route("/") def mainpage(): return jinja_page("start") # Shortlinks @webserver.get("/artist/") def redirect_artist(artist): redirect("/artist?artist=" + artist) @webserver.get("/track//") def redirect_track(artists,title): redirect("/track?title=" + title + "&" + "&".join("artist=" + artist for artist in artists.split("/"))) ###### ### RUNNING THE SERVER ##### # warning interception import logging class WaitressLogHandler(): def __init__(self): self.lastwarned = 0 self.barrier = 5 self.level = 20 self.filters = [] def handle(self,record): if record.name == 'waitress.queue': now = time.time() depth = record.args[0] if depth > self.barrier: log(f"Waitress Task Queue Depth at {depth}") self.lastwarned = now self.barrier = max(depth,self.barrier+5) elif now - self.lastwarned > 5: self.barrier = max(5,self.barrier-5) else: log(f"Waitress: {record.msg % record.args}") logging.getLogger().addHandler(WaitressLogHandler()) def run_server(): log("Starting up Maloja server...") ## start database Thread(target=database.start_db).start() try: #run(webserver, host=HOST, port=MAIN_PORT, server='waitress') listen = f"{HOST}:{PORT}" log(f"Listening on {listen}") waitress.serve(webserver, listen=listen, threads=THREADS) except OSError: log("Error. Is another Maloja process already running?") raise