anki/qt/aqt/mediasrv.py

211 lines
6.5 KiB
Python
Raw Normal View History

2019-02-05 04:59:03 +01:00
# Copyright: Ankitects Pty Ltd and contributors
2016-07-07 15:39:48 +02:00
# -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
from __future__ import annotations
2019-12-20 10:19:03 +01:00
import http.server
import re
import socket
import socketserver
import threading
from http import HTTPStatus
2019-12-20 06:07:40 +01:00
from typing import Optional
2016-07-07 15:39:48 +02:00
import aqt
2020-05-20 09:56:52 +02:00
from anki.collection import Collection
from anki.rsbackend import from_json_bytes
from anki.utils import devMode
2019-12-20 10:19:03 +01:00
from aqt.qt import *
from aqt.utils import aqt_data_folder
2019-12-20 10:19:03 +01:00
2016-07-07 15:39:48 +02:00
2017-07-27 04:28:44 +02:00
def _getExportFolder():
data_folder = aqt_data_folder()
webInSrcFolder = os.path.abspath(os.path.join(data_folder, "web"))
2017-07-27 04:28:44 +02:00
if os.path.exists(webInSrcFolder):
return webInSrcFolder
elif isMac:
dir = os.path.dirname(os.path.abspath(__file__))
return os.path.abspath(dir + "/../../Resources/web")
else:
2019-03-04 07:54:22 +01:00
raise Exception("couldn't find web folder")
2017-07-27 04:28:44 +02:00
2019-12-23 01:34:10 +01:00
2017-07-27 04:28:44 +02:00
_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
2019-12-23 01:34:10 +01:00
class MediaServer(threading.Thread):
2016-07-07 15:39:48 +02:00
2019-12-20 06:07:40 +01:00
_port: Optional[int] = None
_ready = threading.Event()
daemon = True
def __init__(self, mw, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mw = mw
2016-07-07 15:39:48 +02:00
def run(self):
RequestHandler.mw = self.mw
desired_port = int(os.getenv("ANKI_API_PORT", 0))
self.server = ThreadedHTTPServer(("127.0.0.1", desired_port), RequestHandler)
self._ready.set()
2016-07-07 15:39:48 +02:00
self.server.serve_forever()
def getPort(self):
self._ready.wait()
return self.server.server_port
def shutdown(self):
self.server.shutdown()
2019-12-23 01:34:10 +01:00
2016-07-07 15:39:48 +02:00
class RequestHandler(http.server.SimpleHTTPRequestHandler):
timeout = 10
mw: Optional[aqt.main.AnkiQt] = None
2016-07-08 05:22:36 +02:00
def do_GET(self):
f = self.send_head()
if f:
try:
self.copyfile(f, self.wfile)
except Exception as e:
if devMode:
2016-07-08 05:22:36 +02:00
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()
2016-07-07 15:39:48 +02:00
def send_head(self):
path = self.translate_path(self.path)
2017-07-27 04:28:44 +02:00
path = self._redirectWebExports(path)
try:
isdir = os.path.isdir(path)
except ValueError:
# path too long exception on Windows
2016-07-07 15:39:48 +02:00
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
return None
if isdir:
self.send_error(HTTPStatus.NOT_FOUND, "File not found")
return None
2016-07-07 15:39:48 +02:00
ctype = self.guess_type(path)
try:
2019-12-23 01:34:10 +01:00
f = open(path, "rb")
2016-07-07 15:39:48 +02:00
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))
2017-02-21 04:27:29 +01:00
self.send_header("Access-Control-Allow-Origin", "*")
2016-07-07 15:39:48 +02:00
self.end_headers()
return f
except:
f.close()
raise
def log_message(self, format, *args):
if not devMode:
2016-07-07 15:39:48 +02:00
return
2019-12-23 01:34:10 +01:00
print(
"%s - - [%s] %s"
% (self.address_string(), self.log_date_time_string(), format % args)
)
2017-07-27 04:28:44 +02:00
def _redirectWebExports(self, path):
# catch /_anki references and rewrite them to web export folder
targetPath = os.path.join(os.getcwd(), "_anki", "")
2017-07-27 04:28:44 +02:00
if path.startswith(targetPath):
2019-12-23 01:34:10 +01:00
newPath = os.path.join(_exportFolder, path[len(targetPath) :])
2017-07-27 04:28:44 +02:00
return newPath
2019-12-23 01:34:10 +01:00
# 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
2019-12-23 01:34:10 +01:00
addonPath = path[len(targetPath) :]
try:
addon, subPath = addonPath.split(os.path.sep, 1)
except ValueError:
return path
if not addon:
return path
2019-12-23 01:34:10 +01:00
pattern = addMgr.getWebExports(addon)
if not pattern:
return path
2019-12-23 01:34:10 +01:00
subPath2 = subPath.replace(os.sep, "/")
if re.fullmatch(pattern, subPath) or re.fullmatch(pattern, subPath2):
newPath = os.path.join(addMgr.addonsFolder(), addonPath)
return newPath
2019-12-23 01:34:10 +01:00
2017-07-27 04:28:44 +02:00
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))
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)
def graph_data(col: Collection, search: str, days: str) -> bytes:
try:
return col.backend.graphs(search=search, days=days)
except Exception as e:
# likely searching error
print(e)
return b''
2019-12-23 01:34:10 +01:00
# work around Windows machines with incorrect mime type
2019-12-23 01:34:10 +01:00
RequestHandler.extensions_map[".css"] = "text/css"