maloja/maloja/server.py

301 lines
7.2 KiB
Python
Raw Permalink Normal View History

2021-12-23 07:17:19 +03:00
# technical
import sys
import os
from threading import Thread
2022-03-10 07:31:41 +03:00
from importlib import resources
2022-03-06 06:20:26 +03:00
import datauri
2022-04-13 18:51:17 +03:00
import time
2018-12-05 16:30:50 +03:00
# server stuff
2021-12-22 22:35:14 +03:00
from bottle import Bottle, static_file, request, response, FormsDict, redirect, BaseRequest, abort
import waitress
2022-05-26 15:56:04 +03:00
from jinja2.exceptions import TemplateNotFound
2020-08-31 00:49:14 +03:00
2021-12-23 07:17:19 +03:00
# doreah toolkit
from doreah.logging import log
from doreah import auth
2021-12-22 22:35:14 +03:00
# rest of the project
2019-11-24 23:47:03 +03:00
from . import database
from .database.jinjaview import JinjaDBConnection
from .images import resolve_track_image, resolve_artist_image
2021-12-22 22:35:14 +03:00
from .malojauri import uri_to_internal, remove_identical
from .pkg_global.conf import malojaconfig, data_dir
2020-08-31 00:49:14 +03:00
from .jinjaenv.context import jinja_environment
from .apis import init_apis, apikeystore
2021-12-23 07:17:19 +03:00
2022-04-09 22:20:48 +03:00
from .dev.profiler import profile
2018-11-24 18:29:24 +03:00
######
### TECHNICAL SETTINGS
#####
PORT = malojaconfig["PORT"]
2021-12-19 23:10:55 +03:00
HOST = malojaconfig["HOST"]
2022-04-13 18:51:17 +03:00
THREADS = 16
BaseRequest.MEMFILE_MAX = 15 * 1024 * 1024
2022-03-10 07:31:41 +03:00
#STATICFOLDER = importlib.resources.path(__name__,"web/static")
2018-11-24 18:29:24 +03:00
2018-12-19 17:28:10 +03:00
webserver = Bottle()
######
### ERRORS
#####
2018-11-25 16:49:53 +03:00
2019-04-03 19:03:55 +03:00
@webserver.error(400)
@webserver.error(403)
@webserver.error(404)
@webserver.error(405)
@webserver.error(408)
@webserver.error(500)
@webserver.error(503)
2019-04-03 19:03:55 +03:00
@webserver.error(505)
def customerror(error):
2021-12-09 08:26:06 +03:00
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"
2020-08-31 21:12:44 +03:00
adminmode = request.cookies.get("adminmode") == "true" and auth.check(request)
2019-04-03 19:03:55 +03:00
2020-08-31 01:08:55 +03:00
template = jinja_environment.get_template('error.jinja')
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
return template.render(
2021-12-09 08:26:06 +03:00
error_code=error_code,
error_desc=error_desc,
traceback=traceback,
error_full_desc=body,
adminmode=adminmode,
Refactoring (#83) * Merge isinstance calls * Inline variable that is immediately returned * Replace set() with comprehension * Replace assignment with augmented assignment * Remove unnecessary else after guard condition * Convert for loop into list comprehension * Replace unused for index with underscore * Merge nested if conditions * Convert for loop into list comprehension * Convert for loop into set comprehension * Remove unnecessary else after guard condition * Replace if statements with if expressions * Simplify sequence comparison * Replace multiple comparisons with in operator * Merge isinstance calls * Merge nested if conditions * Add guard clause * Merge duplicate blocks in conditional * Replace unneeded comprehension with generator * Inline variable that is immediately returned * Remove unused imports * Replace unneeded comprehension with generator * Remove unused imports * Remove unused import * Inline variable that is immediately returned * Swap if/else branches and remove unnecessary else * Use str.join() instead of for loop * Multiple refactors - Remove redundant pass statement - Hoist repeated code outside conditional statement - Swap if/else to remove empty if body * Inline variable that is immediately returned * Simplify generator expression * Replace if statement with if expression * Multiple refactoring - Replace range(0, x) with range(x) - Swap if/else branches - Remove unnecessary else after guard condition * Use str.join() instead of for loop * Hoist repeated code outside conditional statement * Use str.join() instead of for loop * Inline variables that are immediately returned * Merge dictionary assignment with declaration * Use items() to directly unpack dictionary values * Extract dup code from methods into a new one
2021-10-19 15:58:24 +03:00
)
2019-04-03 19:03:55 +03:00
2018-11-25 16:49:53 +03:00
2019-05-12 19:39:46 +03:00
2019-12-04 21:14:33 +03:00
######
### REGISTERING ENDPOINTS
#####
2019-11-24 23:47:03 +03:00
2020-08-18 05:55:36 +03:00
aliases = {
"admin": "admin_overview",
2020-08-21 19:06:16 +03:00
"manual": "admin_manual",
"setup": "admin_setup",
"issues": "admin_issues"
2020-08-18 05:55:36 +03:00
}
2020-05-13 23:57:55 +03:00
2021-12-09 23:12:10 +03:00
### API
2021-12-09 23:12:10 +03:00
auth.authapi.mount(server=webserver)
init_apis(webserver)
2021-12-09 23:12:10 +03:00
# redirects for backwards compatibility
@webserver.get("/api/s/<pth:path>")
@webserver.post("/api/s/<pth:path>")
def deprecated_api_s(pth):
redirect("/apis/" + pth + "?" + request.query_string,308)
2021-12-09 23:12:10 +03:00
@webserver.get("/api/<pth:path>")
@webserver.post("/api/<pth:path>")
def deprecated_api(pth):
redirect("/apis/mlj_1/" + pth + "?" + request.query_string,308)
2021-12-09 23:12:10 +03:00
### STATIC
2019-11-20 07:19:02 +03:00
2021-12-09 23:12:10 +03:00
@webserver.route("/image")
def dynamic_image():
keys = FormsDict.decode(request.query)
2022-03-26 07:49:30 +03:00
if keys['type'] == 'track':
result = resolve_track_image(keys['id'])
elif keys['type'] == 'artist':
result = resolve_artist_image(keys['id'])
2022-02-17 09:35:05 +03:00
2022-03-26 08:01:05 +03:00
if result is None or result['value'] in [None,'']:
return ""
2022-03-26 07:49:30 +03:00
if result['type'] == 'raw':
2022-03-06 06:20:26 +03:00
# data uris are directly served as image because a redirect to a data uri
# doesnt work
2022-03-26 07:49:30 +03:00
duri = datauri.DataURI(result['value'])
2022-03-06 06:20:26 +03:00
response.content_type = duri.mimetype
return duri.data
2022-03-26 08:01:05 +03:00
if result['type'] == 'url':
2022-03-26 07:49:30 +03:00
redirect(result['value'],307)
2021-12-09 23:12:10 +03:00
@webserver.route("/images/<pth:re:.*\\.jpeg>")
@webserver.route("/images/<pth:re:.*\\.jpg>")
@webserver.route("/images/<pth:re:.*\\.png>")
@webserver.route("/images/<pth:re:.*\\.gif>")
def static_image(pth):
2021-12-22 22:35:14 +03:00
ext = pth.split(".")[-1]
2021-12-09 23:12:10 +03:00
small_pth = pth + "-small"
if os.path.exists(data_dir['images'](small_pth)):
2022-03-27 23:02:50 +03:00
resp = static_file(small_pth,root=data_dir['images']())
2021-12-09 23:12:10 +03:00
else:
try:
from pyvips import Image
thumb = Image.thumbnail(data_dir['images'](pth),300)
thumb.webpsave(data_dir['images'](small_pth))
2022-03-27 23:02:50 +03:00
resp = static_file(small_pth,root=data_dir['images']())
2021-12-22 22:35:14 +03:00
except Exception:
2022-03-27 23:02:50 +03:00
resp = static_file(pth,root=data_dir['images']())
2020-08-31 00:49:14 +03:00
2021-12-09 23:12:10 +03:00
#response = static_file("images/" + pth,root="")
2022-03-27 23:02:50 +03:00
resp.set_header("Cache-Control", "public, max-age=86400")
resp.set_header("Content-Type", "image/" + ext)
return resp
2018-11-24 18:29:24 +03:00
2021-12-09 23:12:10 +03:00
@webserver.route("/login")
def login():
return auth.get_login_page()
# old
2021-12-09 23:12:10 +03:00
@webserver.route("/<name>.<ext>")
@webserver.route("/media/<name>.<ext>")
def static(name,ext):
2022-04-23 18:32:05 +03:00
assert ext in ["txt","ico","jpeg","jpg","png","less","js","ttf","css"]
2022-03-10 07:31:41 +03:00
with resources.files('maloja') / 'web' / 'static' as staticfolder:
response = static_file(ext + "/" + name + "." + ext,root=staticfolder)
2021-12-09 23:12:10 +03:00
response.set_header("Cache-Control", "public, max-age=3600")
return response
# new, direct reference
@webserver.route("/static/<path:path>")
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/<category>/<path:path>")
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
2021-12-09 23:12:10 +03:00
### DYNAMIC
2022-04-12 20:04:22 +03:00
@profile
2022-01-10 07:05:54 +03:00
def jinja_page(name):
2021-12-09 23:12:10 +03:00
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:
2022-05-26 15:56:04 +03:00
template = jinja_environment.get_template(name + '.jinja')
res = template.render(**loc_context)
2022-05-26 15:56:04 +03:00
except TemplateNotFound:
abort(404,f"Not found: '{name}'")
except (ValueError, IndexError):
abort(404,"This Artist or Track does not exist")
2021-12-19 23:10:55 +03:00
if malojaconfig["DEV_MODE"]: jinja_environment.cache.clear()
2022-04-23 18:32:05 +03:00
return res
2021-12-09 23:12:10 +03:00
@webserver.route("/<name:re:admin.*>")
@auth.authenticated
2022-01-10 07:05:54 +03:00
def jinja_page_private(name):
return jinja_page(name)
2021-12-09 23:12:10 +03:00
@webserver.route("/<name>")
2022-01-10 07:05:54 +03:00
def jinja_page_public(name):
return jinja_page(name)
2021-12-09 23:12:10 +03:00
@webserver.route("")
@webserver.route("/")
def mainpage():
2022-01-10 07:05:54 +03:00
return jinja_page("start")
2021-12-09 23:12:10 +03:00
# Shortlinks
@webserver.get("/artist/<artist>")
def redirect_artist(artist):
redirect("/artist?artist=" + artist)
@webserver.get("/track/<artists:path>/<title>")
def redirect_track(artists,title):
redirect("/track?title=" + title + "&" + "&".join("artist=" + artist for artist in artists.split("/")))
######
### RUNNING THE SERVER
#####
2022-04-13 18:51:17 +03:00
# 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():
2021-12-09 23:41:57 +03:00
log("Starting up Maloja server...")
2021-11-17 21:10:49 +03:00
## start database
2021-12-09 23:41:57 +03:00
Thread(target=database.start_db).start()
2021-11-17 21:10:49 +03:00
2022-04-13 18:51:17 +03:00
2021-11-17 21:10:49 +03:00
try:
#run(webserver, host=HOST, port=MAIN_PORT, server='waitress')
2022-04-14 21:49:40 +03:00
listen = f"{HOST}:{PORT}"
log(f"Listening on {listen}")
waitress.serve(webserver, listen=listen, threads=THREADS)
2021-11-17 21:10:49 +03:00
except OSError:
log("Error. Is another Maloja process already running?")
raise