feat: MySQL SSL 支持 + 任务编辑回显 + InfluxDB 备份
MySQL:
- 新增 use_ssl 选项;MariaDB 11.x 默认强制 SSL 且默认 verify cert,
故启用 SSL 时同时加 --skip-ssl-verify-server-cert 以兼容自签名证书。
任务编辑回显:
- 后端 JobOut 新增 source_config_safe 字段,解密后敏感字段(password/token/
secret_key/access_key)脱敏为 "***"。
- 更新端点接收 source_config;若密码字段为 "***" 则保留原值不变,
支持"不改密码只改其它字段"的常见场景。
- 前端 JobForm 完整回显所有数据源字段,type='edit' 时去掉 disabled。
InfluxDB 备份(新增类型):
- 后端 core/backup/influxdb.py:
- v2.x:调用 influx CLI(influxdb2-client-2.7.5 二进制直接安装)执行 influx backup
生成目录后 tar.gz 流式打包上传。
- v1.x:通过 HTTP API 调用 /query?db=...&q=SELECT * FROM /.*/ 拉取全量数据
导出为 CSV + META.txt。
- Dockerfile:直接下载 influx CLI 二进制(绕开 apt 签名问题)。
- 前端 JobForm:根据 version 动态显示 v1/v2 表单字段。
- schemas/job.py:新增 InfluxDBSourceConfigV1 / V2。
- 类型字面量扩展:'mysql' | 'directory' | 'influxdb'。
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -22,7 +22,7 @@ const { Title } = Typography
|
||||
|
||||
interface FormValues {
|
||||
name: string
|
||||
type: 'mysql' | 'directory'
|
||||
type: 'mysql' | 'directory' | 'influxdb'
|
||||
storage_id: number
|
||||
enabled: boolean
|
||||
cron_expression?: string
|
||||
@@ -35,29 +35,64 @@ interface FormValues {
|
||||
mysql_user?: string
|
||||
mysql_password?: string
|
||||
mysql_database?: string
|
||||
mysql_use_ssl?: boolean
|
||||
mysql_extra_args?: string
|
||||
// Directory
|
||||
dir_path?: string
|
||||
dir_exclude_patterns?: string
|
||||
// InfluxDB
|
||||
influx_version?: '1' | '2'
|
||||
influx_host?: string
|
||||
influx_port?: number
|
||||
influx_database?: string
|
||||
influx_username?: string
|
||||
influx_password?: string
|
||||
influx_org?: string
|
||||
influx_bucket?: string
|
||||
influx_token?: string
|
||||
}
|
||||
|
||||
function toPayload(values: FormValues): JobCreate {
|
||||
const source_config =
|
||||
values.type === 'mysql'
|
||||
? {
|
||||
host: values.mysql_host!,
|
||||
port: values.mysql_port || 3306,
|
||||
user: values.mysql_user!,
|
||||
password: values.mysql_password || '',
|
||||
database: values.mysql_database!,
|
||||
extra_args: values.mysql_extra_args || '',
|
||||
}
|
||||
: {
|
||||
path: values.dir_path!,
|
||||
exclude_patterns: values.dir_exclude_patterns
|
||||
? values.dir_exclude_patterns.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
: [],
|
||||
}
|
||||
let source_config: any
|
||||
if (values.type === 'mysql') {
|
||||
source_config = {
|
||||
host: values.mysql_host!,
|
||||
port: values.mysql_port || 3306,
|
||||
user: values.mysql_user!,
|
||||
password: values.mysql_password || '',
|
||||
database: values.mysql_database!,
|
||||
use_ssl: !!values.mysql_use_ssl,
|
||||
extra_args: values.mysql_extra_args || '',
|
||||
}
|
||||
} else if (values.type === 'directory') {
|
||||
source_config = {
|
||||
path: values.dir_path!,
|
||||
exclude_patterns: values.dir_exclude_patterns
|
||||
? values.dir_exclude_patterns.split(',').map((s) => s.trim()).filter(Boolean)
|
||||
: [],
|
||||
}
|
||||
} else {
|
||||
// influxdb
|
||||
if (values.influx_version === '1') {
|
||||
source_config = {
|
||||
version: '1',
|
||||
host: values.influx_host!,
|
||||
port: values.influx_port || 8086,
|
||||
database: values.influx_database!,
|
||||
username: values.influx_username || '',
|
||||
password: values.influx_password || '',
|
||||
}
|
||||
} else {
|
||||
source_config = {
|
||||
version: '2',
|
||||
host: values.influx_host!,
|
||||
port: values.influx_port || 8086,
|
||||
org: values.influx_org!,
|
||||
bucket: values.influx_bucket!,
|
||||
token: values.influx_token!,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: values.name,
|
||||
@@ -89,7 +124,8 @@ export function JobForm({ mode }: { mode: 'create' | 'edit' }) {
|
||||
.get(Number(id))
|
||||
.then((j) => {
|
||||
setExisting(j)
|
||||
const flat: Partial<FormValues> = {
|
||||
const cfg = j.source_config_safe || {}
|
||||
let flat: Partial<FormValues> = {
|
||||
name: j.name,
|
||||
type: j.type,
|
||||
storage_id: j.storage.id,
|
||||
@@ -99,7 +135,38 @@ export function JobForm({ mode }: { mode: 'create' | 'edit' }) {
|
||||
retention_days: j.retention_days ?? undefined,
|
||||
description: j.description || '',
|
||||
}
|
||||
// 反解 source_config(需要解密后的,前端拿不到 - 用空 source_config 占位让用户重填)
|
||||
// 把 source_config_safe 映射到表单字段
|
||||
if (j.type === 'mysql') {
|
||||
flat = {
|
||||
...flat,
|
||||
mysql_host: cfg.host,
|
||||
mysql_port: cfg.port ?? 3306,
|
||||
mysql_user: cfg.user,
|
||||
mysql_password: cfg.password ?? '', // 可能是 "***"
|
||||
mysql_database: cfg.database,
|
||||
mysql_use_ssl: !!cfg.use_ssl,
|
||||
mysql_extra_args: cfg.extra_args ?? '',
|
||||
}
|
||||
} else if (j.type === 'directory') {
|
||||
flat = {
|
||||
...flat,
|
||||
dir_path: cfg.path,
|
||||
dir_exclude_patterns: (cfg.exclude_patterns || []).join(', '),
|
||||
}
|
||||
} else if (j.type === 'influxdb') {
|
||||
flat = {
|
||||
...flat,
|
||||
influx_version: cfg.version ?? '1',
|
||||
influx_host: cfg.host,
|
||||
influx_port: cfg.port ?? 8086,
|
||||
influx_database: cfg.database,
|
||||
influx_username: cfg.username,
|
||||
influx_password: cfg.password,
|
||||
influx_org: cfg.org,
|
||||
influx_bucket: cfg.bucket,
|
||||
influx_token: cfg.token,
|
||||
}
|
||||
}
|
||||
form.setFieldsValue(flat as FormValues)
|
||||
})
|
||||
}
|
||||
@@ -112,16 +179,9 @@ export function JobForm({ mode }: { mode: 'create' | 'edit' }) {
|
||||
await jobApi.create(toPayload(values))
|
||||
message.success('创建成功')
|
||||
} else {
|
||||
const payload: JobUpdate = {
|
||||
name: values.name,
|
||||
storage_id: values.storage_id,
|
||||
enabled: values.enabled,
|
||||
cron_expression: values.cron_expression?.trim() || null,
|
||||
retention_count: values.retention_count,
|
||||
retention_days: values.retention_days ?? null,
|
||||
description: values.description ?? null,
|
||||
}
|
||||
await jobApi.update(Number(id), payload)
|
||||
// 编辑模式:把所有字段一并提交(包括 source_config)
|
||||
// 密码字段为 "***" 时后端会自动保留原值
|
||||
await jobApi.update(Number(id), toPayload(values) as any)
|
||||
message.success('已更新')
|
||||
}
|
||||
navigate('/jobs')
|
||||
@@ -162,41 +222,41 @@ export function JobForm({ mode }: { mode: 'create' | 'edit' }) {
|
||||
options={[
|
||||
{ value: 'mysql', label: 'MySQL 数据库' },
|
||||
{ value: 'directory', label: '服务器目录' },
|
||||
{ value: 'influxdb', label: 'InfluxDB 数据库' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{mode === 'edit' && existing && (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 16 }}
|
||||
message="编辑模式下数据源配置(密码/目录)不可见,请通过重建任务来修改。"
|
||||
/>
|
||||
)}
|
||||
|
||||
{jobType === 'mysql' && (
|
||||
<>
|
||||
<Divider orientation="left">MySQL 数据源</Divider>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Form.Item name="mysql_host" label="主机" rules={[{ required: jobType === 'mysql' }]} style={{ flex: 3, marginRight: 8 }}>
|
||||
<Input placeholder="127.0.0.1" disabled={mode === 'edit'} />
|
||||
<Input placeholder="127.0.0.1" />
|
||||
</Form.Item>
|
||||
<Form.Item name="mysql_port" label="端口" style={{ flex: 1 }}>
|
||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} disabled={mode === 'edit'} />
|
||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
<Form.Item name="mysql_user" label="用户名" rules={[{ required: jobType === 'mysql' }]}>
|
||||
<Input disabled={mode === 'edit'} />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="mysql_password" label="密码" extra="留空表示无密码">
|
||||
<Input.Password placeholder="数据库密码" disabled={mode === 'edit'} />
|
||||
<Input.Password placeholder="数据库密码" />
|
||||
</Form.Item>
|
||||
<Form.Item name="mysql_database" label="数据库名" rules={[{ required: jobType === 'mysql' }]}>
|
||||
<Input disabled={mode === 'edit'} />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="mysql_use_ssl"
|
||||
label="启用 SSL/TLS"
|
||||
extra="默认关闭;若服务器使用自签名证书请保持关闭"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="mysql_extra_args" label="额外 mysqldump 参数" extra="如:--skip-lock-tables">
|
||||
<Input disabled={mode === 'edit'} />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
@@ -205,18 +265,66 @@ export function JobForm({ mode }: { mode: 'create' | 'edit' }) {
|
||||
<>
|
||||
<Divider orientation="left">目录数据源</Divider>
|
||||
<Form.Item name="dir_path" label="要备份的目录" rules={[{ required: jobType === 'directory' }]}>
|
||||
<Input placeholder="/var/log" disabled={mode === 'edit'} />
|
||||
<Input placeholder="/var/log" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="dir_exclude_patterns"
|
||||
label="排除规则(逗号分隔)"
|
||||
extra="glob 模式,按文件名匹配,如:.DS_Store,__pycache__,node_modules"
|
||||
>
|
||||
<Input disabled={mode === 'edit'} />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
{jobType === 'influxdb' && (
|
||||
<>
|
||||
<Divider orientation="left">InfluxDB 数据源</Divider>
|
||||
<Form.Item name="influx_version" label="版本" rules={[{ required: true }]}>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '1', label: 'InfluxDB v1.x' },
|
||||
{ value: '2', label: 'InfluxDB v2.x' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Form.Item name="influx_host" label="主机" rules={[{ required: jobType === 'influxdb' }]} style={{ flex: 3, marginRight: 8 }}>
|
||||
<Input placeholder="127.0.0.1" />
|
||||
</Form.Item>
|
||||
<Form.Item name="influx_port" label="端口" style={{ flex: 1 }}>
|
||||
<InputNumber min={1} max={65535} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Space.Compact>
|
||||
|
||||
{Form.useWatch('influx_version', form) === '1' ? (
|
||||
<>
|
||||
<Form.Item name="influx_database" label="数据库名" rules={[{ required: true }]}>
|
||||
<Input placeholder="telegraf" />
|
||||
</Form.Item>
|
||||
<Form.Item name="influx_username" label="用户名">
|
||||
<Input placeholder="留空表示无认证" />
|
||||
</Form.Item>
|
||||
<Form.Item name="influx_password" label="密码">
|
||||
<Input.Password placeholder="留空表示无密码" />
|
||||
</Form.Item>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Form.Item name="influx_org" label="Organization" rules={[{ required: true }]}>
|
||||
<Input placeholder="my-org" />
|
||||
</Form.Item>
|
||||
<Form.Item name="influx_bucket" label="Bucket" rules={[{ required: true }]}>
|
||||
<Input placeholder="my-bucket" />
|
||||
</Form.Item>
|
||||
<Form.Item name="influx_token" label="API Token" rules={[{ required: true }]}>
|
||||
<Input.Password placeholder="influx token ..." />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
<Divider orientation="left">存储与调度</Divider>
|
||||
|
||||
<Form.Item name="storage_id" label="存储目标" rules={[{ required: true, message: '请选择存储目标' }]}>
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface MySQLSourceConfig {
|
||||
user: string
|
||||
password?: string
|
||||
database: string
|
||||
use_ssl?: boolean
|
||||
extra_args?: string
|
||||
}
|
||||
|
||||
@@ -76,6 +77,26 @@ export interface DirectorySourceConfig {
|
||||
exclude_patterns?: string[]
|
||||
}
|
||||
|
||||
export interface InfluxDBSourceConfigV1 {
|
||||
version: '1'
|
||||
host: string
|
||||
port: number
|
||||
database: string
|
||||
username?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
export interface InfluxDBSourceConfigV2 {
|
||||
version: '2'
|
||||
host: string
|
||||
port: number
|
||||
org: string
|
||||
bucket: string
|
||||
token: string
|
||||
}
|
||||
|
||||
export type InfluxDBSourceConfig = InfluxDBSourceConfigV1 | InfluxDBSourceConfigV2
|
||||
|
||||
export interface RunSummary {
|
||||
id: number
|
||||
status: string
|
||||
@@ -90,7 +111,7 @@ export interface RunSummary {
|
||||
export interface JobOut {
|
||||
id: number
|
||||
name: string
|
||||
type: 'mysql' | 'directory'
|
||||
type: 'mysql' | 'directory' | 'influxdb'
|
||||
cron_expression: string | null
|
||||
enabled: boolean
|
||||
retention_count: number
|
||||
@@ -99,6 +120,7 @@ export interface JobOut {
|
||||
storage: StorageOut
|
||||
created_at: string
|
||||
updated_at: string
|
||||
source_config_safe?: Record<string, any>
|
||||
}
|
||||
|
||||
export interface JobWithLastRun extends JobOut {
|
||||
@@ -107,8 +129,8 @@ export interface JobWithLastRun extends JobOut {
|
||||
|
||||
export interface JobCreate {
|
||||
name: string
|
||||
type: 'mysql' | 'directory'
|
||||
source_config: MySQLSourceConfig | DirectorySourceConfig
|
||||
type: 'mysql' | 'directory' | 'influxdb'
|
||||
source_config: MySQLSourceConfig | DirectorySourceConfig | InfluxDBSourceConfig
|
||||
storage_id: number
|
||||
cron_expression?: string
|
||||
enabled?: boolean
|
||||
@@ -119,7 +141,7 @@ export interface JobCreate {
|
||||
|
||||
export interface JobUpdate {
|
||||
name?: string
|
||||
source_config?: MySQLSourceConfig | DirectorySourceConfig
|
||||
source_config?: MySQLSourceConfig | DirectorySourceConfig | InfluxDBSourceConfig
|
||||
storage_id?: number
|
||||
cron_expression?: string | null
|
||||
enabled?: boolean
|
||||
|
||||
Reference in New Issue
Block a user