Files
zTools2/app/controllers/tunnel_controller.py
zikai fffba79022 feat: 文件浏览页 + 共享白板 + 白板管理页(含补登记分片上传/隧道历史改动)
本次提交包含两批改动(7月2日遗留未入库 + 本次新功能),分述如下:

【补登记:7月2日已上线但未提交的功能】
- 分片上传:chunk_upload_controller/service/dao + UploadSession model/schema,
  支持 4MiB 分片、乱序、断点续传、去重、幂等 complete;后台 reaper 清理过期会话。
- 反向隧道:tunnel_controller/service/dao + TunnelSession model/schema,
  SSH remote forward 经 /api/userPort/{userName} 反代到 user 本地服务。
- 上传页:views/upload_html.py(拖拽/多文件/分片/断点续传 UI)。
- config.py:StorageConfig.chunk_session_dir/ttl、TunnelConfig;
  requirements.txt 加 httpx;start.sh 清理 .work/ 残留;
  schema.sql 加 upload_session/tunnel_session 表;sftp_server 承载隧道转发。

【本次新功能】
- 文件浏览页:GET /files(Basic Auth 同 docs)+ /api/admin/files(list/get/download/DELETE)。
  硬删除(DB 行 + 磁盘文件),删除后列表不再显示。前端 static/file_browser.*。
- 共享白板:GET /whiteboard/{id}(公开,不存在则新建)+ WS /ws/whiteboard/{id}。
  MySQL 持久化(whiteboard 表),Canvas 实时同步,心跳 3s/5 次失活移除,
  清空/复制按钮,移动端兼容。WhiteboardHub 管理 {board_id: set[Connection]},
  disconnect 幂等 + 空 set 清理防泄漏,broadcast 失败连接自动移除。
- 白板管理页:GET /whiteboard-admin(Basic Auth)+ /api/admin/whiteboards(list/DELETE)。
  删除时 hub.close_board 踢出在线连接。
- 清理:合并 UploadService.get_out_with_disk_path(下载/删除复用,消除重复 DB 读),
  移除无用 resolve_disk_path。
- config.py:WhiteboardConfig(heartbeat/threshold/board_id 长度/list_limit);
  schema.sql 加 whiteboard 表;README 补新接口与心跳/内存说明。
- 验证:tests/manual_whiteboard_hub.py / _ws.py / _kick.py 全部通过。
2026-07-21 14:28:13 +00:00

98 lines
3.6 KiB
Python
Raw 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 路由:/api/userPort/{userName}
把进来的 HTTP 请求反代到该 user 当前活跃隧道对应的本地端口
SSH remote forward 绑定的 127.0.0.1:tunnel_port经隧道回指 user 的本地服务。
"""
from __future__ import annotations
import httpx
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from sqlalchemy.orm import Session
from ..database import get_db
from ..dao.tunnel_session_dao import TunnelSessionDAO
from ..services.tunnel_service import TunnelService
router = APIRouter(prefix="/api/userPort", tags=["tunnel"])
# 不应透传给上游的 hop-by-hop / 控制头
_HOP_BY_HOP = {
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
"te", "trailers", "transfer-encoding", "upgrade", "host", "content-length",
}
def _service(db: Session = Depends(get_db)) -> TunnelService:
return TunnelService(TunnelSessionDAO(db))
async def _proxy(request: Request, session, prefix: str) -> Response:
"""把请求透传到该 user 当前活跃隧道对应的本地端口。
去掉 /api/userPort/{userName} 前缀后才是上游路径,根路径补 /。
"""
upstream_path = request.url.path.replace(prefix, "", 1) or "/"
url = f"http://127.0.0.1:{session.tunnel_port}{upstream_path}"
if request.url.query:
url += f"?{request.url.query}"
body = await request.body()
headers = {k: v for k, v in request.headers.items() if k.lower() not in _HOP_BY_HOP}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
upstream = await client.request(
request.method, url, content=body, headers=headers,
)
except httpx.RequestError as exc:
raise HTTPException(502, f"隧道端口不可达:{exc}") from exc
resp_headers = {k: v for k, v in upstream.headers.items() if k.lower() not in _HOP_BY_HOP}
return Response(content=upstream.content, status_code=upstream.status_code,
headers=resp_headers)
# 根路径:/api/userPort/{userName}
@router.api_route(
"/{userName}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
summary="反代到指定 user 的隧道端口(根路径)",
description=(
"查 DB 该 user 当前活跃的隧道端口,把请求透传到 127.0.0.1:tunnel_port"
"(经 SSH 反向隧道回指 user 的本地服务)。无活跃隧道返回 502。"
),
)
async def proxy_to_tunnel(
userName: str,
request: Request,
service: TunnelService = Depends(_service),
) -> Response:
session = service.get_active(userName)
if session is None:
raise HTTPException(502, f"无活跃隧道user={userName}")
return await _proxy(request, session, f"/api/userPort/{userName}")
# 子路径:/api/userPort/{userName}/... —— 反向代理必须能透传任意路径与查询串,
# 否则上游服务里所有非根路由都会 404。
@router.api_route(
"/{userName}/{upstream_path:path}",
methods=["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"],
summary="反代到指定 user 的隧道端口(子路径透传)",
description=(
"把 /api/userPort/{userName}/<path> 透传到 127.0.0.1:tunnel_port/<path>。"
"无活跃隧道返回 502。"
),
)
async def proxy_to_tunnel_path(
userName: str,
upstream_path: str,
request: Request,
service: TunnelService = Depends(_service),
) -> Response:
session = service.get_active(userName)
if session is None:
raise HTTPException(502, f"无活跃隧道user={userName}")
return await _proxy(request, session, f"/api/userPort/{userName}")