Files
zTools2/app/controllers/file_controller.py
zikai 30a263ed50 fix: 代码审查修复 + 精简重写 README
后端修复:
- 白板删除踢人失效:delete_whiteboard 改 async def,删除后直接 await
  hub.close_board()。原实现用 asyncio.get_running_loop() 在同步 REST handler
  (threadpool)里调用必抛 RuntimeError 被 except 吞掉,close_board 从不执行。
  同时移除 service 的 hub 依赖(close_board 改由 controller 调用,service 只管 DB)。
- delete_file 去重复查询:原先 get_out_with_disk_path + get_by_id 查两次,
  合并为一次;磁盘 unlink 失败加 logger.warning(原静默吞掉致磁盘泄漏无记录)。
- get_hub 单例加 threading.Lock 双重检查(防 REST threadpool 与 WS 事件循环
  并发首访各建一个 hub)。
- file_controller 公开 /api/files list 加 Query(ge=1, le=10000) 约束(原无上限可 DoS)。

前端修复:
- applyRemoteUpdate 有未发送编辑时重发:合并远端更新后若本地有 pending 编辑
  (editor.value !== lastSentText)重新 scheduleSend,避免被 lastSentText 短路丢弃。
- init 不覆盖未发送编辑:断线重连后若本地有未发送内容,作为新版本发上去而非被 init 覆盖。
- applyRemoteUpdate 仅在编辑器已有焦点时恢复焦点,避免抢按钮焦点。
- api() 401 时 location.reload() 触发浏览器 Basic Auth 弹窗(原只 toast 卡死)。

README:
- 精简重写,补全 Ubuntu 从 0 安装、Apache 反代(含 WS)、配置项表格、防火墙说明。
2026-07-22 01:03:48 +00:00

90 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.

"""文件上传 / 列表 / 下载接口。"""
from __future__ import annotations
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from ..database import get_db
from ..dao.uploaded_file_dao import UploadedFileDAO
from ..schemas.file import (
FileListResponse,
FileUploadResponse,
UploadedFileOut,
)
from ..services.upload_service import UploadService
router = APIRouter(prefix="/api/files", tags=["files"])
def _service(db: Session = Depends(get_db)) -> UploadService:
return UploadService(UploadedFileDAO(db))
@router.post(
"/upload",
response_model=FileUploadResponse,
summary="上传单个文件(流式,支持大文件)",
description=(
"multipart/form-data 上传,按 1 MiB 分片流式落盘,内存占用恒定;"
"落盘过程中计算 SHA-256 并入库。\n\n"
"极大或极慢的传输建议改用分片上传接口(/api/files/chunk-uploads或 SFTP。"
),
)
async def upload_file(
file: UploadFile = File(..., description="要上传的文件"),
service: UploadService = Depends(_service),
) -> FileUploadResponse:
if not file.filename:
raise HTTPException(400, "请求缺少 'file' 字段")
return service.stream_to_disk(file, source="http", uploaded_by="anonymous")
@router.get("", response_model=FileListResponse, summary="列出已上传的文件")
def list_files(
limit: int = Query(100, ge=1, le=10000),
offset: int = Query(0, ge=0),
service: UploadService = Depends(_service),
) -> FileListResponse:
total, items = service.list_files(limit=limit, offset=offset)
return FileListResponse(total=total, items=items)
@router.get(
"/exists",
response_model=UploadedFileOut,
summary="按 sha256 查询是否已上传",
description="命中返回 200 + 元数据;未命中返回 404。供客户端在上传前去重。",
)
def file_exists(
sha256: str,
service: UploadService = Depends(_service),
) -> UploadedFileOut:
out = service.find_by_sha256(sha256)
if out is None:
raise HTTPException(404, "未找到匹配的 sha256")
return out
@router.get("/{file_id}", response_model=UploadedFileOut, summary="查询单个文件元数据")
def get_file(file_id: int, service: UploadService = Depends(_service)) -> UploadedFileOut:
out = service.get_out(file_id)
if out is None:
raise HTTPException(404, "文件不存在")
return out
@router.get("/{file_id}/download", summary="下载文件")
def download_file(file_id: int, service: UploadService = Depends(_service)) -> FileResponse:
out, path = service.get_out_with_disk_path(file_id)
if out is None:
raise HTTPException(404, "文件不存在")
if path is None or not path.exists():
raise HTTPException(410, "文件实体已不在磁盘上")
return FileResponse(
path=str(path),
media_type=out.content_type or "application/octet-stream",
filename=out.original_filename,
)