mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
- 移除各路由文件顶部冗余注释 - 将 JobRouter/NodeRouter 重命名为 CornJobRouter/CornJobNodeRouter - 新增存储浏览、节点、传输、工作流路由注册 - 调整路由导入来源与任务表名 - 优化 main.py 启动方式及环境配置加载
158 lines
8.0 KiB
Python
158 lines
8.0 KiB
Python
import codecs
|
||
|
||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||
|
||
from app.core.base_schema import BaseQueryParam, BaseSchema, UserByQueryParam, UserBySchema
|
||
from app.modules.task.storage.core.base import DEFAULT_PORTS, StorageProtocol
|
||
|
||
|
||
class StorageNodeConfigSchema(BaseModel):
|
||
"""存储源连接配置模型(创建/测试共用)"""
|
||
|
||
protocol: StorageProtocol = Field(..., description="协议(ftp/ftps/sftp/s3/obs/oss/cos/local)")
|
||
host: str | None = Field(default=None, max_length=255, description="主机地址/根目录(对象存储可不填,用 endpoint)")
|
||
port: int | None = Field(default=None, ge=0, le=65535, description="端口(local 协议为0;不传则使用协议默认端口)")
|
||
username: str | None = Field(default=None, max_length=255, description="用户名/AccessKey")
|
||
password: str | None = Field(default=None, max_length=512, description="密码/SecretKey")
|
||
bucket: str | None = Field(default=None, max_length=255, description="桶名/根目录")
|
||
endpoint: str | None = Field(default=None, max_length=255, description="接入点(对象存储)")
|
||
region: str | None = Field(default=None, max_length=64, description="区域(对象存储)")
|
||
path_prefix: str | None = Field(default=None, max_length=255, description="统一路径前缀")
|
||
is_secure: bool = Field(default=False, description="是否启用TLS(FTPS)")
|
||
implicit_tls: bool = Field(default=False, description="FTPS是否隐式TLS(默认显式)")
|
||
scheme: str = Field(default="https", description="对象存储访问协议(http/https)")
|
||
encrypt_type: int = Field(default=1, description="FTPS加密类型(0=明文 1=显式TLS可用时 2=要求显式TLS 3=隐式TLS)")
|
||
connection_mode: int = Field(default=0, description="FTP/FTPS传输模式(0=默认 1=主动 2=被动)")
|
||
encoding: str = Field(default="UTF-8", description="FTP/FTPS/SFTP编码(utf-8/gbk 等)")
|
||
# 分片传输参数(对象存储分片上传,每个端点独立配置;FTP/SFTP/LOCAL 等协议忽略)
|
||
multipart_part_size: int = Field(default=50, ge=5, le=5000, description="分片大小(MB)")
|
||
multipart_concurrency: int = Field(default=6, ge=1, le=64, description="分片上传并发路数")
|
||
multipart_memory_budget: int = Field(default=512, ge=8, le=10240, description="分片上传内存预算(MB)")
|
||
# SDK 高级配置(按协议解析,见 core/base.py;留空使用默认值)
|
||
advanced_config: dict | None = Field(default=None, description="SDK高级配置(JSON)")
|
||
|
||
@field_validator("path_prefix")
|
||
@classmethod
|
||
def validate_path_prefix(cls, value: str | None) -> str | None:
|
||
if value:
|
||
value = value.strip()
|
||
if ".." in value or "\x00" in value:
|
||
raise ValueError("路径前缀包含非法字符")
|
||
return value
|
||
|
||
@field_validator("encoding")
|
||
@classmethod
|
||
def validate_encoding(cls, value: str | None) -> str | None:
|
||
if value:
|
||
try:
|
||
codecs.lookup(value)
|
||
except LookupError:
|
||
raise ValueError(f"未知的编码: {value}(如 utf-8/gbk 等)")
|
||
return value
|
||
|
||
@field_validator("advanced_config")
|
||
@classmethod
|
||
def validate_advanced_config(cls, value: dict | None, info) -> dict | None:
|
||
"""校验协议 SDK 高级配置取值,避免非法值直达 SDK 报错。"""
|
||
if not value:
|
||
return value
|
||
if info.data.get("protocol") == StorageProtocol.S3:
|
||
s3_enum = {
|
||
"signature_version": {"s3v4", "s3", "v2"},
|
||
"retries_mode": {"standard", "adaptive", "legacy"},
|
||
"addressing_style": {"auto", "path", "virtual"},
|
||
}
|
||
for key, allowed in s3_enum.items():
|
||
if key in value and value[key] not in allowed:
|
||
raise ValueError(f"S3 {key} 取值必须为 {'/'.join(sorted(allowed))}")
|
||
return value
|
||
|
||
@model_validator(mode="after")
|
||
def validate_protocol_fields(self):
|
||
"""按协议校验必填字段并填充默认端口。"""
|
||
if self.port is None:
|
||
self.port = DEFAULT_PORTS[self.protocol]
|
||
# 对象存储类协议必须配置桶/空间名
|
||
obj_store_protocols = (
|
||
StorageProtocol.S3,
|
||
StorageProtocol.OBS,
|
||
StorageProtocol.OSS,
|
||
StorageProtocol.COS,
|
||
)
|
||
if self.protocol in obj_store_protocols:
|
||
# 对象存储协议由接入点 URL 决定端口,统一置 0(避免无意义的端口输入)
|
||
self.port = 0
|
||
if self.protocol in obj_store_protocols and not self.bucket:
|
||
raise ValueError(f"{self.protocol.value} 协议必须配置 bucket")
|
||
# 非对象存储协议必须配置主机地址
|
||
if self.protocol not in obj_store_protocols and not self.host:
|
||
raise ValueError(f"{self.protocol.value} 协议必须配置 host")
|
||
# endpoint 必须配置的协议(接入点须为完整 URL,如 https://xxx)
|
||
if self.protocol in (StorageProtocol.S3, StorageProtocol.OBS, StorageProtocol.OSS) and not self.endpoint:
|
||
raise ValueError(f"{self.protocol.value} 协议必须配置 endpoint")
|
||
if self.protocol == StorageProtocol.COS and not self.region:
|
||
raise ValueError("cos 协议必须配置 region")
|
||
if self.protocol == StorageProtocol.COS:
|
||
# COS 默认走 https(适配器以 is_secure 决定 Scheme)
|
||
self.is_secure = True
|
||
appid = self.advanced_config.get("appid") if self.advanced_config else None
|
||
if appid and not str(appid).isdigit():
|
||
raise ValueError("COS appid 必须为纯数字")
|
||
if self.protocol == StorageProtocol.FTPS:
|
||
if self.encrypt_type < 1:
|
||
raise ValueError("FTPS 加密方式不能为 0(明文),明文请使用 FTP 协议")
|
||
# 显式 TLS(1/2) 默认端口 21,990 是隐式 TLS 端口
|
||
if self.encrypt_type < 3 and self.port == DEFAULT_PORTS[StorageProtocol.FTPS]:
|
||
self.port = 21
|
||
self.is_secure = True
|
||
return self
|
||
|
||
|
||
class StorageNodeCreateSchema(StorageNodeConfigSchema):
|
||
"""存储源创建模型"""
|
||
|
||
name: str = Field(..., min_length=1, max_length=64, description="存储源名称")
|
||
is_default: bool = Field(default=False, description="是否默认存储源")
|
||
status: int = Field(default=0, ge=0, le=1, description="状态(0:启用 1:停用)")
|
||
description: str | None = Field(default=None, max_length=255, description="备注")
|
||
|
||
@field_validator("name")
|
||
@classmethod
|
||
def validate_name(cls, value: str) -> str:
|
||
value = value.strip()
|
||
if not value:
|
||
raise ValueError("存储源名称不能为空")
|
||
return value
|
||
|
||
|
||
class StorageNodeUpdateSchema(StorageNodeCreateSchema):
|
||
"""存储源更新模型(password 为空表示不修改原密码)"""
|
||
|
||
|
||
class StorageNodeTestSchema(StorageNodeConfigSchema):
|
||
"""存储源连接测试模型(仅校验连接配置,不落库;密码留空且传 source_id 时回退已保存密码)"""
|
||
|
||
node_id: int | None = Field(default=None, ge=1, description="已保存的存储源ID(编辑态测试时使用)")
|
||
|
||
|
||
class StorageNodeOutSchema(StorageNodeCreateSchema, BaseSchema, UserBySchema):
|
||
"""存储源详情响应模型(密码永不明文返回)"""
|
||
|
||
model_config = ConfigDict(from_attributes=True)
|
||
|
||
password: None = Field(default=None, exclude=True, repr=False, description="密码(不返回)")
|
||
has_password: bool = Field(default=False, description="是否已配置密码")
|
||
|
||
@field_validator("password", mode="before")
|
||
@classmethod
|
||
def mask_password(cls, value) -> None:
|
||
return None
|
||
|
||
|
||
class StorageNodeQueryParam(BaseQueryParam, UserByQueryParam):
|
||
"""存储源管理查询参数"""
|
||
|
||
name: str | None = Field(None, description="存储源名称", json_schema_extra={"q": "like"})
|
||
protocol: StorageProtocol | None = Field(None, description="协议", json_schema_extra={"q": "eq"})
|
||
status: int | None = Field(None, ge=0, le=1, description="状态(0:启用 1:停用)", json_schema_extra={"q": "eq"})
|