上传与后台任务
上传与后台任务 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
这一课按本站资料库(/api/documents)的真实设计,用 Python 实现文档上传:先验大小和类型再落盘,用状态机记录解析进度和失败原因,失败可重试,删除时把文件和索引清干净。
问题场景
你要做自己的资料库:上传 markdown/PDF 文档,后台解析切分建索引,AI 助手检索时引用。要防的事很具体:用户传 200MB 的文件撑爆磁盘、传 exe 冒充文档、解析到一半服务重启后文档永远卡在「处理中」、删除文档后磁盘上还留着孤儿文件。本站的答案是一套状态机 + 检查前置 + 可重试设计,Python 侧逐点照做。
核心解释
上传接口的核心顺序:先检查,后落盘,再建任务。
import os
import uuid
from pathlib import Path
from fastapi import FastAPI, UploadFile, HTTPException, BackgroundTasks
app = FastAPI()
UPLOAD_DIR = Path("data/uploads")
ACCEPTED_EXT = [".md", ".txt", ".pdf"]
MAX_FILE_BYTES = 10 * 1024 * 1024 # 10MB,与本站一致
@app.put("/documents")
async def upload(file: UploadFile, collection: str = "default",
tasks: BackgroundTasks = BackgroundTasks()):
ext = Path(file.filename or "").suffix.lower()
if ext not in ACCEPTED_EXT:
raise HTTPException(400, detail=f"不支持的文件类型 {ext},支持:{' '.join(ACCEPTED_EXT)}")
size = file.size or 0
if size > MAX_FILE_BYTES:
raise HTTPException(400, detail="文件超过 10MB 上限")
stored = f"{uuid.uuid4().hex}{ext}" # 存储名与原名解耦
path = UPLOAD_DIR / stored
path.write_bytes(await file.read())
doc_id = f"doc_{uuid.uuid4().hex[:8]}"
set_status(doc_id, "pending") # 1. 记 pending
tasks.add_task(index_document, doc_id, path) # 2. 后台解析
return {"ok": True, "id": doc_id, "status": "pending"}
状态机与失败处理是这一课的灵魂,逐条对齐本站设计:
- 状态只有四值:
pending → indexed或pending → failed。状态存document.status,失败原因写error字段——本站两张表(document+index_job)分头记录,理由是失败原因必须可解释、可查询,而不是丢一句「失败了」。 - 失败不删文件、保留原因。解析失败(比如 PDF 编码不支持)时状态置
failed+ 原因入库,前端能看到「为什么失败」并触发重试;重试就是重新跑一遍解析任务,而不是要求重新上传。 - BackgroundTasks 够用就别上重型队列。FastAPI 的
BackgroundTasks在响应返回后执行函数,适合本机单人场景;真正的分布式队列(Celery)在数据规模到之前都是负债。 - 删除 = 级联三清:删数据库行、删切分索引、删磁盘文件(本站 DELETE 接口正是这个顺序)。落盘时用了随机存储名,删除时按
stored_name字段找回,别用原始文件名拼路径——重名和路径穿越都从这来。
示例拆解
状态查询接口把整个链路串起来:
@app.get("/documents/{doc_id}")
def get_document(doc_id: str):
doc = get_doc(doc_id)
return {"id": doc_id, "status": doc["status"], "error": doc["error"],
"chunks": doc["chunks"]} # 前端轮询这个接口直到状态离开 pending
验证流程:上传一个 2MB 的 md 文件,立即 GET 查询应看到 pending,几秒后变 indexed 且 chunks 有值;再传一个 .exe,应直接 400 带出支持的类型列表;手动把解析函数改抛异常,重传后状态应停在 failed 且 error 里是异常信息——三个场景分别验证检查前置、状态推进、失败留痕。
动手任务
- 实现
retry_document(doc_id):把failed的文档重新放进BackgroundTasks,状态回到pending。 - 给上传加「同名覆盖检测」:同 collection 下同名文件已存在时返回 409,让调用方显式决定是否替换。
- 实现
DELETE /documents?id=...:按顺序清数据库行、索引、磁盘文件,并验证磁盘上不留孤儿文件。
常见错误
- 先落盘再校验:超限文件已经写进磁盘,校验失败留下垃圾文件;大小和类型检查必须在 write 之前。
- 用原始文件名存储:重名覆盖、路径穿越(
../../)全来了;存储名随机化,原名只作展示。 - 状态卡在 pending:进程重启后台任务就没了,没有重试机制的状态机是僵尸制造机;启动时把 pending 超时的文档标记 failed 或重排。
- 删除只删数据库:文件和切分索引成为孤儿,磁盘慢慢被吃光;删除操作必须级联且验证。
检查与复盘
自查清单:
- 大小/类型检查发生在落盘之前,非法文件不留痕
- 每个文档随时能查到状态和失败原因,failed 可重试
- 删除一个文档后,数据库、索引、磁盘三处都干净
参考资料
- FastAPI 官方教程:Request Files —— UploadFile 与表单上传
- FastAPI 官方教程:Background Tasks —— 响应后执行的后台任务
读完这一课了?
标记完成后,工作台会安排接下来的学习。