"""文件管理接口(Basic Auth,鉴权同 docs)。 与公开的 /api/files 区分:本路由面向「文件浏览页」,提供列表 / 查询 / 下载 / 删除, 均需 docs 凭据。公开路由(user.py 依赖的查重、查询、下载)保留不变。 硬删除策略:删 DB 行 + 删磁盘文件(unlink missing_ok),列表只展示仍存在的行。 """ from __future__ import annotations from pathlib import Path from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from pydantic import BaseModel from sqlalchemy.orm import Session from ..database import get_db from ..dao.uploaded_file_dao import UploadedFileDAO from ..schemas.file import FileListResponse, UploadedFileOut from ..security import require_docs_auth from ..services.upload_service import UploadService router = APIRouter(prefix="/api/admin/files", tags=["files-admin"]) def _service(db: Session = Depends(get_db)) -> UploadService: return UploadService(UploadedFileDAO(db)) class DeleteResult(BaseModel): deleted: bool @router.get( "", response_model=FileListResponse, summary="列出已上传文件(需鉴权)", description="供文件浏览页使用;字段同公开 /api/files,但要求 docs Basic Auth。", ) def list_files( limit: int = 100, offset: int = 0, service: UploadService = Depends(_service), _: str = Depends(require_docs_auth), ) -> FileListResponse: total, items = service.list_files(limit=limit, offset=offset) return FileListResponse(total=total, items=items) @router.get( "/{file_id}", response_model=UploadedFileOut, summary="查询单个文件元数据(需鉴权)", ) def get_file( file_id: int, service: UploadService = Depends(_service), _: str = Depends(require_docs_auth), ) -> UploadedFileOut: out = service.get_out(file_id) if out is None: raise HTTPException(404, "文件不存在") return out @router.get( "/{file_id}/download", summary="下载文件(需鉴权,校验磁盘存在)", description="文件实体不在磁盘上时返回 410 Gone。", ) def download_file( file_id: int, service: UploadService = Depends(_service), _: str = Depends(require_docs_auth), ) -> 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, ) @router.delete( "/{file_id}", response_model=DeleteResult, summary="硬删除文件(需鉴权)", description="删除 DB 行与磁盘文件实体;不可恢复。列表随后不再显示。", ) def delete_file( file_id: int, service: UploadService = Depends(_service), _: str = Depends(require_docs_auth), ) -> DeleteResult: out, path = service.get_out_with_disk_path(file_id) if out is None: return DeleteResult(deleted=False) # 先删磁盘文件,再删 DB 行;磁盘文件缺失不阻断 DB 清理 if path is not None: try: Path(path).unlink(missing_ok=True) except Exception: # 即使磁盘删除失败也继续清 DB 行,保证列表不再显示 pass service.dao.delete(file_id) return DeleteResult(deleted=True)