Merge pull request #4593 from StarryKira/fix/image-storage-env-unreachable
fix: 异步生图开关配了却不生效(环境变量被静默丢弃 + 迁移到后台开关)
This commit is contained in:
@@ -54,6 +54,81 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Async image object storage -->
|
||||
<div class="card p-6">
|
||||
<div class="mb-4 flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('admin.backup.imageStorage.title') }}
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ t('admin.backup.imageStorage.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input v-model="imageStorageForm.enabled" type="checkbox" />
|
||||
<span>{{ t('admin.backup.imageStorage.enabled') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
<input v-model="imageStorageForm.reuse_backup_s3" type="checkbox" />
|
||||
<span>{{ t('admin.backup.imageStorage.reuseBackupS3') }}</span>
|
||||
</label>
|
||||
|
||||
<div class="mt-3 grid grid-cols-1 gap-3 md:grid-cols-2">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.imageStorage.bucket') }}</label>
|
||||
<input v-model="imageStorageForm.bucket" class="input w-full" :placeholder="imageStorageForm.reuse_backup_s3 ? t('admin.backup.imageStorage.bucketInherited') : ''" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.imageStorage.prefix') }}</label>
|
||||
<input v-model="imageStorageForm.prefix" class="input w-full" placeholder="images/" />
|
||||
</div>
|
||||
|
||||
<template v-if="!imageStorageForm.reuse_backup_s3">
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.s3.endpoint') }}</label>
|
||||
<input v-model="imageStorageForm.endpoint" class="input w-full" placeholder="https://<account_id>.r2.cloudflarestorage.com" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.s3.region') }}</label>
|
||||
<input v-model="imageStorageForm.region" class="input w-full" placeholder="auto" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.s3.accessKeyId') }}</label>
|
||||
<input v-model="imageStorageForm.access_key_id" class="input w-full" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.s3.secretAccessKey') }}</label>
|
||||
<input v-model="imageStorageForm.secret_access_key" type="password" class="input w-full" :placeholder="imageStorageSecretConfigured ? t('admin.backup.s3.secretConfigured') : ''" />
|
||||
</div>
|
||||
<label class="inline-flex items-center gap-2 text-sm text-gray-700 dark:text-gray-300 md:col-span-2">
|
||||
<input v-model="imageStorageForm.force_path_style" type="checkbox" />
|
||||
<span>{{ t('admin.backup.s3.forcePathStyle') }}</span>
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.imageStorage.publicBaseUrl') }}</label>
|
||||
<input v-model="imageStorageForm.public_base_url" class="input w-full" :placeholder="t('admin.backup.imageStorage.publicBaseUrlPlaceholder')" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400">{{ t('admin.backup.imageStorage.presignExpiryHours') }}</label>
|
||||
<input v-model.number="imageStorageForm.presign_expiry_hours" type="number" min="1" class="input w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-secondary btn-sm" :disabled="testingImageStorage" @click="testImageStorage">
|
||||
{{ testingImageStorage ? t('common.loading') : t('admin.backup.s3.testConnection') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" :disabled="savingImageStorage" @click="saveImageStorageConfig">
|
||||
{{ savingImageStorage ? t('common.loading') : t('common.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Schedule Config -->
|
||||
<div class="card p-6">
|
||||
<div class="mb-4">
|
||||
@@ -284,7 +359,12 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { adminAPI } from '@/api'
|
||||
import { useAppStore } from '@/stores'
|
||||
import type { BackupS3Config, BackupScheduleConfig, BackupRecord } from '@/api/admin/backup'
|
||||
import type {
|
||||
BackupS3Config,
|
||||
BackupScheduleConfig,
|
||||
BackupRecord,
|
||||
ImageStorageConfig,
|
||||
} from '@/api/admin/backup'
|
||||
import { useStepUp, isStepUpBlocked, isStepUpCancelled, stepUpBlockReason } from '@/composables/useStepUp'
|
||||
import TotpStepUpDialog from '@/components/auth/TotpStepUpDialog.vue'
|
||||
|
||||
@@ -317,6 +397,26 @@ const s3SecretConfigured = ref(false)
|
||||
const savingS3 = ref(false)
|
||||
const testingS3 = ref(false)
|
||||
|
||||
// Async image object storage. Shares the S3 client with backups, so the default is
|
||||
// to reuse the credentials configured above and only differ by prefix.
|
||||
const imageStorageForm = ref<ImageStorageConfig>({
|
||||
enabled: false,
|
||||
reuse_backup_s3: true,
|
||||
bucket: '',
|
||||
prefix: 'images/',
|
||||
public_base_url: '',
|
||||
presign_expiry_hours: 24,
|
||||
max_download_bytes: 33554432,
|
||||
endpoint: '',
|
||||
region: 'auto',
|
||||
access_key_id: '',
|
||||
secret_access_key: '',
|
||||
force_path_style: false,
|
||||
})
|
||||
const imageStorageSecretConfigured = ref(false)
|
||||
const savingImageStorage = ref(false)
|
||||
const testingImageStorage = ref(false)
|
||||
|
||||
// Schedule config
|
||||
const scheduleForm = ref<BackupScheduleConfig>({
|
||||
enabled: false,
|
||||
@@ -485,6 +585,54 @@ async function saveS3Config() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadImageStorageConfig() {
|
||||
try {
|
||||
const { config, secret_configured } = await adminAPI.backup.getImageStorageConfig()
|
||||
imageStorageForm.value = {
|
||||
...config,
|
||||
prefix: config.prefix || 'images/',
|
||||
region: config.region || 'auto',
|
||||
secret_access_key: '',
|
||||
}
|
||||
imageStorageSecretConfigured.value = secret_configured
|
||||
} catch (error) {
|
||||
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
||||
}
|
||||
}
|
||||
|
||||
async function saveImageStorageConfig() {
|
||||
savingImageStorage.value = true
|
||||
try {
|
||||
await backupStepUp.run(() => adminAPI.backup.updateImageStorageConfig(imageStorageForm.value))
|
||||
appStore.showSuccess(t('admin.backup.imageStorage.saved'))
|
||||
await loadImageStorageConfig()
|
||||
} catch (error) {
|
||||
if (isStepUpCancelled(error)) {
|
||||
savingImageStorage.value = false
|
||||
return
|
||||
}
|
||||
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
||||
} finally {
|
||||
savingImageStorage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testImageStorage() {
|
||||
testingImageStorage.value = true
|
||||
try {
|
||||
const result = await adminAPI.backup.testImageStorageConnection(imageStorageForm.value)
|
||||
if (result.ok) {
|
||||
appStore.showSuccess(result.message || t('admin.backup.s3.testSuccess'))
|
||||
} else {
|
||||
appStore.showError(result.message || t('admin.backup.s3.testFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
appStore.showError((error as { message?: string })?.message || t('errors.networkError'))
|
||||
} finally {
|
||||
testingImageStorage.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function testS3() {
|
||||
testingS3.value = true
|
||||
try {
|
||||
@@ -642,7 +790,7 @@ function formatDate(value?: string): string {
|
||||
|
||||
onMounted(async () => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
await Promise.all([loadS3Config(), loadSchedule(), loadBackups()])
|
||||
await Promise.all([loadS3Config(), loadImageStorageConfig(), loadSchedule(), loadBackups()])
|
||||
|
||||
// 如果有正在 running 的备份,恢复轮询
|
||||
const runningBackup = backups.value.find(r => r.status === 'running')
|
||||
|
||||
Reference in New Issue
Block a user