mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 13:12:24 +00:00
Merge branch 'master' into pre-tenant
# Conflicts: # uv.lock
This commit is contained in:
+29
-11
@@ -51,7 +51,7 @@ async def _append_env_example(plugin_path: anyio.Path) -> None:
|
||||
await f.write(new_content)
|
||||
|
||||
|
||||
async def install_zip_plugin(file: UploadFile | str) -> str:
|
||||
async def install_zip_plugin(file: UploadFile | str) -> str: # noqa: C901
|
||||
"""
|
||||
安装 ZIP 插件
|
||||
|
||||
@@ -71,9 +71,11 @@ async def install_zip_plugin(file: UploadFile | str) -> str:
|
||||
with zipfile.ZipFile(file_bytes) as zf:
|
||||
# 校验压缩包
|
||||
plugin_namelist = zf.namelist()
|
||||
plugin_dir_name = plugin_namelist[0].split('/')[0]
|
||||
if not plugin_namelist:
|
||||
raise errors.RequestError(msg='插件压缩包内容非法')
|
||||
plugin_dir_name = plugin_namelist[0].split('/', 1)[0].strip()
|
||||
if not plugin_dir_name:
|
||||
raise errors.RequestError(msg='插件压缩包内容非法')
|
||||
if (
|
||||
len(plugin_namelist) <= 3
|
||||
or f'{plugin_dir_name}/plugin.toml' not in plugin_namelist
|
||||
@@ -82,29 +84,45 @@ async def install_zip_plugin(file: UploadFile | str) -> str:
|
||||
raise errors.RequestError(msg='插件压缩包内缺少必要文件')
|
||||
|
||||
# 插件是否可安装
|
||||
plugin_name = re.match(
|
||||
plugin_name_match = re.match(
|
||||
r'^([a-zA-Z0-9_]+)',
|
||||
file.split(os.sep)[-1].split('.')[0].strip()
|
||||
if isinstance(file, str)
|
||||
else file.filename.split('.')[0].strip(),
|
||||
).group()
|
||||
)
|
||||
if not plugin_name_match:
|
||||
raise errors.RequestError(msg='插件压缩包文件名非法')
|
||||
plugin_name = plugin_name_match.group()
|
||||
full_plugin_path = anyio.Path(PLUGIN_DIR / plugin_name)
|
||||
if await full_plugin_path.exists():
|
||||
raise errors.ConflictError(msg='此插件已安装')
|
||||
await full_plugin_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 解压(安装)
|
||||
members = []
|
||||
prefix = f'{plugin_dir_name}/'
|
||||
for member in zf.infolist():
|
||||
if member.filename.startswith(plugin_dir_name):
|
||||
new_filename = member.filename.replace(plugin_dir_name, '')
|
||||
if new_filename:
|
||||
member.filename = new_filename
|
||||
members.append(member)
|
||||
if member.filename in {plugin_dir_name, prefix}:
|
||||
continue
|
||||
if not member.filename.startswith(prefix):
|
||||
continue
|
||||
|
||||
relative_filename = member.filename.removeprefix(prefix)
|
||||
if not relative_filename:
|
||||
if member.is_dir():
|
||||
continue
|
||||
raise errors.RequestError(msg='插件压缩包内容非法')
|
||||
|
||||
member.filename = relative_filename
|
||||
members.append(member)
|
||||
|
||||
if not members:
|
||||
raise errors.RequestError(msg='插件压缩包内容非法')
|
||||
|
||||
await full_plugin_path.mkdir(parents=True, exist_ok=True)
|
||||
await run_in_threadpool(zf.extractall, full_plugin_path, members)
|
||||
|
||||
await _append_env_example(full_plugin_path)
|
||||
await install_requirements_async(plugin_dir_name)
|
||||
await install_requirements_async(plugin_name)
|
||||
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'true')
|
||||
|
||||
return plugin_name
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
select 1;
|
||||
@@ -0,0 +1 @@
|
||||
select 1;
|
||||
@@ -0,0 +1 @@
|
||||
select 1;
|
||||
@@ -0,0 +1 @@
|
||||
select 1;
|
||||
+34
-18
@@ -1,10 +1,10 @@
|
||||
import warnings
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
from backend.common.enums import PluginLevelType
|
||||
from backend.core.path_conf import PLUGIN_DIR
|
||||
from backend.plugin.errors import PluginConfigError
|
||||
from backend.utils.pattern_validate import match_string
|
||||
|
||||
@@ -23,8 +23,8 @@ class PluginInfoSchema(BaseModel):
|
||||
version: str = Field(..., description='版本号')
|
||||
description: str = Field(..., min_length=1, max_length=500, description='描述')
|
||||
author: str = Field(..., min_length=1, max_length=50, description='作者')
|
||||
tags: list[str] = Field(default_factory=list, description='标签')
|
||||
database: list[str] = Field(default_factory=list, description='数据库支持')
|
||||
tags: list[str] = Field(..., min_length=1, description='标签')
|
||||
database: list[str] = Field(..., min_length=1, description='数据库支持')
|
||||
|
||||
@field_validator('version')
|
||||
@classmethod
|
||||
@@ -183,19 +183,35 @@ def validate_plugin_config(plugin_name: str, config: dict[str, Any]) -> PluginLe
|
||||
error_msg = '; '.join(error_details)
|
||||
raise PluginConfigError(f'插件 {plugin_name} 配置校验失败: {error_msg}') from e
|
||||
|
||||
# TODO 下个重大版本变更为必填
|
||||
plugin_info = config.get('plugin', {})
|
||||
if not plugin_info.get('tags'):
|
||||
warnings.warn(
|
||||
f"插件 '{plugin_name}' 未配置 'tags' 字段,该字段将在下个重大版本中必填,请及时联系插件作者同步更新",
|
||||
FutureWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
if not plugin_info.get('database'):
|
||||
warnings.warn(
|
||||
f"插件 '{plugin_name}' 未配置 'database' 字段,该字段将在下个重大版本中必填,请及时联系插件作者同步更新",
|
||||
FutureWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
plugin_dir = Path(PLUGIN_DIR) / plugin_name
|
||||
model_dir = plugin_dir / 'model'
|
||||
if model_dir.is_dir():
|
||||
sql_dir = plugin_dir / 'sql'
|
||||
supported_db_types = []
|
||||
missing_details = []
|
||||
|
||||
for db_type in ('mysql', 'postgresql'):
|
||||
db_sql_dir = sql_dir / db_type
|
||||
required_sql_files = (
|
||||
db_sql_dir / 'init.sql',
|
||||
db_sql_dir / 'destroy.sql',
|
||||
db_sql_dir / 'init_snowflake.sql',
|
||||
db_sql_dir / 'destroy_snowflake.sql',
|
||||
)
|
||||
missing_files = [
|
||||
str(sql_file.relative_to(plugin_dir)) for sql_file in required_sql_files if not sql_file.is_file()
|
||||
]
|
||||
|
||||
if not missing_files:
|
||||
supported_db_types.append(db_type)
|
||||
continue
|
||||
|
||||
missing_details.append(f'{db_type}: {", ".join(missing_files)}')
|
||||
|
||||
if not supported_db_types:
|
||||
raise PluginConfigError(
|
||||
f'插件 {plugin_name} 必须至少提供一种数据库的初始化和销毁 SQL 脚本,'
|
||||
f'当前缺失: {"; ".join(missing_details)}'
|
||||
)
|
||||
|
||||
return plugin_level
|
||||
|
||||
Reference in New Issue
Block a user