Files
zTools2/app/controllers/file_admin_controller.py
zikai 41580a9025 feat: 文件浏览页支持多选、批量下载/删除与分页
- 后端:
  - 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 接口。
  - 删除后重载当前页,当前页空则回退一页。
2026-07-21 14:37:13 +00:00

132 lines
4.2 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.

"""文件管理接口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))