from datetime import datetime from typing import Annotated from uuid import UUID from fastapi import APIRouter, Depends, HTTPException from fastapi.responses import FileResponse from loguru import logger as l from sqlalchemy import and_ from middleware.auth import admin_required from middleware.dependencies import SessionDep, TableViewRequestDep from models import ( Policy, PolicyType, User, ResponseBase, ListResponse, Object, ObjectType, AdminFileResponse, FileBanRequest, ) from service.storage import LocalStorageService admin_file_router = APIRouter( prefix="/file", tags=["admin", "admin_file"], ) @admin_file_router.get( path='/list', summary='获取文件列表', description='Get file list', dependencies=[Depends(admin_required)], ) async def router_admin_get_file_list( session: SessionDep, table_view: TableViewRequestDep, user_id: UUID | None = None, is_banned: bool | None = None, keyword: str | None = None, ) -> ListResponse[AdminFileResponse]: """ 获取系统中的文件列表,支持筛选。 :param session: 数据库会话 :param table_view: 分页排序参数依赖 :param user_id: 按用户筛选 :param is_banned: 按封禁状态筛选 :param keyword: 按文件名搜索 :return: 分页文件列表 """ # 构建查询条件 conditions = [Object.type == ObjectType.FILE] if user_id: conditions.append(Object.owner_id == user_id) if is_banned is not None: conditions.append(Object.is_banned == is_banned) if keyword: conditions.append(Object.name.ilike(f"%{keyword}%")) condition = and_(*conditions) if len(conditions) > 1 else conditions[0] result = await Object.get_with_count(session, condition, table_view=table_view, load=Object.owner) # 构建响应 items: list[AdminFileResponse] = [] for f in result.items: owner = await f.awaitable_attrs.owner policy = await f.awaitable_attrs.policy items.append(AdminFileResponse.from_object(f, owner, policy)) return ListResponse(items=items, count=result.count) @admin_file_router.get( path='/preview/{file_id}', summary='预览文件', description='Preview file by ID', dependencies=[Depends(admin_required)], ) async def router_admin_preview_file( session: SessionDep, file_id: UUID, ) -> FileResponse: """ 管理员预览文件内容。 :param session: 数据库会话 :param file_id: 文件UUID :return: 文件内容 """ file_obj = await Object.get(session, Object.id == file_id) if not file_obj: raise HTTPException(status_code=404, detail="文件不存在") if not file_obj.is_file: raise HTTPException(status_code=400, detail="对象不是文件") # 获取物理文件 physical_file = await file_obj.awaitable_attrs.physical_file if not physical_file or not physical_file.storage_path: raise HTTPException(status_code=500, detail="文件存储路径丢失") policy = await Policy.get(session, Policy.id == file_obj.policy_id) if not policy: raise HTTPException(status_code=500, detail="存储策略不存在") if policy.type == PolicyType.LOCAL: storage_service = LocalStorageService(policy) if not await storage_service.file_exists(physical_file.storage_path): raise HTTPException(status_code=404, detail="物理文件不存在") return FileResponse( path=physical_file.storage_path, filename=file_obj.name, ) else: raise HTTPException(status_code=501, detail="S3 存储暂未实现") @admin_file_router.patch( path='/ban/{file_id}', summary='封禁/解禁文件', description='Ban the file, user can\'t open, copy, move, download or share this file if administrator ban.', dependencies=[Depends(admin_required)], ) async def router_admin_ban_file( session: SessionDep, file_id: UUID, request: FileBanRequest, admin: Annotated[User, Depends(admin_required)], ) -> ResponseBase: """ 封禁或解禁文件。封禁后用户无法访问该文件。 :param session: 数据库会话 :param file_id: 文件UUID :param request: 封禁请求 :param admin: 当前管理员 :return: 封禁结果 """ file_obj = await Object.get(session, Object.id == file_id) if not file_obj: raise HTTPException(status_code=404, detail="文件不存在") file_obj.is_banned = request.is_banned if request.is_banned: file_obj.banned_at = datetime.now() file_obj.banned_by = admin.id file_obj.ban_reason = request.reason else: file_obj.banned_at = None file_obj.banned_by = None file_obj.ban_reason = None file_obj = await file_obj.save(session) action = "封禁" if request.is_banned else "解禁" l.info(f"管理员{action}了文件: {file_obj.name}") return ResponseBase(data={ "id": str(file_obj.id), "is_banned": file_obj.is_banned, }) @admin_file_router.delete( path='/{file_id}', summary='删除文件', description='Delete file by ID', dependencies=[Depends(admin_required)], ) async def router_admin_delete_file( session: SessionDep, file_id: UUID, delete_physical: bool = True, ) -> ResponseBase: """ 删除文件。 :param session: 数据库会话 :param file_id: 文件UUID :param delete_physical: 是否同时删除物理文件 :return: 删除结果 """ file_obj = await Object.get(session, Object.id == file_id) if not file_obj: raise HTTPException(status_code=404, detail="文件不存在") if not file_obj.is_file: raise HTTPException(status_code=400, detail="对象不是文件") file_name = file_obj.name file_size = file_obj.size owner_id = file_obj.owner_id # 删除物理文件(可选) if delete_physical: physical_file = await file_obj.awaitable_attrs.physical_file if physical_file and physical_file.storage_path: policy = await Policy.get(session, Policy.id == file_obj.policy_id) if policy and policy.type == PolicyType.LOCAL: try: storage_service = LocalStorageService(policy) await storage_service.delete_file(physical_file.storage_path) except Exception as e: l.warning(f"删除物理文件失败: {e}") # 更新用户存储量 owner = await User.get(session, User.id == owner_id) if owner: owner.storage = max(0, owner.storage - file_size) await owner.save(session) await Object.delete(session, file_obj) l.info(f"管理员删除了文件: {file_name}") return ResponseBase(data={"deleted": True})