mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
- 修正 AI 会话表初始化及异步数据库 URI 处理 - 新增全局 API 与 WS 聊天限流,健康检查豁免 - 修复忘记密码接口防用户枚举与参数校验 - 修复 SSE 推送数据重复编码问题 - 修复 Cron 表达式 Quartz 问号兼容 - 修复存储模块权限标识前缀 - 新增过期日志清理与任务错误中文提示
175 lines
10 KiB
Python
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="生成分享链接成功")
|