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:
publ
2026-06-22 19:14:45 +08:00
parent febdeab35c
commit 2568c0380b
9 changed files with 779 additions and 61 deletions
+154 -46
View File
@@ -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: '请选择存储目标' }]}>
+26 -4
View File
@@ -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