mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
refactor mediasrv request processing
_redirectWebExports was doing more than it was originally intended for, and it was difficult to follow.
This commit is contained in:
parent
70dbd06be3
commit
4b5004c472
1 changed files with 145 additions and 103 deletions
|
@ -10,6 +10,7 @@ import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
|
from dataclasses import dataclass
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
|
@ -53,6 +54,22 @@ app = flask.Flask(__name__)
|
||||||
flask_cors.CORS(app)
|
flask_cors.CORS(app)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class LocalFileRequest:
|
||||||
|
# base folder, eg media folder
|
||||||
|
root: str
|
||||||
|
# path to file relative to root folder
|
||||||
|
path: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class NotFound:
|
||||||
|
message: str
|
||||||
|
|
||||||
|
|
||||||
|
DynamicRequest = Callable[[], Response]
|
||||||
|
|
||||||
|
|
||||||
class MediaServer(threading.Thread):
|
class MediaServer(threading.Thread):
|
||||||
|
|
||||||
_ready = threading.Event()
|
_ready = threading.Event()
|
||||||
|
@ -103,16 +120,9 @@ class MediaServer(threading.Thread):
|
||||||
return int(self.server.effective_port) # type: ignore
|
return int(self.server.effective_port) # type: ignore
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<path:pathin>", methods=["GET", "POST"])
|
def _handle_local_file_request(request: LocalFileRequest) -> Response:
|
||||||
def allroutes(pathin: str) -> Response:
|
directory = request.root
|
||||||
try:
|
path = request.path
|
||||||
directory, path = _redirectWebExports(pathin)
|
|
||||||
except TypeError:
|
|
||||||
return flask.make_response(
|
|
||||||
f"Invalid path: {pathin}",
|
|
||||||
HTTPStatus.FORBIDDEN,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
isdir = os.path.isdir(os.path.join(directory, path))
|
isdir = os.path.isdir(os.path.join(directory, path))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -138,13 +148,7 @@ def allroutes(pathin: str) -> Response:
|
||||||
HTTPStatus.FORBIDDEN,
|
HTTPStatus.FORBIDDEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
if devMode:
|
|
||||||
print(f"{time.time():.3f} {flask.request.method} /{pathin}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if flask.request.method == "POST":
|
|
||||||
return handle_post(path)
|
|
||||||
|
|
||||||
if fullpath.endswith(".css"):
|
if fullpath.endswith(".css"):
|
||||||
# some users may have invalid mime type in the Windows registry
|
# some users may have invalid mime type in the Windows registry
|
||||||
mimetype = "text/css"
|
mimetype = "text/css"
|
||||||
|
@ -156,9 +160,9 @@ def allroutes(pathin: str) -> Response:
|
||||||
if os.path.exists(fullpath):
|
if os.path.exists(fullpath):
|
||||||
return flask.send_file(fullpath, mimetype=mimetype, conditional=True)
|
return flask.send_file(fullpath, mimetype=mimetype, conditional=True)
|
||||||
else:
|
else:
|
||||||
print(f"Not found: {ascii(pathin)}")
|
print(f"Not found: {path}")
|
||||||
return flask.make_response(
|
return flask.make_response(
|
||||||
f"Invalid path: {pathin}",
|
f"Invalid path: {path}",
|
||||||
HTTPStatus.NOT_FOUND,
|
HTTPStatus.NOT_FOUND,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -178,83 +182,120 @@ def allroutes(pathin: str) -> Response:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _redirectWebExports(path: str) -> tuple[str, str]:
|
@app.route("/<path:pathin>", methods=["GET", "POST"])
|
||||||
# catch /_anki references and rewrite them to web export folder
|
def handle_request(pathin: str) -> Response:
|
||||||
targetPath = "_anki/"
|
request = _extract_request(pathin)
|
||||||
if path.startswith(targetPath):
|
if devMode:
|
||||||
dirname = os.path.dirname(path)
|
print(f"{time.time():.3f} {flask.request.method} /{pathin}")
|
||||||
filename = os.path.basename(path)
|
|
||||||
addprefix = None
|
|
||||||
|
|
||||||
# remap legacy top-level references
|
if isinstance(request, NotFound):
|
||||||
if dirname == "_anki":
|
print(request.message)
|
||||||
base, ext = os.path.splitext(filename)
|
return flask.make_response(
|
||||||
if ext == ".css":
|
f"Invalid path: {pathin}",
|
||||||
addprefix = "css/"
|
HTTPStatus.NOT_FOUND,
|
||||||
elif ext == ".js":
|
)
|
||||||
if base in ("browsersel", "jquery-ui", "jquery", "plot"):
|
elif callable(request):
|
||||||
addprefix = "js/vendor/"
|
return _handle_dynamic_request(request)
|
||||||
else:
|
elif isinstance(request, LocalFileRequest):
|
||||||
addprefix = "js/"
|
return _handle_local_file_request(request)
|
||||||
|
else:
|
||||||
|
return flask.make_response(
|
||||||
|
f"unexpected request: {pathin}",
|
||||||
|
HTTPStatus.FORBIDDEN,
|
||||||
|
)
|
||||||
|
|
||||||
elif dirname == "_anki/js/vendor":
|
|
||||||
base, ext = os.path.splitext(filename)
|
|
||||||
|
|
||||||
if base == "jquery":
|
def _extract_internal_request(
|
||||||
base = "jquery.min"
|
path: str,
|
||||||
addprefix = "js/vendor/"
|
) -> LocalFileRequest | DynamicRequest | NotFound | None:
|
||||||
|
"Catch /_anki references and rewrite them to web export folder."
|
||||||
elif base == "jquery-ui":
|
prefix = "_anki/"
|
||||||
base = "jquery-ui.min"
|
if not path.startswith(prefix):
|
||||||
addprefix = "js/vendor/"
|
|
||||||
|
|
||||||
elif base == "browsersel":
|
|
||||||
base = "css_browser_selector.min"
|
|
||||||
addprefix = "js/vendor/"
|
|
||||||
|
|
||||||
if addprefix:
|
|
||||||
oldpath = path
|
|
||||||
path = f"{targetPath}{addprefix}{base}{ext}"
|
|
||||||
print(f"legacy {oldpath} remapped to {path}")
|
|
||||||
|
|
||||||
return _exportFolder, path[len(targetPath) :]
|
|
||||||
|
|
||||||
# catch /_addons references and rewrite them to addons folder
|
|
||||||
targetPath = "_addons/"
|
|
||||||
if path.startswith(targetPath):
|
|
||||||
addonPath = path[len(targetPath) :]
|
|
||||||
|
|
||||||
try:
|
|
||||||
addMgr = aqt.mw.addonManager
|
|
||||||
except AttributeError as error:
|
|
||||||
if devMode:
|
|
||||||
print(f"_redirectWebExports: {error}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
addon, subPath = addonPath.split("/", 1)
|
|
||||||
except ValueError:
|
|
||||||
return None
|
|
||||||
if not addon:
|
|
||||||
return None
|
|
||||||
|
|
||||||
pattern = addMgr.getWebExports(addon)
|
|
||||||
if not pattern:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if re.fullmatch(pattern, subPath):
|
|
||||||
return addMgr.addonsFolder(), addonPath
|
|
||||||
|
|
||||||
print(f"couldn't locate item in add-on folder {path}")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
dirname = os.path.dirname(path)
|
||||||
|
filename = os.path.basename(path)
|
||||||
|
additional_prefix = None
|
||||||
|
|
||||||
|
if dirname == "_anki":
|
||||||
|
if flask.request.method == "POST":
|
||||||
|
return _extract_collection_post_request(filename)
|
||||||
|
# remap legacy top-level references
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
if ext == ".css":
|
||||||
|
additional_prefix = "css/"
|
||||||
|
elif ext == ".js":
|
||||||
|
if base in ("browsersel", "jquery-ui", "jquery", "plot"):
|
||||||
|
additional_prefix = "js/vendor/"
|
||||||
|
else:
|
||||||
|
additional_prefix = "js/"
|
||||||
|
# handle requests for vendored libraries
|
||||||
|
elif dirname == "_anki/js/vendor":
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
|
||||||
|
if base == "jquery":
|
||||||
|
base = "jquery.min"
|
||||||
|
additional_prefix = "js/vendor/"
|
||||||
|
|
||||||
|
elif base == "jquery-ui":
|
||||||
|
base = "jquery-ui.min"
|
||||||
|
additional_prefix = "js/vendor/"
|
||||||
|
|
||||||
|
elif base == "browsersel":
|
||||||
|
base = "css_browser_selector.min"
|
||||||
|
additional_prefix = "js/vendor/"
|
||||||
|
|
||||||
|
if additional_prefix:
|
||||||
|
oldpath = path
|
||||||
|
path = f"{prefix}{additional_prefix}{base}{ext}"
|
||||||
|
print(f"legacy {oldpath} remapped to {path}")
|
||||||
|
|
||||||
|
return LocalFileRequest(root=_exportFolder, path=path[len(prefix) :])
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_addon_request(path: str) -> LocalFileRequest | NotFound | None:
|
||||||
|
"Catch /_addons references and rewrite them to addons folder."
|
||||||
|
prefix = "_addons/"
|
||||||
|
if not path.startswith(prefix):
|
||||||
|
return None
|
||||||
|
|
||||||
|
addon_path = path[len(prefix) :]
|
||||||
|
|
||||||
|
try:
|
||||||
|
manager = aqt.mw.addonManager
|
||||||
|
except AttributeError as error:
|
||||||
|
if devMode:
|
||||||
|
print(f"_redirectWebExports: {error}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
addon, sub_path = addon_path.split("/", 1)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
if not addon:
|
||||||
|
return None
|
||||||
|
|
||||||
|
pattern = manager.getWebExports(addon)
|
||||||
|
if not pattern:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if re.fullmatch(pattern, sub_path):
|
||||||
|
return LocalFileRequest(root=manager.addonsFolder(), path=addon_path)
|
||||||
|
|
||||||
|
return NotFound(message=f"couldn't locate item in add-on folder {path}")
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_request(path: str) -> LocalFileRequest | DynamicRequest | NotFound:
|
||||||
|
if internal := _extract_internal_request(path):
|
||||||
|
return internal
|
||||||
|
elif addon := _extract_addon_request(path):
|
||||||
|
return addon
|
||||||
|
|
||||||
if not aqt.mw.col:
|
if not aqt.mw.col:
|
||||||
print(f"collection not open, ignore request for {path}")
|
return NotFound(message=f"collection not open, ignore request for {path}")
|
||||||
return None
|
|
||||||
|
|
||||||
path = hooks.media_file_filter(path)
|
path = hooks.media_file_filter(path)
|
||||||
|
return LocalFileRequest(root=aqt.mw.col.media.dir(), path=path)
|
||||||
return aqt.mw.col.media.dir(), path
|
|
||||||
|
|
||||||
|
|
||||||
def graph_data() -> bytes:
|
def graph_data() -> bytes:
|
||||||
|
@ -365,31 +406,32 @@ post_handlers = {
|
||||||
"changeNotetypeInfo": change_notetype_info,
|
"changeNotetypeInfo": change_notetype_info,
|
||||||
"notetypeNames": notetype_names,
|
"notetypeNames": notetype_names,
|
||||||
"changeNotetype": change_notetype,
|
"changeNotetype": change_notetype,
|
||||||
# pylint: disable=unnecessary-lambda
|
|
||||||
"i18nResources": i18n_resources,
|
"i18nResources": i18n_resources,
|
||||||
"congratsInfo": congrats_info,
|
"congratsInfo": congrats_info,
|
||||||
"completeTag": complete_tag,
|
"completeTag": complete_tag,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def handle_post(path: str) -> Response:
|
def _extract_collection_post_request(path: str) -> DynamicRequest | NotFound:
|
||||||
if not aqt.mw.col:
|
if not aqt.mw.col:
|
||||||
print(f"collection not open, ignore request for {path}")
|
return NotFound(message=f"collection not open, ignore request for {path}")
|
||||||
return flask.make_response("Collection not open", HTTPStatus.NOT_FOUND)
|
if handler := post_handlers.get(path):
|
||||||
|
# convert bytes/None into response
|
||||||
if path in post_handlers:
|
def wrapped() -> Response:
|
||||||
try:
|
if data := handler():
|
||||||
if data := post_handlers[path]():
|
|
||||||
response = flask.make_response(data)
|
response = flask.make_response(data)
|
||||||
response.headers["Content-Type"] = "application/binary"
|
response.headers["Content-Type"] = "application/binary"
|
||||||
else:
|
else:
|
||||||
response = flask.make_response("", HTTPStatus.NO_CONTENT)
|
response = flask.make_response("", HTTPStatus.NO_CONTENT)
|
||||||
except Exception as e:
|
return response
|
||||||
return flask.make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
|
||||||
else:
|
|
||||||
response = flask.make_response(
|
|
||||||
f"Unhandled post to {path}",
|
|
||||||
HTTPStatus.FORBIDDEN,
|
|
||||||
)
|
|
||||||
|
|
||||||
return response
|
return wrapped
|
||||||
|
else:
|
||||||
|
return NotFound(message=f"{path} not found")
|
||||||
|
|
||||||
|
|
||||||
|
def _handle_dynamic_request(request: DynamicRequest) -> Response:
|
||||||
|
try:
|
||||||
|
return request()
|
||||||
|
except Exception as e:
|
||||||
|
return flask.make_response(str(e), HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
|
Loading…
Reference in a new issue