- 后端: - UploadService.delete_file 下沉删除逻辑(DB 行 + 磁盘文件),单删/批删复用。 - POST /api/admin/files/batch-delete:批量硬删除,返回 deleted 数与 not_found 列表。 - list 接口加 Query 约束(limit 1-10000、offset>=0),非法返回 422。 - 前端(file_browser): - 复选框列 + 全选/反选,选中行高亮,批量操作栏(已选计数/下载/删除/取消)。 - 分页控件:每页 20/50/100/全部 切换,页码导航(首页/末页/当前±1/省略号)。 - 批量下载逐个触发(间隔 150ms 防浏览器拦截);批量删除走 batch-delete 接口。 - 删除后重载当前页,当前页空则回退一页。
132 lines
4.2 KiB
Python
132 lines
4.2 KiB
Python
"""文件管理接口(Basic Auth,鉴权同 docs)。
|
||
|
||
与公开的 /api/files 区分:本路由面向「文件浏览页」,提供列表 / 查询 / 下载 / 删除,
|
||
均需 docs 凭据。公开路由(user.py 依赖的查重、查询、下载)保留不变。
|
||
|
||
硬删除策略:删 DB 行 + 删磁盘文件(unlink missing_ok),列表只展示仍存在的行。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||
from fastapi.responses import FileResponse
|
||
from pydantic import BaseModel, Field
|
||
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
|
||
|
||
|
||
class BatchDeleteRequest(BaseModel):
|
||
ids: list[int] = Field(..., description="要删除的文件 id 列表")
|
||
|
||
|
||
class BatchDeleteResult(BaseModel):
|
||
deleted: int = Field(..., description="实际删除的条数")
|
||
not_found: list[int] = Field(default_factory=list, description="未找到的 id")
|
||
|
||
|
||
@router.get(
|
||
"",
|
||
response_model=FileListResponse,
|
||
summary="列出已上传文件(需鉴权)",
|
||
description="供文件浏览页使用;字段同公开 /api/files,但要求 docs Basic Auth。支持分页。",
|
||
)
|
||
def list_files(
|
||
limit: int = Query(100, ge=1, le=10000, description="每页条数,1-10000"),
|
||
offset: int = Query(0, ge=0, description="偏移量"),
|
||
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.post(
|
||
"/batch-delete",
|
||
response_model=BatchDeleteResult,
|
||
summary="批量硬删除文件(需鉴权)",
|
||
description="一次删除多个文件;返回实际删除条数与未命中的 id 列表。",
|
||
)
|
||
def batch_delete_files(
|
||
body: BatchDeleteRequest,
|
||
service: UploadService = Depends(_service),
|
||
_: str = Depends(require_docs_auth),
|
||
) -> BatchDeleteResult:
|
||
if not body.ids:
|
||
raise HTTPException(400, "ids 不能为空")
|
||
deleted = 0
|
||
not_found: list[int] = []
|
||
for fid in body.ids:
|
||
ok = service.delete_file(fid)
|
||
if ok:
|
||
deleted += 1
|
||
else:
|
||
not_found.append(fid)
|
||
return BatchDeleteResult(deleted=deleted, not_found=not_found)
|
||
|
||
|
||
@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:
|
||
return DeleteResult(deleted=service.delete_file(file_id))
|