Files
zTools2/app/controllers/chunk_upload_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

94 lines
3.0 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.

"""分片上传接口:创建会话 / 查状态 / 传分片 / 完成拼接。
支持大文件分片上传与断点续传。前端先创建会话拿 upload_id逐片上传
可随时查 status 获取已传分片以补传缺失部分,最后 complete 触发拼接入库。
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, Request
from sqlalchemy.orm import Session
from ..database import get_db
from ..dao.upload_session_dao import UploadSessionDAO
from ..dao.uploaded_file_dao import UploadedFileDAO
from ..schemas.chunk import (
ChunkUploadResponse,
CreateSessionRequest,
CreateSessionResponse,
SessionStatusResponse,
)
from ..schemas.file import FileUploadResponse
from ..services.chunk_upload_service import ChunkUploadService
router = APIRouter(prefix="/api/files/chunk-uploads", tags=["chunk-upload"])
def _service(db: Session = Depends(get_db)) -> ChunkUploadService:
return ChunkUploadService(UploadSessionDAO(db), UploadedFileDAO(db))
@router.post(
"",
response_model=CreateSessionResponse,
summary="创建分片上传会话",
description=(
"客户端把文件切成固定大小的分片后,先调本接口创建会话。"
"服务端生成 upload_id 返回,后续上传分片、查状态、完成拼接都需要它。"
),
)
def create_session(
body: CreateSessionRequest,
service: ChunkUploadService = Depends(_service),
) -> CreateSessionResponse:
return service.create_session(body)
@router.get(
"/{upload_id}/status",
response_model=SessionStatusResponse,
summary="查询会话状态(断点续传)",
description="返回已上传分片下标集合,前端据此只补传缺失分片。",
)
def session_status(
upload_id: str,
service: ChunkUploadService = Depends(_service),
) -> SessionStatusResponse:
return service.get_status(upload_id)
@router.post(
"/{upload_id}/chunks/{index}",
response_model=ChunkUploadResponse,
summary="上传单个分片",
description=(
"请求体为单个分片的原始二进制。分片可乱序上传,重传同一分片会覆盖。"
"返回当前已上传的分片下标集合。"
),
)
async def upload_chunk(
upload_id: str,
index: int,
request: Request,
service: ChunkUploadService = Depends(_service),
) -> ChunkUploadResponse:
uploaded = service.write_chunk(upload_id, index, await request.body())
return ChunkUploadResponse(upload_id=upload_id, index=index, uploaded_chunks=uploaded)
@router.post(
"/{upload_id}/complete",
response_model=FileUploadResponse,
summary="完成拼接并入库",
description=(
"服务端校验分片齐全后,按顺序拼接为完整文件、流式计算 sha256"
"按 sha256 去重(命中则返回旧行不重复落盘),最后原子改名入库。"
"重复调用幂等,返回同一 file_id。"
),
)
def complete_session(
upload_id: str,
service: ChunkUploadService = Depends(_service),
) -> FileUploadResponse:
return service.complete(upload_id)