Merge pull request #554 from evandroforks/fix_html5_media_support
Fixed HTML5 media tags
This commit is contained in:
commit
e45c939649
@ -4,13 +4,18 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import http.server
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
import socketserver
|
||||
import sys
|
||||
import threading
|
||||
import traceback
|
||||
from http import HTTPStatus
|
||||
from typing import Optional
|
||||
|
||||
import flask
|
||||
import flask_cors # type: ignore
|
||||
from flask import request
|
||||
from waitress.server import create_server
|
||||
|
||||
import aqt
|
||||
from anki.collection import Collection
|
||||
@ -33,171 +38,157 @@ def _getExportFolder():
|
||||
|
||||
|
||||
_exportFolder = _getExportFolder()
|
||||
|
||||
# webengine on windows sometimes opens a connection and fails to send a request,
|
||||
# which will hang the server if unthreaded
|
||||
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
||||
# allow for a flood of requests before we've started up properly
|
||||
request_queue_size = 100
|
||||
|
||||
# work around python not being able to handle non-latin hostnames
|
||||
def server_bind(self):
|
||||
"""Override server_bind to store the server name."""
|
||||
socketserver.TCPServer.server_bind(self)
|
||||
host, port = self.server_address[:2]
|
||||
try:
|
||||
self.server_name = socket.getfqdn(host)
|
||||
except:
|
||||
self.server_name = "server"
|
||||
self.server_port = port
|
||||
app = flask.Flask(__name__)
|
||||
flask_cors.CORS(app)
|
||||
|
||||
|
||||
class MediaServer(threading.Thread):
|
||||
|
||||
_port: Optional[int] = None
|
||||
_ready = threading.Event()
|
||||
daemon = True
|
||||
|
||||
def __init__(self, mw, *args, **kwargs):
|
||||
def __init__(self, mw: aqt.main.AnkiQt, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.mw = mw
|
||||
self.is_shutdown = False
|
||||
_redirectWebExports.mw = mw # type: ignore
|
||||
allroutes.mw = mw # type: ignore
|
||||
|
||||
def run(self):
|
||||
RequestHandler.mw = self.mw
|
||||
try:
|
||||
if devMode:
|
||||
# idempotent if logging has already been set up
|
||||
logging.basicConfig()
|
||||
else:
|
||||
logging.getLogger("waitress").setLevel(logging.ERROR)
|
||||
|
||||
desired_port = int(os.getenv("ANKI_API_PORT", "0"))
|
||||
self.server = ThreadedHTTPServer(("127.0.0.1", desired_port), RequestHandler)
|
||||
self.server = create_server(app, host="127.0.0.1", port=desired_port)
|
||||
if devMode:
|
||||
print(
|
||||
"Serving on http://%s:%s"
|
||||
% (self.server.effective_host, self.server.effective_port)
|
||||
)
|
||||
|
||||
self._ready.set()
|
||||
self.server.serve_forever()
|
||||
self.server.run()
|
||||
|
||||
except Exception:
|
||||
if not self.is_shutdown:
|
||||
raise
|
||||
|
||||
def shutdown(self):
|
||||
self.is_shutdown = True
|
||||
sockets = list(self.server._map.values())
|
||||
for socket in sockets:
|
||||
socket.handle_close()
|
||||
# https://github.com/Pylons/webtest/blob/4b8a3ebf984185ff4fefb31b4d0cf82682e1fcf7/webtest/http.py#L93-L104
|
||||
self.server.task_dispatcher.shutdown()
|
||||
|
||||
def getPort(self):
|
||||
self._ready.wait()
|
||||
return self.server.server_port
|
||||
|
||||
def shutdown(self):
|
||||
self.server.shutdown()
|
||||
return int(self.server.effective_port)
|
||||
|
||||
|
||||
class RequestHandler(http.server.SimpleHTTPRequestHandler):
|
||||
|
||||
timeout = 10
|
||||
mw: Optional[aqt.main.AnkiQt] = None
|
||||
|
||||
def do_GET(self):
|
||||
f = self.send_head()
|
||||
if f:
|
||||
@app.route("/", defaults={"path": ""})
|
||||
@app.route("/<path:pathin>", methods=["GET", "POST"])
|
||||
def allroutes(pathin):
|
||||
directory, path = _redirectWebExports(pathin)
|
||||
try:
|
||||
self.copyfile(f, self.wfile)
|
||||
except Exception as e:
|
||||
isdir = os.path.isdir(os.path.join(directory, path))
|
||||
except ValueError:
|
||||
return flask.make_response(
|
||||
"Path for '%s - %s' is too long!" % (directory, path),
|
||||
HTTPStatus.BAD_REQUEST,
|
||||
)
|
||||
|
||||
directory = os.path.realpath(directory)
|
||||
path = os.path.normpath(path)
|
||||
fullpath = os.path.realpath(os.path.join(directory, path))
|
||||
|
||||
# protect against directory transversal: https://security.openstack.org/guidelines/dg_using-file-paths.html
|
||||
if not fullpath.startswith(directory):
|
||||
return flask.make_response(
|
||||
"Path for '%s - %s' is a security leak!" % (directory, path),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
|
||||
if isdir:
|
||||
return flask.make_response(
|
||||
"Path for '%s - %s' is a directory (not supported)!" % (directory, path),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
|
||||
if devMode:
|
||||
print("http server caught exception:", e)
|
||||
print("Sending file '%s - %s'" % (directory, path))
|
||||
|
||||
try:
|
||||
if flask.request.method == "POST":
|
||||
if path == "graphData":
|
||||
body = request.data
|
||||
data = graph_data(allroutes.mw.col, **from_json_bytes(body))
|
||||
elif path == "i18nResources":
|
||||
data = allroutes.mw.col.backend.i18n_resources()
|
||||
else:
|
||||
return flask.make_response(
|
||||
"Post request to '%s - %s' is a security leak!" % (directory, path),
|
||||
HTTPStatus.FORBIDDEN,
|
||||
)
|
||||
|
||||
response = flask.make_response(data)
|
||||
response.headers["Content-Type"] = "application/binary"
|
||||
return response
|
||||
|
||||
return flask.send_file(fullpath, conditional=True)
|
||||
|
||||
except Exception as error:
|
||||
if devMode:
|
||||
print(
|
||||
"Caught HTTP server exception,\n%s"
|
||||
% "".join(traceback.format_exception(*sys.exc_info())),
|
||||
)
|
||||
|
||||
# swallow it - user likely surfed away from
|
||||
# review screen before an image had finished
|
||||
# downloading
|
||||
pass
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def send_head(self):
|
||||
path = self.translate_path(self.path)
|
||||
path = self._redirectWebExports(path)
|
||||
try:
|
||||
isdir = os.path.isdir(path)
|
||||
except ValueError:
|
||||
# path too long exception on Windows
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
||||
return None
|
||||
|
||||
if isdir:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
||||
return None
|
||||
|
||||
ctype = self.guess_type(path)
|
||||
try:
|
||||
f = open(path, "rb")
|
||||
except OSError:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
|
||||
return None
|
||||
try:
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-type", ctype)
|
||||
fs = os.fstat(f.fileno())
|
||||
self.send_header("Content-Length", str(fs[6]))
|
||||
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
return f
|
||||
except:
|
||||
f.close()
|
||||
raise
|
||||
|
||||
def log_message(self, format, *args):
|
||||
if not devMode:
|
||||
return
|
||||
print(
|
||||
"%s - - [%s] %s"
|
||||
% (self.address_string(), self.log_date_time_string(), format % args)
|
||||
return flask.make_response(
|
||||
"For path '%s - %s' %s!" % (directory, path, error),
|
||||
HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
)
|
||||
|
||||
def _redirectWebExports(self, path):
|
||||
|
||||
def _redirectWebExports(path):
|
||||
# catch /_anki references and rewrite them to web export folder
|
||||
targetPath = os.path.join(os.getcwd(), "_anki", "")
|
||||
targetPath = "_anki/"
|
||||
if path.startswith(targetPath):
|
||||
newPath = os.path.join(_exportFolder, path[len(targetPath) :])
|
||||
return newPath
|
||||
return _exportFolder, path[len(targetPath) :]
|
||||
|
||||
# catch /_addons references and rewrite them to addons folder
|
||||
targetPath = os.path.join(os.getcwd(), "_addons", "")
|
||||
targetPath = "_addons/"
|
||||
if path.startswith(targetPath):
|
||||
try:
|
||||
addMgr = self.mw.addonManager
|
||||
except AttributeError:
|
||||
return path
|
||||
|
||||
addonPath = path[len(targetPath) :]
|
||||
|
||||
try:
|
||||
addMgr = _redirectWebExports.mw.addonManager
|
||||
except AttributeError as error:
|
||||
if devMode:
|
||||
print("_redirectWebExports: %s" % error)
|
||||
return _exportFolder, addonPath
|
||||
|
||||
try:
|
||||
addon, subPath = addonPath.split(os.path.sep, 1)
|
||||
except ValueError:
|
||||
return path
|
||||
return addMgr.addonsFolder(), path
|
||||
if not addon:
|
||||
return path
|
||||
return addMgr.addonsFolder(), path
|
||||
|
||||
pattern = addMgr.getWebExports(addon)
|
||||
if not pattern:
|
||||
return path
|
||||
return addMgr.addonsFolder(), path
|
||||
|
||||
subPath2 = subPath.replace(os.sep, "/")
|
||||
if re.fullmatch(pattern, subPath) or re.fullmatch(pattern, subPath2):
|
||||
newPath = os.path.join(addMgr.addonsFolder(), addonPath)
|
||||
return newPath
|
||||
if re.fullmatch(pattern, subPath):
|
||||
return addMgr.addonsFolder(), addonPath
|
||||
|
||||
return path
|
||||
|
||||
def do_POST(self):
|
||||
if not self.path.startswith("/_anki/"):
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Method not found")
|
||||
return
|
||||
|
||||
cmd = self.path[len("/_anki/") :]
|
||||
|
||||
if cmd == "graphData":
|
||||
content_length = int(self.headers["Content-Length"])
|
||||
body = self.rfile.read(content_length)
|
||||
data = graph_data(self.mw.col, **from_json_bytes(body))
|
||||
elif cmd == "i18nResources":
|
||||
data = self.mw.col.backend.i18n_resources()
|
||||
else:
|
||||
self.send_error(HTTPStatus.NOT_FOUND, "Method not found")
|
||||
return
|
||||
|
||||
self.send_response(HTTPStatus.OK)
|
||||
self.send_header("Content-Type", "application/binary")
|
||||
self.send_header("Content-Length", str(len(data)))
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.end_headers()
|
||||
|
||||
self.wfile.write(data)
|
||||
return _redirectWebExports.mw.col.media.dir(), path
|
||||
|
||||
|
||||
def graph_data(col: Collection, search: str, days: int) -> bytes:
|
||||
@ -207,7 +198,3 @@ def graph_data(col: Collection, search: str, days: int) -> bytes:
|
||||
# likely searching error
|
||||
print(e)
|
||||
return b""
|
||||
|
||||
|
||||
# work around Windows machines with incorrect mime type
|
||||
RequestHandler.extensions_map[".css"] = "text/css"
|
||||
|
@ -28,6 +28,9 @@ install_requires = [
|
||||
"jsonschema",
|
||||
# "pyaudio", # https://anki.tenderapp.com/discussions/add-ons/44009-problems-with-code-completion
|
||||
# "pyqtwebengine", # https://github.com/ankitects/anki/pull/530 - Set to checks.yml install and import anki wheels
|
||||
"flask",
|
||||
"flask_cors",
|
||||
"waitress",
|
||||
"pyqt5>=5.9",
|
||||
'psutil; sys.platform == "win32"',
|
||||
'pywin32; sys.platform == "win32"',
|
||||
|
Loading…
Reference in New Issue
Block a user