Files
FastapiAdmin/backend/app/modules/task/storage/browse/controller.py
T
zhangtao a8d29d4ab4 fix: 修复多项后端缺陷并加固安全与限流
- 修正 AI 会话表初始化及异步数据库 URI 处理
- 新增全局 API 与 WS 聊天限流,健康检查豁免
- 修复忘记密码接口防用户枚举与参数校验
- 修复 SSE 推送数据重复编码问题
- 修复 Cron 表达式 Quartz 问号兼容
- 修复存储模块权限标识前缀
- 新增过期日志清理与任务错误中文提示
2026-09-07 00:08:00 +08:00

175 lines
10 KiB
Python

import os
from typing import Annotated
from fastapi import APIRouter, BackgroundTasks, Body, Depends, File, Form, Query, Security, UploadFile
from fastapi.responses import JSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from app.common.response import ResponseSchema, SuccessResponse, UploadFileResponse
from app.core.base_schema import AuthSchema
from app.core.dependencies import AuthPermission, db_getter
from app.core.router_class import OperationLogRoute
from app.modules.task.storage.core.base import StorageObject, StoragePage
from .schema import StoragePathCreateSchema, StoragePathResultSchema, StorageUploadResultSchema
from .service import StorageFileService
StorageBrowseRouter = APIRouter(route_class=OperationLogRoute, prefix="/storage/browse", tags=["存储管理"])
def _delete_temp_file(path: str) -> None:
"""响应发送后清理临时下载文件。"""
try:
os.unlink(path)
except OSError:
pass
@StorageBrowseRouter.post("/upload", summary="上传文件到存储源", response_model=ResponseSchema[StorageUploadResultSchema])
async def upload_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:upload"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
file: Annotated[UploadFile, File(description="上传文件")],
source_id: Annotated[int | None, Form(description="存储源ID(不传使用默认存储源)")] = None,
remote_path: Annotated[str | None, Form(description="远端目录路径(不传自动生成文件名)")] = None,
bucket: Annotated[str | None, Form(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> JSONResponse:
result: StorageUploadResultSchema = await StorageFileService(auth, db).upload(
source_id=source_id, file=file, remote_path=remote_path, bucket=bucket
)
return SuccessResponse(data=result, msg="上传文件成功")
@StorageBrowseRouter.post("/download", summary="下载存储源文件", response_model=None)
async def download_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:download"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
background_tasks: BackgroundTasks,
remote_path: Annotated[str, Body(description="远端文件路径")],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
bucket: Annotated[str | None, Body(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> UploadFileResponse:
local_path, file_name = await StorageFileService(auth, db).download(
source_id=source_id, remote_path=remote_path, bucket=bucket
)
background_tasks.add_task(_delete_temp_file, local_path)
return UploadFileResponse(file_path=local_path, filename=file_name)
@StorageBrowseRouter.post("/download_dir", summary="下载存储源目录(递归打包ZIP)", response_model=None)
async def download_dir_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:download"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
background_tasks: BackgroundTasks,
remote_path: Annotated[str, Body(description="远端目录路径")],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
bucket: Annotated[str | None, Body(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> UploadFileResponse:
local_path, file_name = await StorageFileService(auth, db).download_dir(
source_id=source_id, remote_path=remote_path, bucket=bucket
)
background_tasks.add_task(_delete_temp_file, local_path)
return UploadFileResponse(file_path=local_path, filename=file_name)
@StorageBrowseRouter.delete("/delete", summary="删除存储源文件", response_model=ResponseSchema[None])
async def delete_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:delete"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
remote_path: Annotated[str, Body(description="远端文件路径")],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
bucket: Annotated[str | None, Body(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> JSONResponse:
await StorageFileService(auth, db).delete(source_id=source_id, remote_path=remote_path, bucket=bucket)
return SuccessResponse(msg="删除文件成功")
@StorageBrowseRouter.get("/list", summary="查询存储源文件列表", response_model=None)
async def list_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Query(description="存储源ID(不传使用默认存储源)")] = None,
prefix: Annotated[str | None, Query(description="目录前缀(可选)")] = None,
bucket: Annotated[str | None, Query(description="存储桶(对象存储多桶浏览用,可选)")] = None,
page_size: Annotated[int | None, Query(ge=1, le=500, description="每页数量(传了才走游标分页,不传返回全量)")] = None,
cursor: Annotated[str | None, Query(description="下一页游标(上一页返回的 next_cursor,可选)")] = None,
) -> JSONResponse:
result: StoragePage | list[StorageObject] = await StorageFileService(auth, db).list_files(
source_id=source_id, prefix=prefix or "", bucket=bucket, page_size=page_size, cursor=cursor
)
return SuccessResponse(data=result, msg="查询文件列表成功")
@StorageBrowseRouter.get("/buckets", summary="查询存储源桶列表", response_model=ResponseSchema[list[str]])
async def list_storage_buckets_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Query(description="存储源ID(不传使用默认存储源)")] = None,
) -> JSONResponse:
result: list[str] = await StorageFileService(auth, db).list_buckets(source_id=source_id)
return SuccessResponse(data=result, msg="查询桶列表成功")
@StorageBrowseRouter.post("/copy", summary="复制/移动文件", response_model=ResponseSchema[StoragePathResultSchema])
async def copy_or_move_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Body(description="源存储源ID(不传使用默认存储源)")] = None,
source_path: Annotated[str, Body(description="源文件路径")] = "",
target_id: Annotated[int, Body(description="目标存储源ID")] = 0,
target_path: Annotated[str, Body(description="目标路径")] = "",
move: Annotated[bool, Body(description="是否为移动(true 移动/重命名,false 复制)")] = False,
bucket: Annotated[str | None, Body(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> JSONResponse:
result: StoragePathResultSchema = await StorageFileService(auth, db).copy_or_move(
source_id=source_id,
source_path=source_path,
target_id=target_id,
target_path=target_path,
move=move,
bucket=bucket,
)
return SuccessResponse(data=result, msg="操作文件成功")
@StorageBrowseRouter.put("/rename", summary="重命名/移动文件", response_model=ResponseSchema[StoragePathResultSchema])
async def rename_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
source_path: Annotated[str, Body(description="原路径")] = "",
target_path: Annotated[str, Body(description="新路径")] = "",
bucket: Annotated[str | None, Body(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> JSONResponse:
result: StoragePathResultSchema = await StorageFileService(auth, db).rename(
source_id=source_id, src_path=source_path, dst_path=target_path, bucket=bucket
)
return SuccessResponse(data=result, msg="重命名成功")
@StorageBrowseRouter.post("/mkdir", summary="新建目录", response_model=ResponseSchema[StoragePathCreateSchema])
async def mkdir_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:update"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
remote_dir: Annotated[str, Body(description="目录路径")] = "",
bucket: Annotated[str | None, Body(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> JSONResponse:
result: StoragePathCreateSchema = await StorageFileService(auth, db).mkdir(source_id=source_id, remote_dir=remote_dir, bucket=bucket)
return SuccessResponse(data=result, msg="新建目录成功")
@StorageBrowseRouter.post("/share", summary="生成分享链接", response_model=ResponseSchema[str | None])
async def share_storage_file_controller(
auth: Annotated[AuthSchema, Security(AuthPermission(["module_task:storage:browse:query"]))],
db: Annotated[AsyncSession, Depends(db_getter)],
remote_path: Annotated[str, Body(description="远端文件路径")] = "",
source_id: Annotated[int | None, Body(description="存储源ID(不传使用默认存储源)")] = None,
expire: Annotated[int, Body(description="有效期(秒)", ge=60, le=604800)] = 3600,
bucket: Annotated[str | None, Body(description="存储桶(对象存储多桶浏览用,可选)")] = None,
) -> JSONResponse:
result: str | None = await StorageFileService(auth, db).share(
source_id=source_id, remote_path=remote_path, expire=expire, bucket=bucket
)
return SuccessResponse(data=result, msg="生成分享链接成功")