feat(admin): expose s3 backup as module

This commit is contained in:
fawney19
2026-05-27 00:30:37 +08:00
parent 42693c2c52
commit 614c999871
11 changed files with 193 additions and 36 deletions
@@ -0,0 +1,283 @@
import { computed, ref } from 'vue'
import { adminApi, type S3BackupScope } from '@/api/admin'
import { useToast } from '@/composables/useToast'
import { parseApiError } from '@/utils/errorParser'
import { log } from '@/utils/logger'
export type S3BackupScheduleUnit = 'hours' | 'days' | 'weeks' | 'months'
export interface S3BackupConfig {
enabled: boolean
scope: S3BackupScope
endpoint: string
region: string
bucket: string
prefix: string
accessKeyId: string
secretAccessKey: string
secretAccessKeyIsSet: boolean
pathStyle: boolean
compression: string
scheduleUnit: S3BackupScheduleUnit
scheduleInterval: number
scheduleMinute: number
scheduleHour: number
scheduleWeekday: number
scheduleMonthDay: number
retentionCount: number
}
type ConfigField = Exclude<keyof S3BackupConfig, 'secretAccessKeyIsSet'>
const CONFIG_KEY_BY_FIELD: Record<ConfigField, string> = {
enabled: 'backup_s3_enabled',
scope: 'backup_s3_scope',
endpoint: 'backup_s3_endpoint',
region: 'backup_s3_region',
bucket: 'backup_s3_bucket',
prefix: 'backup_s3_prefix',
accessKeyId: 'backup_s3_access_key_id',
secretAccessKey: 'backup_s3_secret_access_key',
pathStyle: 'backup_s3_path_style',
compression: 'backup_s3_compression',
scheduleUnit: 'backup_s3_schedule_unit',
scheduleInterval: 'backup_s3_schedule_interval',
scheduleMinute: 'backup_s3_schedule_minute',
scheduleHour: 'backup_s3_schedule_hour',
scheduleWeekday: 'backup_s3_schedule_weekday',
scheduleMonthDay: 'backup_s3_schedule_month_day',
retentionCount: 'backup_s3_retention_count',
}
const FIELD_BY_CONFIG_KEY = Object.fromEntries(
Object.entries(CONFIG_KEY_BY_FIELD).map(([field, key]) => [key, field])
) as Record<string, ConfigField>
const CONFIG_KEYS = Object.values(CONFIG_KEY_BY_FIELD)
const VALID_SCOPES: S3BackupScope[] = ['config', 'users', 'data']
const VALID_UNITS: S3BackupScheduleUnit[] = ['hours', 'days', 'weeks', 'months']
function defaultS3BackupConfig(): S3BackupConfig {
return {
enabled: false,
scope: 'data',
endpoint: '',
region: 'auto',
bucket: '',
prefix: 'aether/backups/',
accessKeyId: '',
secretAccessKey: '',
secretAccessKeyIsSet: false,
pathStyle: true,
compression: 'zstd',
scheduleUnit: 'days',
scheduleInterval: 1,
scheduleMinute: 0,
scheduleHour: 3,
scheduleWeekday: 1,
scheduleMonthDay: 1,
retentionCount: 7,
}
}
function stringValue(value: unknown, fallback = ''): string {
return typeof value === 'string' ? value : fallback
}
function numberValue(value: unknown, fallback: number): number {
if (typeof value === 'number' && Number.isFinite(value)) return value
if (typeof value === 'string' && value.trim() !== '') {
const parsed = Number(value)
if (Number.isFinite(parsed)) return parsed
}
return fallback
}
function booleanValue(value: unknown, fallback: boolean): boolean {
if (typeof value === 'boolean') return value
if (value === 'true') return true
if (value === 'false') return false
return fallback
}
function scopeValue(value: unknown, fallback: S3BackupScope): S3BackupScope {
return typeof value === 'string' && VALID_SCOPES.includes(value as S3BackupScope)
? value as S3BackupScope
: fallback
}
function scheduleUnitValue(
value: unknown,
fallback: S3BackupScheduleUnit,
): S3BackupScheduleUnit {
return typeof value === 'string' && VALID_UNITS.includes(value as S3BackupScheduleUnit)
? value as S3BackupScheduleUnit
: fallback
}
function cloneConfig(value: S3BackupConfig): S3BackupConfig {
return JSON.parse(JSON.stringify(value)) as S3BackupConfig
}
export function useS3BackupConfig() {
const { success, error } = useToast()
const config = ref<S3BackupConfig>(defaultS3BackupConfig())
const originalConfig = ref<S3BackupConfig | null>(null)
const loading = ref(false)
const saving = ref(false)
const running = ref(false)
const advancedOpen = ref(false)
const hasChanges = computed(() => {
if (!originalConfig.value) return false
return JSON.stringify({
...config.value,
secretAccessKey: config.value.secretAccessKey.trim(),
}) !== JSON.stringify({
...originalConfig.value,
secretAccessKey: '',
})
})
async function loadS3BackupConfig() {
loading.value = true
try {
const next = defaultS3BackupConfig()
await Promise.all(CONFIG_KEYS.map(async (key) => {
try {
const response = await adminApi.getSystemConfig(key)
const field = FIELD_BY_CONFIG_KEY[key]
if (field === 'secretAccessKey') {
next.secretAccessKey = ''
next.secretAccessKeyIsSet = !!response.is_set
return
}
if (response.value === null || response.value === undefined) return
if (field === 'enabled' || field === 'pathStyle') {
next[field] = booleanValue(response.value, next[field])
} else if (
field === 'scheduleInterval' ||
field === 'scheduleMinute' ||
field === 'scheduleHour' ||
field === 'scheduleWeekday' ||
field === 'scheduleMonthDay' ||
field === 'retentionCount'
) {
next[field] = numberValue(response.value, next[field])
} else if (field === 'scope') {
next.scope = scopeValue(response.value, next.scope)
} else if (field === 'scheduleUnit') {
next.scheduleUnit = scheduleUnitValue(response.value, next.scheduleUnit)
} else {
next[field] = stringValue(response.value, next[field] as string)
}
} catch {
// 单个配置缺失时使用默认值
}
}))
config.value = next
originalConfig.value = cloneConfig(next)
} catch (err) {
error('加载 S3 备份配置失败')
log.error('加载 S3 备份配置失败:', err)
} finally {
loading.value = false
}
}
async function saveS3BackupConfig() {
saving.value = true
try {
const entries: Array<{ key: string; value: unknown; description: string }> = [
{ key: 'backup_s3_enabled', value: config.value.enabled, description: 'S3 自动备份开关' },
{ key: 'backup_s3_scope', value: config.value.scope, description: 'S3 备份范围' },
{ key: 'backup_s3_endpoint', value: config.value.endpoint.trim() || null, description: 'S3 Endpoint' },
{ key: 'backup_s3_region', value: config.value.region.trim() || 'auto', description: 'S3 Region' },
{ key: 'backup_s3_bucket', value: config.value.bucket.trim() || null, description: 'S3 Bucket' },
{ key: 'backup_s3_prefix', value: config.value.prefix.trim() || 'aether/backups/', description: 'S3 备份前缀' },
{ key: 'backup_s3_access_key_id', value: config.value.accessKeyId.trim() || null, description: 'S3 Access Key ID' },
{ key: 'backup_s3_path_style', value: config.value.pathStyle, description: 'S3 Path Style' },
{ key: 'backup_s3_compression', value: config.value.compression || 'zstd', description: 'S3 备份压缩格式' },
{ key: 'backup_s3_schedule_unit', value: config.value.scheduleUnit, description: 'S3 备份周期单位' },
{ key: 'backup_s3_schedule_interval', value: Math.max(1, Math.floor(config.value.scheduleInterval || 1)), description: 'S3 备份周期间隔' },
{ key: 'backup_s3_schedule_minute', value: Math.min(59, Math.max(0, Math.floor(config.value.scheduleMinute || 0))), description: 'S3 备份分钟' },
{ key: 'backup_s3_schedule_hour', value: Math.min(23, Math.max(0, Math.floor(config.value.scheduleHour || 0))), description: 'S3 备份小时' },
{ key: 'backup_s3_schedule_weekday', value: Math.min(7, Math.max(1, Math.floor(config.value.scheduleWeekday || 1))), description: 'S3 备份星期' },
{ key: 'backup_s3_schedule_month_day', value: Math.min(31, Math.max(1, Math.floor(config.value.scheduleMonthDay || 1))), description: 'S3 备份月日' },
{ key: 'backup_s3_retention_count', value: Math.max(1, Math.floor(config.value.retentionCount || 1)), description: 'S3 备份保留份数' },
]
const secret = config.value.secretAccessKey.trim()
if (secret) {
entries.push({
key: 'backup_s3_secret_access_key',
value: secret,
description: 'S3 Secret Access Key',
})
}
for (const item of entries) {
await adminApi.updateSystemConfig(item.key, item.value, item.description)
}
if (secret) {
config.value.secretAccessKey = ''
config.value.secretAccessKeyIsSet = true
}
originalConfig.value = cloneConfig(config.value)
success('S3 备份配置已保存')
} catch (err) {
error(parseApiError(err, '保存 S3 备份配置失败'))
log.error('保存 S3 备份配置失败:', err)
await loadS3BackupConfig()
} finally {
saving.value = false
}
}
async function clearS3SecretAccessKey() {
saving.value = true
try {
await adminApi.updateSystemConfig(
'backup_s3_secret_access_key',
'',
'S3 Secret Access Key',
)
config.value.secretAccessKey = ''
config.value.secretAccessKeyIsSet = false
if (originalConfig.value) {
originalConfig.value.secretAccessKey = ''
originalConfig.value.secretAccessKeyIsSet = false
}
success('S3 访问密钥已清除')
} catch (err) {
error(parseApiError(err, '清除 S3 访问密钥失败'))
log.error('清除 S3 访问密钥失败:', err)
} finally {
saving.value = false
}
}
async function runS3BackupNow() {
running.value = true
try {
const response = await adminApi.runS3Backup()
success(response.message || 'S3 备份任务已提交')
} catch (err) {
error(parseApiError(err, '提交 S3 备份任务失败'))
log.error('提交 S3 备份任务失败:', err)
} finally {
running.value = false
}
}
return {
config,
loading,
saving,
running,
advancedOpen,
hasChanges,
loadS3BackupConfig,
saveS3BackupConfig,
clearS3SecretAccessKey,
runS3BackupNow,
}
}