Ctrl K
ComunidadGuías…

model/frontend_option_migration.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package model

import (
	"errors"
	"fmt"
	"strings"

	"github.com/QuantumNous/new-api/common"
	"github.com/QuantumNous/new-api/setting/console_setting"
	"gorm.io/gorm"
)

const retiredThemeOptionKey = "theme.frontend"

type legacyOptionTransform func(string) (string, error)

// MigrateRetiredFrontendOptions normalizes options that belonged to the
// removed dashboard frontend. Each legacy console setting is migrated in its
// own transaction so one malformed value cannot block the other settings.
func MigrateRetiredFrontendOptions() error {
	if DB == nil {
		return errors.New("database is not initialized")
	}

	var migrationErrors []error
	if err := normalizeRetiredThemeOption(); err != nil {
		migrationErrors = append(migrationErrors, fmt.Errorf("normalize %s: %w", retiredThemeOptionKey, err))
	}

	migrations := []struct {
		source    string
		target    string
		transform legacyOptionTransform
	}{
		{source: "ApiInfo", target: "console_setting.api_info", transform: transformLegacyAPIInfo},
		{source: "Announcements", target: "console_setting.announcements", transform: transformLegacyAnnouncements},
		{source: "FAQ", target: "console_setting.faq", transform: transformLegacyFAQ},
	}
	for _, migration := range migrations {
		if err := migrateLegacyOption(migration.source, migration.target, migration.transform); err != nil {
			migrationErrors = append(migrationErrors, err)
		}
	}
	if err := migrateLegacyUptimeOptions(); err != nil {
		migrationErrors = append(migrationErrors, err)
	}
	return errors.Join(migrationErrors...)
}

func normalizeRetiredThemeOption() error {
	return DB.Transaction(func(tx *gorm.DB) error {
		var option Option
		err := tx.Where(&Option{Key: retiredThemeOptionKey}).First(&option).Error
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return tx.Create(&Option{Key: retiredThemeOptionKey, Value: "default"}).Error
		}
		if err != nil {
			return err
		}
		if option.Value == "default" {
			return nil
		}
		return tx.Model(&option).Update("value", "default").Error
	})
}

func migrateLegacyOption(sourceKey, targetKey string, transform legacyOptionTransform) error {
	return DB.Transaction(func(tx *gorm.DB) error {
		var source Option
		if err := tx.Where(&Option{Key: sourceKey}).First(&source).Error; err != nil {
			if errors.Is(err, gorm.ErrRecordNotFound) {
				return nil
			}
			return fmt.Errorf("read legacy option %s: %w", sourceKey, err)
		}

		var target Option
		err := tx.Where(&Option{Key: targetKey}).First(&target).Error
		if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
			return fmt.Errorf("read target option %s: %w", targetKey, err)
		}
		if err == nil {
			return tx.Delete(&source).Error
		}

		value, transformErr := transform(source.Value)
		if transformErr != nil {
			common.SysError(common.LogText("legacy option %s was not migrated: %v", sourceKey, transformErr))
			return nil
		}
		if errors.Is(err, gorm.ErrRecordNotFound) {
			target = Option{Key: targetKey}
		}
		target.Value = value
		if err := tx.Save(&target).Error; err != nil {
			return fmt.Errorf("write target option %s: %w", targetKey, err)
		}
		if err := tx.Delete(&source).Error; err != nil {
			return fmt.Errorf("delete legacy option %s: %w", sourceKey, err)
		}
		return nil
	})
}

func transformLegacyAPIInfo(value string) (string, error) {
	if strings.TrimSpace(value) == "" {
		return "", errors.New("value is empty")
	}
	var items []map[string]any
	if err := common.UnmarshalJsonStr(value, &items); err != nil {
		return "", err
	}
	if len(items) > 50 {
		items = items[:50]
	}
	encoded, err := common.Marshal(items)
	if err != nil {
		return "", err
	}
	result := string(encoded)
	if err := console_setting.ValidateConsoleSettings(result, "ApiInfo"); err != nil {
		return "", err
	}
	return result, nil
}

func transformLegacyAnnouncements(value string) (string, error) {
	if strings.TrimSpace(value) == "" {
		return "", errors.New("value is empty")
	}
	if err := console_setting.ValidateConsoleSettings(value, "Announcements"); err != nil {
		return "", err
	}
	return value, nil
}

func transformLegacyFAQ(value string) (string, error) {
	if strings.TrimSpace(value) == "" {
		return "", errors.New("value is empty")
	}
	var legacyItems []map[string]any
	if err := common.UnmarshalJsonStr(value, &legacyItems); err != nil {
		return "", err
	}
	items := make([]map[string]any, 0, len(legacyItems))
	for index, item := range legacyItems {
		question, _ := item["question"].(string)
		if strings.TrimSpace(question) == "" {
			question, _ = item["title"].(string)
		}
		answer, _ := item["answer"].(string)
		if strings.TrimSpace(answer) == "" {
			answer, _ = item["content"].(string)
		}
		if strings.TrimSpace(question) == "" || strings.TrimSpace(answer) == "" {
			return "", fmt.Errorf("FAQ entry %d is missing a question or answer", index)
		}
		items = append(items, map[string]any{"question": question, "answer": answer})
	}
	if len(items) > 50 {
		items = items[:50]
	}
	encoded, err := common.Marshal(items)
	if err != nil {
		return "", err
	}
	result := string(encoded)
	if err := console_setting.ValidateConsoleSettings(result, "FAQ"); err != nil {
		return "", err
	}
	return result, nil
}

func migrateLegacyUptimeOptions() error {
	return DB.Transaction(func(tx *gorm.DB) error {
		var urlOption Option
		urlErr := tx.Where(&Option{Key: "UptimeKumaUrl"}).First(&urlOption).Error
		if urlErr != nil && !errors.Is(urlErr, gorm.ErrRecordNotFound) {
			return fmt.Errorf("read legacy option UptimeKumaUrl: %w", urlErr)
		}
		var slugOption Option
		slugErr := tx.Where(&Option{Key: "UptimeKumaSlug"}).First(&slugOption).Error
		if slugErr != nil && !errors.Is(slugErr, gorm.ErrRecordNotFound) {
			return fmt.Errorf("read legacy option UptimeKumaSlug: %w", slugErr)
		}
		if errors.Is(urlErr, gorm.ErrRecordNotFound) && errors.Is(slugErr, gorm.ErrRecordNotFound) {
			return nil
		}

		var target Option
		targetErr := tx.Where(&Option{Key: "console_setting.uptime_kuma_groups"}).First(&target).Error
		if targetErr != nil && !errors.Is(targetErr, gorm.ErrRecordNotFound) {
			return fmt.Errorf("read target option console_setting.uptime_kuma_groups: %w", targetErr)
		}
		if targetErr == nil {
			if urlErr == nil {
				if err := tx.Delete(&urlOption).Error; err != nil {
					return err
				}
			}
			if slugErr == nil {
				return tx.Delete(&slugOption).Error
			}
			return nil
		}

		if urlErr != nil || slugErr != nil || strings.TrimSpace(urlOption.Value) == "" || strings.TrimSpace(slugOption.Value) == "" {
			common.SysError(common.LogText("legacy Uptime Kuma options were not migrated: both URL and slug are required"))
			return nil
		}
		groups := []map[string]any{{
			"id":           1,
			"categoryName": "old",
			"url":          urlOption.Value,
			"slug":         slugOption.Value,
			"description":  "",
		}}
		encoded, err := common.Marshal(groups)
		if err != nil {
			return err
		}
		value := string(encoded)
		if err := console_setting.ValidateConsoleSettings(value, "UptimeKumaGroups"); err != nil {
			common.SysError(common.LogText("legacy Uptime Kuma options were not migrated: %v", err))
			return nil
		}
		if errors.Is(targetErr, gorm.ErrRecordNotFound) {
			target = Option{Key: "console_setting.uptime_kuma_groups"}
		}
		target.Value = value
		if err := tx.Save(&target).Error; err != nil {
			return fmt.Errorf("write target option console_setting.uptime_kuma_groups: %w", err)
		}
		if err := tx.Delete(&urlOption).Error; err != nil {
			return err
		}
		return tx.Delete(&slugOption).Error
	})
}