mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-18 01:17:36 +08:00
feat: normalize adapter types and improve validation in adapters
This commit is contained in:
@@ -5,7 +5,7 @@ from typing import Annotated
|
||||
from models import StorageAdapter
|
||||
from schemas import AdapterCreate, AdapterOut
|
||||
from services.auth import get_current_active_user, User
|
||||
from services.adapters.registry import runtime_registry, get_config_schemas
|
||||
from services.adapters.registry import runtime_registry, get_config_schemas, normalize_adapter_type
|
||||
from api.response import success
|
||||
from services.logging import LogService
|
||||
|
||||
@@ -14,6 +14,9 @@ router = APIRouter(prefix="/api/adapters", tags=["adapters"])
|
||||
|
||||
def validate_and_normalize_config(adapter_type: str, cfg):
|
||||
schemas = get_config_schemas()
|
||||
adapter_type = normalize_adapter_type(adapter_type)
|
||||
if not adapter_type:
|
||||
raise HTTPException(400, detail="不支持的适配器类型")
|
||||
if not isinstance(cfg, dict):
|
||||
raise HTTPException(400, detail="config 必须是对象")
|
||||
schema = schemas.get(adapter_type)
|
||||
@@ -77,17 +80,10 @@ async def list_adapters(
|
||||
async def available_adapter_types(
|
||||
current_user: Annotated[User, Depends(get_current_active_user)]
|
||||
):
|
||||
name_map = {
|
||||
"local": "本地文件系统",
|
||||
"webdav": "WebDAV",
|
||||
"GoogleDrive": "Google Drive",
|
||||
"OneDrive": "OneDrive",
|
||||
}
|
||||
data = []
|
||||
for t, fields in get_config_schemas().items():
|
||||
data.append({
|
||||
"type": t,
|
||||
"name": name_map.get(t, t),
|
||||
"config_schema": fields,
|
||||
})
|
||||
return success(data)
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
import re
|
||||
from typing import Dict, Optional
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
|
||||
|
||||
class AdapterBase(BaseModel):
|
||||
name: str
|
||||
type: str = Field(pattern=r"^[a-zA-Z0-9_]+$")
|
||||
type: str = Field(pattern=r"^[a-z0-9_]+$")
|
||||
config: Dict = Field(default_factory=dict)
|
||||
enabled: bool = True
|
||||
path: str = None
|
||||
sub_path: Optional[str] = None
|
||||
|
||||
@field_validator("type", mode="before")
|
||||
@classmethod
|
||||
def _normalize_type(cls, v: str):
|
||||
if not isinstance(v, str):
|
||||
raise ValueError("type required")
|
||||
normalized = v.strip().lower()
|
||||
if not normalized:
|
||||
raise ValueError("type required")
|
||||
if not re.fullmatch(r"[a-z0-9_]+", normalized):
|
||||
raise ValueError("type must be lowercase alphanumeric or underscore")
|
||||
return normalized
|
||||
|
||||
|
||||
class AdapterCreate(AdapterBase):
|
||||
@staticmethod
|
||||
|
||||
@@ -543,7 +543,7 @@ class GoogleDriveAdapter:
|
||||
return None
|
||||
|
||||
|
||||
ADAPTER_TYPE = "GoogleDrive"
|
||||
ADAPTER_TYPE = "googledrive"
|
||||
|
||||
CONFIG_SCHEMA = [
|
||||
{"key": "client_id", "label": "Client ID", "type": "string", "required": True},
|
||||
|
||||
@@ -445,7 +445,7 @@ class OneDriveAdapter:
|
||||
return self._format_item(resp.json())
|
||||
|
||||
|
||||
ADAPTER_TYPE = "OneDrive"
|
||||
ADAPTER_TYPE = "onedrive"
|
||||
|
||||
CONFIG_SCHEMA = [
|
||||
{"key": "client_id", "label": "Client ID", "type": "string", "required": True},
|
||||
|
||||
@@ -718,7 +718,7 @@ class QuarkAdapter:
|
||||
return it["fid"]
|
||||
|
||||
|
||||
ADAPTER_TYPE = "Quark"
|
||||
ADAPTER_TYPE = "quark"
|
||||
|
||||
CONFIG_SCHEMA = [
|
||||
{"key": "cookie", "label": "Cookie", "type": "password", "required": True, "placeholder": "从 pan.quark.cn 复制"},
|
||||
|
||||
@@ -12,6 +12,13 @@ TYPE_MAP: Dict[str, AdapterFactory] = {}
|
||||
CONFIG_SCHEMAS: Dict[str, list] = {}
|
||||
|
||||
|
||||
def normalize_adapter_type(value: str | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
normalized = str(value).strip().lower()
|
||||
return normalized or None
|
||||
|
||||
|
||||
def discover_adapters():
|
||||
"""扫描 services.adapters 包, 自动注册适配器类型、工厂与配置 schema。"""
|
||||
from .. import adapters as adapters_pkg
|
||||
@@ -25,7 +32,7 @@ def discover_adapters():
|
||||
module = import_module(full_name)
|
||||
except Exception:
|
||||
continue
|
||||
adapter_type = getattr(module, "ADAPTER_TYPE", None)
|
||||
adapter_type = normalize_adapter_type(getattr(module, "ADAPTER_TYPE", None))
|
||||
schema = getattr(module, "CONFIG_SCHEMA", None)
|
||||
factory = getattr(module, "ADAPTER_FACTORY", None)
|
||||
|
||||
@@ -64,7 +71,16 @@ class RuntimeRegistry:
|
||||
self._instances.clear()
|
||||
adapters = await StorageAdapter.filter(enabled=True)
|
||||
for rec in adapters:
|
||||
factory = TYPE_MAP.get(rec.type)
|
||||
normalized_type = normalize_adapter_type(rec.type)
|
||||
if not normalized_type:
|
||||
continue
|
||||
if normalized_type != rec.type:
|
||||
rec.type = normalized_type
|
||||
try:
|
||||
await rec.save(update_fields=["type"])
|
||||
except Exception:
|
||||
continue
|
||||
factory = TYPE_MAP.get(normalized_type)
|
||||
if not factory:
|
||||
continue
|
||||
try:
|
||||
@@ -89,10 +105,21 @@ class RuntimeRegistry:
|
||||
self.remove(rec.id)
|
||||
return
|
||||
|
||||
factory = TYPE_MAP.get(rec.type)
|
||||
normalized_type = normalize_adapter_type(rec.type)
|
||||
if not normalized_type:
|
||||
self.remove(rec.id)
|
||||
return
|
||||
if normalized_type != rec.type:
|
||||
rec.type = normalized_type
|
||||
try:
|
||||
await rec.save(update_fields=["type"])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
factory = TYPE_MAP.get(normalized_type)
|
||||
if not factory:
|
||||
discover_adapters()
|
||||
factory = TYPE_MAP.get(rec.type)
|
||||
factory = TYPE_MAP.get(normalized_type)
|
||||
if not factory:
|
||||
return
|
||||
|
||||
|
||||
@@ -359,7 +359,7 @@ class S3Adapter:
|
||||
return StreamingResponse(iterator(), status_code=status, headers=headers, media_type=content_type)
|
||||
|
||||
|
||||
ADAPTER_TYPE = "S3"
|
||||
ADAPTER_TYPE = "s3"
|
||||
|
||||
CONFIG_SCHEMA = [
|
||||
{"key": "bucket_name", "label": "Bucket 名称",
|
||||
|
||||
@@ -8,7 +8,7 @@ from telethon.sessions import StringSession
|
||||
import socks
|
||||
|
||||
# 适配器类型标识
|
||||
ADAPTER_TYPE = "Telegram"
|
||||
ADAPTER_TYPE = "telegram"
|
||||
|
||||
# 适配器配置项定义
|
||||
CONFIG_SCHEMA = [
|
||||
|
||||
@@ -21,7 +21,6 @@ export interface AdapterTypeField {
|
||||
|
||||
export interface AdapterTypeMeta {
|
||||
type: string;
|
||||
name: string;
|
||||
config_schema: AdapterTypeField[];
|
||||
}
|
||||
|
||||
|
||||
@@ -525,6 +525,15 @@ export const en = {
|
||||
'Select adapter type': 'Select adapter type',
|
||||
'/ or /drive': '/ or /drive',
|
||||
'Adapter Config': 'Adapter Config',
|
||||
'adapter.type.local': 'Local Filesystem',
|
||||
'adapter.type.webdav': 'WebDAV',
|
||||
'adapter.type.googledrive': 'Google Drive',
|
||||
'adapter.type.onedrive': 'OneDrive',
|
||||
'adapter.type.s3': 'Amazon S3',
|
||||
'adapter.type.ftp': 'FTP',
|
||||
'adapter.type.sftp': 'SFTP',
|
||||
'adapter.type.telegram': 'Telegram',
|
||||
'adapter.type.quark': 'Quark Drive',
|
||||
|
||||
// Tasks
|
||||
'Automation Tasks': 'Automation Tasks',
|
||||
|
||||
@@ -528,6 +528,15 @@ export const zh = {
|
||||
'Select adapter type': '选择适配器类型',
|
||||
'/ or /drive': '/或/drive',
|
||||
'Adapter Config': '适配器配置',
|
||||
'adapter.type.local': '本地文件系统',
|
||||
'adapter.type.webdav': 'WebDAV',
|
||||
'adapter.type.googledrive': 'Google Drive',
|
||||
'adapter.type.onedrive': 'OneDrive',
|
||||
'adapter.type.s3': 'Amazon S3',
|
||||
'adapter.type.ftp': 'FTP',
|
||||
'adapter.type.sftp': 'SFTP',
|
||||
'adapter.type.telegram': 'Telegram',
|
||||
'adapter.type.quark': '夸克网盘',
|
||||
|
||||
// Tasks
|
||||
'Automation Tasks': '自动化任务',
|
||||
|
||||
@@ -130,9 +130,16 @@ const AdaptersPage = memo(function AdaptersPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const renderTypeLabel = useCallback((type?: string) => {
|
||||
if (!type) return '-';
|
||||
const key = `adapter.type.${type}`;
|
||||
const label = t(key);
|
||||
return label === key ? type : label;
|
||||
}, [t]);
|
||||
|
||||
const columns = [
|
||||
{ title: t('Name'), dataIndex: 'name' },
|
||||
{ title: t('Type'), dataIndex: 'type', width: 100 },
|
||||
{ title: t('Type'), dataIndex: 'type', width: 140, render: (value: string) => renderTypeLabel(value) },
|
||||
{ title: t('Mount Path'), dataIndex: 'path', width: 140, render: (v: string) => v || '-' },
|
||||
{ title: t('Sub Path'), dataIndex: 'sub_path', width: 140, render: (v: string) => v || '-' },
|
||||
{
|
||||
@@ -233,9 +240,9 @@ const AdaptersPage = memo(function AdaptersPage() {
|
||||
<Form.Item name="type" label={t('Type')} rules={[{ required: true }]}>
|
||||
<Select
|
||||
placeholder={t('Select adapter type')}
|
||||
options={availableTypes.map(t => ({ value: t.type, label: `${t.name} (${t.type})` }))}
|
||||
onChange={() => {
|
||||
const t = availableTypes.find(v => v.type === form.getFieldValue('type'));
|
||||
options={availableTypes.map(t => ({ value: t.type, label: renderTypeLabel(t.type) }))}
|
||||
onChange={(value) => {
|
||||
const t = availableTypes.find(v => v.type === value);
|
||||
const cfgDefaults: Record<string, any> = {};
|
||||
t?.config_schema.forEach(f => {
|
||||
if (f.default !== undefined) cfgDefaults[f.key] = f.default;
|
||||
|
||||
Reference in New Issue
Block a user