"""文件上传/管理路由""" import uuid from pathlib import Path from fastapi import APIRouter, HTTPException, UploadFile, File from sqlmodel import Session, select from ..database import engine, DATA_DIR from ..models import Novel, NovelFile from ..schemas import FileRead, FileList from ..services.file_parser import parse_file, ALLOWED_EXTENSIONS, MAX_FILE_SIZE router = APIRouter(prefix="/api/novels/{novel_id}/files", tags=["files"]) # 上传文件存储根目录 UPLOAD_DIR = DATA_DIR / "uploads" def _get_novel_upload_dir(novel_id: int) -> Path: """获取小说的上传目录,不存在则创建""" d = UPLOAD_DIR / str(novel_id) d.mkdir(parents=True, exist_ok=True) return d @router.post("", response_model=FileRead, status_code=201) async def upload_file(novel_id: int, file: UploadFile = File(...)): """上传文件并解析为纯文本""" with Session(engine) as session: # 校验小说存在 novel = session.get(Novel, novel_id) if not novel: raise HTTPException(status_code=404, detail="小说不存在") # 校验扩展名 if not file.filename: raise HTTPException(status_code=400, detail="缺少文件名") ext = Path(file.filename).suffix.lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException( status_code=400, detail=f"不支持的文件格式 {ext},仅支持: {', '.join(sorted(ALLOWED_EXTENSIONS))}", ) # 读取文件内容 content = await file.read() file_size = len(content) if file_size > MAX_FILE_SIZE: raise HTTPException( status_code=400, detail=f"文件过大({file_size / 1024 / 1024:.1f}MB),最大允许 10MB", ) # 生成存储名并保存原始文件 stored_name = f"{uuid.uuid4().hex}{ext}" upload_dir = _get_novel_upload_dir(novel_id) file_path = upload_dir / stored_name file_path.write_bytes(content) # 解析纯文本 try: text_content = parse_file(file_path, file.content_type or "") except Exception as e: # 解析失败,清理已保存的文件 file_path.unlink(missing_ok=True) raise HTTPException(status_code=400, detail=f"文件解析失败: {e}") # 保存纯文本缓存 text_path = upload_dir / f"{stored_name}.txt" text_path.write_text(text_content, encoding="utf-8") # 写入数据库 with Session(engine) as session: novel_file = NovelFile( novel_id=novel_id, filename=file.filename, stored_name=stored_name, file_size=file_size, mime_type=file.content_type or "", text_length=len(text_content), ) session.add(novel_file) session.commit() session.refresh(novel_file) return novel_file @router.get("", response_model=FileList) def list_files(novel_id: int): """列出小说关联的所有文件""" with Session(engine) as session: stmt = ( select(NovelFile) .where(NovelFile.novel_id == novel_id) .order_by(NovelFile.created_at.desc()) ) files = session.exec(stmt).all() items = [ FileRead( id=f.id, # type: ignore novel_id=f.novel_id, filename=f.filename, file_size=f.file_size, mime_type=f.mime_type, text_length=f.text_length, created_at=f.created_at, ) for f in files ] return FileList(items=items, total=len(items)) @router.delete("/{file_id}", status_code=204) def delete_file(novel_id: int, file_id: int): """删除文件(数据库记录 + 磁盘文件)""" with Session(engine) as session: novel_file = session.get(NovelFile, file_id) if not novel_file or novel_file.novel_id != novel_id: raise HTTPException(status_code=404, detail="文件不存在") # 删除磁盘文件 upload_dir = _get_novel_upload_dir(novel_id) (upload_dir / novel_file.stored_name).unlink(missing_ok=True) (upload_dir / f"{novel_file.stored_name}.txt").unlink(missing_ok=True) # 删除数据库记录 session.delete(novel_file) session.commit()