139 lines
4.3 KiB
Python
139 lines
4.3 KiB
Python
"""HTTP 服务器、静态路由和应用启动流程。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import http.server
|
||
import logging
|
||
import os
|
||
import socket
|
||
import urllib.parse
|
||
from http import HTTPStatus
|
||
from pathlib import Path
|
||
|
||
from group_system.config import STATIC_DIR, XLSX_PATH
|
||
from group_system.http.api import GroupRequestHandler
|
||
from group_system.http.static_files import send_text, serve_file
|
||
from group_system.storage.store import GroupStore
|
||
|
||
LOG = logging.getLogger("group-system")
|
||
|
||
|
||
class GroupHTTPServer(http.server.ThreadingHTTPServer):
|
||
"""Carry the application store on the standard threading HTTP server."""
|
||
|
||
def __init__(self, address: tuple[str, int], store: GroupStore):
|
||
super().__init__(address, RequestHandler)
|
||
self.store = store
|
||
|
||
|
||
class RequestHandler(http.server.BaseHTTPRequestHandler):
|
||
@property
|
||
def store(self) -> GroupStore:
|
||
return self.server.store # type: ignore[attr-defined]
|
||
|
||
def do_GET(self) -> None:
|
||
route(self)
|
||
|
||
def do_POST(self) -> None:
|
||
route(self)
|
||
|
||
def do_HEAD(self) -> None:
|
||
route(self)
|
||
|
||
def log_message(self, format_string: str, *args) -> None:
|
||
LOG.info("%s - %s", self.address_string(), format_string % args)
|
||
|
||
|
||
def route(handler: RequestHandler) -> None:
|
||
path = urllib.parse.urlparse(handler.path).path
|
||
if path.startswith("/api/"):
|
||
request = GroupRequestHandler(handler, handler.store)
|
||
result = request.handle_api(path[len("/api/"):], handler.command)
|
||
if callable(result):
|
||
result(handler)
|
||
return
|
||
|
||
if path in ("/", "/index.html", "/student"):
|
||
serve_file(handler, STATIC_DIR / "index.html")
|
||
return
|
||
if path in ("/teacher", "/admin"):
|
||
serve_file(handler, STATIC_DIR / "teacher.html")
|
||
return
|
||
|
||
static_root = STATIC_DIR.resolve()
|
||
safe_path = (static_root / urllib.parse.unquote(path).lstrip("/")).resolve()
|
||
try:
|
||
safe_path.relative_to(static_root)
|
||
except ValueError:
|
||
send_text(handler, HTTPStatus.NOT_FOUND, "Not Found")
|
||
return
|
||
serve_file(handler, safe_path)
|
||
|
||
|
||
def local_ipv4_addresses() -> list[str]:
|
||
addresses: set[str] = set()
|
||
try:
|
||
host_name = socket.gethostname()
|
||
for item in socket.getaddrinfo(host_name, None, socket.AF_INET):
|
||
address = item[4][0]
|
||
if address != "127.0.0.1":
|
||
addresses.add(address)
|
||
except socket.gaierror:
|
||
pass
|
||
|
||
# A UDP connection does not send packets here, but reveals the default-route address.
|
||
try:
|
||
probe = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||
try:
|
||
probe.connect(("192.0.2.1", 9))
|
||
address = probe.getsockname()[0]
|
||
if address != "127.0.0.1":
|
||
addresses.add(address)
|
||
finally:
|
||
probe.close()
|
||
except OSError:
|
||
pass
|
||
return sorted(addresses)
|
||
|
||
|
||
def bootstrap_store(store: GroupStore, xlsx_path: Path) -> None:
|
||
"""Import the initial roster only when the database is still empty."""
|
||
if any(store.class_list()):
|
||
return
|
||
if not xlsx_path.exists():
|
||
LOG.warning("未找到 %s,请先通过教师后台上传学生名单", xlsx_path)
|
||
return
|
||
student_count, class_count = store.import_roster(xlsx_path)
|
||
LOG.info("已导入 %s: %d 名学生 / %d 个班", xlsx_path, student_count, class_count)
|
||
|
||
|
||
def run(
|
||
store: GroupStore,
|
||
xlsx_path: Path = XLSX_PATH,
|
||
host: str | None = None,
|
||
port: int | None = None,
|
||
) -> None:
|
||
bootstrap_store(store, xlsx_path)
|
||
host = host or os.environ.get("HOST", "0.0.0.0")
|
||
port = port if port is not None else int(os.environ.get("PORT", "8000"))
|
||
server = GroupHTTPServer((host, port), store)
|
||
LOG.info("学生分组系统已启动:http://0.0.0.0:%d", port)
|
||
addresses = local_ipv4_addresses()
|
||
if addresses:
|
||
for address in addresses:
|
||
LOG.info(
|
||
"学生入口:http://%s:%d/ 教师入口:http://%s:%d/teacher",
|
||
address,
|
||
port,
|
||
address,
|
||
port,
|
||
)
|
||
else:
|
||
LOG.warning("未能自动获取局域网 IP,请在教师机上查看网络设置;端口:%d", port)
|
||
try:
|
||
server.serve_forever()
|
||
except KeyboardInterrupt:
|
||
LOG.info("服务停止")
|
||
finally:
|
||
server.server_close()
|