Skip to content

Commit

Permalink
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 122 deletions.
226 changes: 104 additions & 122 deletions qt/aqt/mediasrv.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
# Copyright: Ankitects Pty Ltd and contributors
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
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

from anki.collection import _Collection
import flask
import flask_cors # type: ignore
from waitress.server import create_server # type: ignore

from anki.utils import devMode
from aqt.qt import *
from aqt.utils import aqt_data_folder
Expand All @@ -28,146 +31,125 @@ 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):
super().__init__(*args, **kwargs)
self.mw = mw
self.is_shutdown = False
_redirectWebExports.mw = mw

def run(self):
RequestHandler.mw = self.mw
self.server = ThreadedHTTPServer(("127.0.0.1", 0), RequestHandler)
self._ready.set()
self.server.serve_forever()
try:
if devMode:
# idempotent if logging has already been set up
logging.basicConfig()

def getPort(self):
self._ready.wait()
return self.server.server_port
self.server = create_server(app, host="127.0.0.1", port=0)
if devMode:
print(
"Serving on http://%s:%s"
% (self.server.effective_host, self.server.effective_port)
)

self._ready.set()
self.server.run()

except Exception:
if not self.is_shutdown:
raise

def shutdown(self):
self.server.shutdown()


class RequestHandler(http.server.SimpleHTTPRequestHandler):

timeout = 1
mw: Optional[_Collection] = None

def do_GET(self):
f = self.send_head()
if f:
try:
self.copyfile(f, self.wfile)
except Exception as e:
if devMode:
print("http server caught exception:", e)
else:
# 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
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()

if isdir:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
return None
def getPort(self):
self._ready.wait()
return int(self.server.effective_port)


@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")
def allroutes(path):
directory, path = _redirectWebExports(path)
try:
isdir = os.path.isdir(os.path.join(directory, path))
except ValueError:
return flask.Response(
"Path for '%s - %s' is too long!" % (directory, path),
status=HTTPStatus.BAD_REQUEST,
mimetype="text/plain",
)

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)
if isdir:
return flask.Response(
"Path for '%s - %s' is a directory (not supported)!" % (directory, path),
status=HTTPStatus.FORBIDDEN,
mimetype="text/plain",
)

def _redirectWebExports(self, path):
# catch /_anki references and rewrite them to web export folder
targetPath = os.path.join(os.getcwd(), "_anki", "")
if path.startswith(targetPath):
newPath = os.path.join(_exportFolder, path[len(targetPath) :])
return newPath
try:
if devMode:
print("Sending file '%s - %s'" % (directory, path))
return flask.send_from_directory(directory, path)

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
return flask.Response(
"For path '%s - %s' %s!" % (directory, path, error),
status=HTTPStatus.INTERNAL_SERVER_ERROR,
mimetype="text/plain",
)

# catch /_addons references and rewrite them to addons folder
targetPath = os.path.join(os.getcwd(), "_addons", "")
if path.startswith(targetPath):
try:
addMgr = self.mw.addonManager
except AttributeError:
return path

addonPath = path[len(targetPath) :]
def _redirectWebExports(path):
# catch /_anki references and rewrite them to web export folder
targetPath = "_anki/"
if path.startswith(targetPath):
return _exportFolder, path[len(targetPath) :]

try:
addon, subPath = addonPath.split(os.path.sep, 1)
except ValueError:
return path
if not addon:
return path
# catch /_addons references and rewrite them to addons folder
targetPath = "_addons/"
if path.startswith(targetPath):
addonPath = path[len(targetPath) :]

pattern = addMgr.getWebExports(addon)
if not pattern:
return path
try:
addMgr = _redirectWebExports.mw.addonManager
except AttributeError as error:
if devMode:
print("_redirectWebExports: %s" % error)
return _exportFolder, addonPath

subPath2 = subPath.replace(os.sep, "/")
if re.fullmatch(pattern, subPath) or re.fullmatch(pattern, subPath2):
newPath = os.path.join(addMgr.addonsFolder(), addonPath)
return newPath
try:
addon, subPath = addonPath.split(os.path.sep, 1)
except ValueError:
return addMgr.addonsFolder(), path
if not addon:
return addMgr.addonsFolder(), path

return path
pattern = addMgr.getWebExports(addon)
if not pattern:
return addMgr.addonsFolder(), path

if re.fullmatch(pattern, subPath):
return addMgr.addonsFolder(), addonPath

# work around Windows machines with incorrect mime type
RequestHandler.extensions_map[".css"] = "text/css"
return _redirectWebExports.mw.col.media.dir(), path
3 changes: 3 additions & 0 deletions qt/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ def package_files(directory):
"pyaudio",
"markdown",
"jsonschema",
"flask",
"flask_cors",
"waitress",
"pyqt5>=5.9",
'psutil; sys.platform == "win32"',
'pywin32; sys.platform == "win32"',
Expand Down

0 comments on commit 72ce323

Please sign in to comment.