fix(storage): allow staging local config when S3 is unreachable

When switching storage drivers in admin settings, save now validates
connectivity against the selected target backend instead of retesting
the still-active driver. The active driver remains unchanged until
migration completes, so unreachable MinIO no longer blocks saving local
storage settings.
This commit is contained in:
ryan
2026-06-18 13:28:56 +08:00
parent 2648f3f3b7
commit e766066a75
3 changed files with 91 additions and 18 deletions
@@ -149,13 +149,8 @@ export function StorageConfigTab() {
const saveMutation = useMutation({
mutationFn: async (value: StorageConfig) => {
const activeDriver = query.data?.config.driver || value.driver
const valueToSave = {
...value,
driver: activeDriver,
}
await services.adminSystemConfig.updateSystemConfig(storageConfigKey, {
value: JSON.stringify(valueToSave),
value: JSON.stringify(value),
})
},
onSuccess: () => {
+37 -12
View File
@@ -307,19 +307,12 @@ func validateAndMergeStorageConfig(ctx context.Context, value string, currentCon
// 合并被掩码屏蔽的敏感信息,获取完整的真实配置
targetCfg := storage.MergeMaskedSecrets(newCfg, currentCfg)
// 校验配置参数是否合法
if err := storage.ValidateConfig(targetCfg); err != nil {
return "", fmt.Errorf("验证存储配置参数失败: %w", err)
if err := validateMergedStorageConfig(ctx, currentCfg, newCfg, targetCfg); err != nil {
return "", err
}
// 进行连通性测试验证,如果测试失败则拒绝保存
testBackend, err := storage.NewBackend(ctx, targetCfg, targetCfg.Driver)
if err != nil {
return "", fmt.Errorf("初始化测试存储实例失败: %w", err)
}
if err := testBackend.Test(ctx); err != nil {
return "", fmt.Errorf("存储连通性测试失败: %w", err)
if newCfg.Driver != "" && newCfg.Driver != currentCfg.Driver {
// 切换存储类型时仅暂存目标后端参数,活动 driver 由迁移任务正式切换。
targetCfg.Driver = currentCfg.Driver
}
// 序列化为最终保存的真实明文配置,防止保存屏蔽的 ****** 字符
@@ -330,3 +323,35 @@ func validateAndMergeStorageConfig(ctx context.Context, value string, currentCon
return string(unmaskedVal), nil
}
func validateMergedStorageConfig(ctx context.Context, currentCfg, newCfg, targetCfg storage.Config) error {
if newCfg.Driver != "" && newCfg.Driver != currentCfg.Driver {
if err := validateDriverConfig(targetCfg, newCfg.Driver); err != nil {
return fmt.Errorf("验证目标存储配置参数失败: %w", err)
}
pendingCfg := targetCfg
pendingCfg.Driver = newCfg.Driver
return testStorageBackend(ctx, pendingCfg, newCfg.Driver)
}
if err := storage.ValidateConfig(targetCfg); err != nil {
return fmt.Errorf("验证存储配置参数失败: %w", err)
}
return testStorageBackend(ctx, targetCfg, targetCfg.Driver)
}
func validateDriverConfig(cfg storage.Config, driver storage.Driver) error {
cfg.Driver = driver
return storage.ValidateConfig(cfg)
}
func testStorageBackend(ctx context.Context, cfg storage.Config, driver storage.Driver) error {
testBackend, err := storage.NewBackend(ctx, cfg, driver)
if err != nil {
return fmt.Errorf("初始化测试存储实例失败: %w", err)
}
if err := testBackend.Test(ctx); err != nil {
return fmt.Errorf("存储连通性测试失败: %w", err)
}
return nil
}
@@ -440,4 +440,57 @@ func TestUpdateStorageConfigValidation(t *testing.T) {
t.Errorf("expected 400 Bad Request, got %d. Body: %s", w.Code, w.Body.String())
}
})
t.Run("stage local config while active s3 is unreachable", func(t *testing.T) {
activeCfg := storage.DefaultConfig()
activeCfg.Driver = storage.DriverS3
activeCfg.S3.Endpoint = "http://127.0.0.1:9999"
activeCfg.S3.Region = "us-east-1"
activeCfg.S3.Bucket = "wavelet"
activeCfg.S3.AccessKeyID = "test"
activeCfg.S3.SecretAccessKey = "test"
activeBytes, _ := json.Marshal(activeCfg)
seedCfg := model.SystemConfig{
Key: "storage_config",
Value: string(activeBytes),
Type: "system",
}
if err := dbConn.Where("key = ?", "storage_config").
Assign(map[string]any{"value": seedCfg.Value, "type": seedCfg.Type}).
FirstOrCreate(&seedCfg).Error; err != nil {
t.Fatalf("seed active storage config failed: %v", err)
}
tempDir := t.TempDir()
stagedCfg := activeCfg
stagedCfg.Driver = storage.DriverLocal
stagedCfg.Local.Root = tempDir
cfgBytes, _ := json.Marshal(stagedCfg)
payload := UpdateSystemConfigRequest{Value: string(cfgBytes)}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("PUT", "/api/v1/admin/system-configs/storage_config", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200 OK, got %d. Body: %s", w.Code, w.Body.String())
}
var dbCfg model.SystemConfig
if err := dbConn.Where("key = ?", "storage_config").First(&dbCfg).Error; err != nil {
t.Fatalf("load saved storage config failed: %v", err)
}
var savedCfg storage.Config
if err := json.Unmarshal([]byte(dbCfg.Value), &savedCfg); err != nil {
t.Fatalf("parse saved storage config failed: %v", err)
}
if savedCfg.Driver != storage.DriverS3 {
t.Fatalf("active driver = %q, want %q until migration completes", savedCfg.Driver, storage.DriverS3)
}
if savedCfg.Local.Root != tempDir {
t.Fatalf("staged local root = %q, want %q", savedCfg.Local.Root, tempDir)
}
})
}