Files
selfgroup/group_system/http/server.py
2026-09-09 16:43:57 +08:00

139 lines
4.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()