Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 24 additions & 16 deletions web/src/components/storage-targets/StorageTargetFormDrawer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Alert, Button, Divider, Drawer, Input, Select, Space, Switch, Typography } from '@arco-design/web-react'
import { useEffect, useMemo, useState } from 'react'
import { getStorageTargetFieldConfigs, getStorageTargetTypeLabel, isBuiltinType, builtinTypeOptions } from './field-config'
import { getStorageTargetFieldConfigs, getStorageTargetTypeLabel, isBuiltinType, buildAllTypeOptions } from './field-config'
import type { StorageConnectionTestResult, StorageTargetDetail, StorageTargetPayload, StorageTargetType } from '../../types/storage-targets'
import { listRcloneBackends, type RcloneBackendInfo } from '../../services/rclone'

Expand Down Expand Up @@ -56,17 +56,18 @@ export function StorageTargetFormDrawer({
setTestResult(null)
}, [initialValue, visible])

// 合并类型选项:内置 + 全部 rclone 后端
const allTypeOptions = useMemo(() => {
const builtinValues = new Set(builtinTypeOptions.map((o) => o.value))
const rcloneOptions = rcloneBackends
.filter((b) => !builtinValues.has(b.name) && b.name !== 'local' && b.name !== 'rclone')
.map((b) => ({ label: `${b.name.toUpperCase()} — ${b.description}`, value: b.name }))
return [
...builtinTypeOptions.map((o) => ({ ...o, label: o.label, value: o.value as string })),
...rcloneOptions,
]
}, [rcloneBackends])
// 构建分类的类型选项(去重、中文标注)
const allTypeOptions = useMemo(() => buildAllTypeOptions(rcloneBackends), [rcloneBackends])

// 按分组聚合,用于 Select 的 OptGroup 渲染
const groupedOptions = useMemo(() => {
const groups: Record<string, { label: string; value: string }[]> = {}
for (const opt of allTypeOptions) {
if (!groups[opt.group]) groups[opt.group] = []
groups[opt.group].push({ label: opt.label, value: opt.value })
}
return groups
}, [allTypeOptions])

// 当前类型是否为非内置(rclone 动态后端)
const isDynamicType = !isBuiltinType(draft.type)
Expand Down Expand Up @@ -179,17 +180,24 @@ export function StorageTargetFormDrawer({
<Select
showSearch
value={draft.type || undefined}
placeholder="搜索存储类型(如 SFTP、Azure Blob、Dropbox...)"
options={allTypeOptions}
placeholder="搜索存储类型..."
filterOption={(input, option) => {
const label = (option?.props?.children ?? option?.props?.label ?? '') as string
const label = String(option?.props?.children ?? option?.props?.label ?? '')
return label.toLowerCase().includes(input.toLowerCase())
}}
onChange={(value) => {
setDraft((c) => ({ ...c, type: value as string, config: {} }))
setTestResult(null)
}}
/>
>
{Object.entries(groupedOptions).map(([group, options]) => (
<Select.OptGroup key={group} label={group}>
{options.map((opt) => (
<Select.Option key={opt.value} value={opt.value}>{opt.label}</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</div>

<div>
Expand Down
122 changes: 104 additions & 18 deletions web/src/components/storage-targets/field-config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { StorageTargetFieldConfig, StorageTargetType } from '../../types/storage-targets'

// 内置类型的静态字段配置(定制化配置结构)
// ---------------------------------------------------------------------------
// 内置类型的静态字段配置
// ---------------------------------------------------------------------------

const BUILTIN_FIELD_CONFIG: Record<string, StorageTargetFieldConfig[]> = {
local_disk: [
{ key: 'basePath', label: '基础目录', type: 'input', required: true, placeholder: '/data/backups', description: 'BackupX 将在该目录下创建和管理备份文件。' },
Expand Down Expand Up @@ -55,34 +58,117 @@ const BUILTIN_FIELD_CONFIG: Record<string, StorageTargetFieldConfig[]> = {

const BUILTIN_TYPES = new Set(Object.keys(BUILTIN_FIELD_CONFIG))

/** 是否为内置类型 */
export function isBuiltinType(type: StorageTargetType): boolean {
return BUILTIN_TYPES.has(type)
}

/** 获取静态字段配置 */
export function getStorageTargetFieldConfigs(type: StorageTargetType): StorageTargetFieldConfig[] {
return BUILTIN_FIELD_CONFIG[type] ?? []
}

const BUILTIN_LABELS: Record<string, string> = {
// ---------------------------------------------------------------------------
// 存储类型完整列表(分类、中文标注、去重)
// ---------------------------------------------------------------------------

export interface TypeOption {
label: string
value: string
group: string
}

// rclone 后端中不适合做存储目标的(工具类/代理类/只读类)
const EXCLUDED_BACKENDS = new Set([
'alias', 'cache', 'http', 'archive', 'memory', 'tardigrade', // tardigrade = storj 别名
'union', 'crypt', 'chunker', 'compress', 'hasher', 'combine',
'local', // 用内置 local_disk 替代
'drive', // 用内置 google_drive 替代(避免和 rclone 的 drive 重复)
])

// 内置类型(带中文标签的定制化类型,优先展示)
const BUILTIN_OPTIONS: TypeOption[] = [
{ label: '本地磁盘', value: 'local_disk', group: '常用' },
{ label: 'S3 兼容存储(AWS / MinIO / 阿里云 / 腾讯云等)', value: 's3', group: '常用' },
{ label: '阿里云 OSS', value: 'aliyun_oss', group: '常用' },
{ label: '腾讯云 COS', value: 'tencent_cos', group: '常用' },
{ label: '七牛云 Kodo', value: 'qiniu_kodo', group: '常用' },
{ label: 'Google Drive', value: 'google_drive', group: '常用' },
{ label: 'WebDAV(Nextcloud / 坚果云等)', value: 'webdav', group: '常用' },
{ label: 'FTP / FTPS', value: 'ftp', group: '常用' },
]

// rclone 后端的中文标注(仅标注常见的,其余用原始描述)
const RCLONE_LABELS: Record<string, { label: string; group: string }> = {
sftp: { label: 'SFTP(SSH 文件传输)', group: '文件传输' },
smb: { label: 'SMB / CIFS(Windows 共享)', group: '文件传输' },
azureblob: { label: 'Azure Blob 存储', group: '云存储' },
azurefiles: { label: 'Azure Files 存储', group: '云存储' },
'google cloud storage': { label: 'Google Cloud Storage(GCS)', group: '云存储' },
b2: { label: 'Backblaze B2', group: '云存储' },
swift: { label: 'OpenStack Swift', group: '云存储' },
dropbox: { label: 'Dropbox', group: '网盘' },
onedrive: { label: 'Microsoft OneDrive', group: '网盘' },
box: { label: 'Box', group: '网盘' },
pcloud: { label: 'pCloud', group: '网盘' },
mega: { label: 'MEGA', group: '网盘' },
'google photos': { label: 'Google Photos', group: '网盘' },
yandex: { label: 'Yandex Disk', group: '网盘' },
pikpak: { label: 'PikPak', group: '网盘' },
iclouddrive: { label: 'iCloud Drive', group: '网盘' },
jottacloud: { label: 'Jottacloud', group: '网盘' },
hidrive: { label: 'HiDrive', group: '网盘' },
protondrive: { label: 'Proton Drive', group: '网盘' },
mailru: { label: 'Mail.ru Cloud', group: '网盘' },
sugarsync: { label: 'SugarSync', group: '网盘' },
putio: { label: 'Put.io', group: '网盘' },
zoho: { label: 'Zoho WorkDrive', group: '网盘' },
internxt: { label: 'Internxt Drive', group: '网盘' },
seafile: { label: 'Seafile', group: '自建存储' },
storj: { label: 'Storj 去中心化存储', group: '云存储' },
hdfs: { label: 'Hadoop HDFS', group: '企业存储' },
oracleobjectstorage: { label: 'Oracle 对象存储', group: '云存储' },
qingstor: { label: '青云 QingStor', group: '云存储' },
sharefile: { label: 'Citrix ShareFile', group: '企业存储' },
filefabric: { label: 'Enterprise File Fabric', group: '企业存储' },
netstorage: { label: 'Akamai NetStorage', group: '企业存储' },
sia: { label: 'Sia 去中心化存储', group: '云存储' },
koofr: { label: 'Koofr / Digi Storage', group: '网盘' },
opendrive: { label: 'OpenDrive', group: '网盘' },
}

/** 构建完整类型选项列表(内置 + rclone,去重+分类) */
export function buildAllTypeOptions(rcloneBackends: { name: string; description: string }[]): TypeOption[] {
const result = [...BUILTIN_OPTIONS]
const existingValues = new Set(BUILTIN_OPTIONS.map((o) => o.value))

for (const backend of rcloneBackends) {
if (EXCLUDED_BACKENDS.has(backend.name) || existingValues.has(backend.name)) continue
// 也排除和内置类型实际是同一后端的(如 rclone 的 s3, ftp, webdav 已被内置覆盖)
existingValues.add(backend.name)

const meta = RCLONE_LABELS[backend.name]
result.push({
label: meta?.label ?? `${backend.name} — ${backend.description}`,
value: backend.name,
group: meta?.group ?? '其他',
})
}

return result
}

// ---------------------------------------------------------------------------
// 类型标签
// ---------------------------------------------------------------------------

const TYPE_LABELS: Record<string, string> = {
local_disk: '本地磁盘', google_drive: 'Google Drive', s3: 'S3 Compatible',
webdav: 'WebDAV', aliyun_oss: '阿里云 OSS', tencent_cos: '腾讯云 COS',
qiniu_kodo: '七牛云 Kodo', ftp: 'FTP', rclone: 'Rclone',
qiniu_kodo: '七牛云 Kodo', ftp: 'FTP',
sftp: 'SFTP', smb: 'SMB', azureblob: 'Azure Blob', dropbox: 'Dropbox',
onedrive: 'OneDrive', b2: 'Backblaze B2', mega: 'MEGA', pcloud: 'pCloud',
box: 'Box', swift: 'Swift', pikpak: 'PikPak',
}

export function getStorageTargetTypeLabel(type: StorageTargetType): string {
return BUILTIN_LABELS[type] || type.toUpperCase()
return TYPE_LABELS[type] || type.toUpperCase()
}

/** 内置类型选项(下拉框"常用"分组) */
export const builtinTypeOptions = [
{ label: '本地磁盘', value: 'local_disk' },
{ label: '阿里云 OSS', value: 'aliyun_oss' },
{ label: '腾讯云 COS', value: 'tencent_cos' },
{ label: '七牛云 Kodo', value: 'qiniu_kodo' },
{ label: 'S3 Compatible', value: 's3' },
{ label: 'Google Drive', value: 'google_drive' },
{ label: 'WebDAV', value: 'webdav' },
{ label: 'FTP', value: 'ftp' },
]
Loading