mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-08 02:17:46 +08:00
feat(admin): expose s3 backup as module
This commit is contained in:
@@ -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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user