rewrited logic structure
This commit is contained in:
138
group_system/http/server.py
Normal file
138
group_system/http/server.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user