Files
zTools2/app/controllers/file_controller.py
zikai 6c7d3f1272 feat: 增加 /api/files/exists 与 /api/files/register-sftp 接口
- DAO 增加 get_by_sha256
- UploadService 抽出 _to_response 复用;新增 find_by_sha256、register_sftp
- 控制器新增两条路由(注意排在 /{file_id} 之前以避免被吞)
- 引入 SftpRegisterRequest schema
- SFTP 服务启动时自动创建 uploads/incoming/
- 行为:路径穿越 400、文件不存在 404、sha256 已存在则去重返回已有行
2026-06-23 16:22:03 +00:00

114 lines
3.7 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 pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, 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,
SftpRegisterRequest,
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"
"极大或极慢的传输建议改用 SFTP详见 READMEHTTP 链路受 Apache 代理 300s 超时限制。"
),
)
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 = 100,
offset: int = 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.post(
"/register-sftp",
response_model=FileUploadResponse,
summary="登记一个已通过 SFTP 落盘的文件",
description=(
"客户端先把文件 SFTP 到 ``incoming/<name>``,再用本接口登记入库。"
"服务端会计算 sha256已存在则去重、把文件原子改名到 ``YYYY/MM/<uuid>.<ext>``、写 DB 行。"
),
)
def register_sftp(
body: SftpRegisterRequest,
service: UploadService = Depends(_service),
) -> FileUploadResponse:
return service.register_sftp(
filename=body.filename,
original_filename=body.original_filename,
uploaded_by=body.uploaded_by,
)
@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 = service.get_out(file_id)
if out is None:
raise HTTPException(404, "文件不存在")
path: Path | None = service.resolve_disk_path(file_id)
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,
)