package controller import ( "fmt" "io" "net/http" "os" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/songquanpeng/one-api/common" "github.com/songquanpeng/one-api/common/config" ) func DownloadServerConfig(c *gin.Context) { configs := collectServerConfigs() var lines []string lines = append(lines, "# One API Server Configuration") lines = append(lines, "# Generated at runtime - download from settings page") lines = append(lines, "") // Group by category for readability categories := []string{"database", "redis", "performance", "network", "ratelimit", "metrics", "other"} categoryNames := map[string]string{ "database": "# ===== Database =====\n", "redis": "\n# ===== Redis =====\n", "performance": "\n# ===== Performance =====\n", "network": "\n# ===== Network =====\n", "ratelimit": "\n# ===== Rate Limit =====\n", "metrics": "\n# ===== Metrics =====\n", "other": "\n# ===== Other =====\n", } for _, cat := range categories { lines = append(lines, categoryNames[cat]) for _, item := range configs { if item.Category != cat { continue } // Skip the virtual db_type key if item.Key == "db_type" { continue } // Get the actual value from env/runtime for the download actualValue := getActualConfigValue(item.Key) if item.Key == "SESSION_SECRET" || item.Key == "REDIS_PASSWORD" || strings.HasSuffix(item.Key, "TOKEN") || strings.HasSuffix(item.Key, "TOKEN") || strings.HasSuffix(item.Key, "SECRET") || strings.HasSuffix(item.Key, "Secret") || item.Key == "SQL_DSN" || item.Key == "LOG_SQL_DSN" || item.Key == "REDIS_CONN_STRING" || item.Key == "HTTPS_PROXY" { // Include masked values for secrets lines = append(lines, fmt.Sprintf("# %s=%s", item.Key, item.Value)) lines = append(lines, fmt.Sprintf("# %s (masked above, set manually if needed)", item.Key)) } else { lines = append(lines, fmt.Sprintf("%s=%s", item.Key, actualValue)) } } } content := strings.Join(lines, "\n") c.Header("Content-Disposition", "attachment; filename=server-config.env") c.Header("Content-Type", "text/plain; charset=utf-8") c.String(http.StatusOK, content) } func ImportServerConfig(c *gin.Context) { file, _, err := c.Request.FormFile("file") if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "请上传 .env 配置文件", }) return } defer file.Close() content, err := io.ReadAll(file) if err != nil { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "读取文件失败:" + err.Error(), }) return } // Validate basic .env format lines := strings.Split(string(content), "\n") validLines := 0 for _, line := range lines { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } if strings.Contains(line, "=") { validLines++ } } if validLines == 0 { c.JSON(http.StatusBadRequest, gin.H{ "success": false, "message": "无效的 .env 文件格式,未找到有效的配置项", }) return } // Determine the .env file path envPath := ".env" if p := os.Getenv("SERVER_CONFIG_PATH"); p != "" { envPath = p } err = os.WriteFile(envPath, content, 0644) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "success": false, "message": "写入配置文件失败:" + err.Error(), }) return } c.JSON(http.StatusOK, gin.H{ "success": true, "message": fmt.Sprintf("配置文件已保存到 %s,请重启服务以生效。", envPath), }) } func getActualConfigValue(key string) string { switch key { case "PORT": return strconv.Itoa(*common.Port) case "SESSION_SECRET": return config.SessionSecret case "SQL_DSN", "LOG_SQL_DSN", "REDIS_CONN_STRING", "REDIS_PASSWORD", "REDIS_MASTER_NAME", "NODE_TYPE", "POLLING_INTERVAL", "GIN_MODE", "FRONTEND_BASE_URL", "CHANNEL_TEST_FREQUENCY", "INITIAL_ROOT_TOKEN", "INITIAL_ROOT_ACCESS_TOKEN", "HTTPS_PROXY": return os.Getenv(key) case "SQLITE_PATH": if os.Getenv("SQLITE_PATH") != "" { return os.Getenv("SQLITE_PATH") } return common.SQLitePath case "SQLITE_BUSY_TIMEOUT": return strconv.Itoa(common.SQLiteBusyTimeout) case "SQL_MAX_IDLE_CONNS": if v := os.Getenv("SQL_MAX_IDLE_CONNS"); v != "" { return v } return "100" case "SQL_MAX_OPEN_CONNS": if v := os.Getenv("SQL_MAX_OPEN_CONNS"); v != "" { return v } return "1000" case "SQL_MAX_LIFETIME": if v := os.Getenv("SQL_MAX_LIFETIME"); v != "" { return v } return "60" case "SYNC_FREQUENCY": return strconv.Itoa(config.SyncFrequency) case "DEBUG": return strconv.FormatBool(config.DebugEnabled) case "DEBUG_SQL": return strconv.FormatBool(config.DebugSQLEnabled) case "MEMORY_CACHE_ENABLED": return strconv.FormatBool(config.MemoryCacheEnabled) case "BATCH_UPDATE_ENABLED": return strconv.FormatBool(config.BatchUpdateEnabled) case "BATCH_UPDATE_INTERVAL": return strconv.Itoa(config.BatchUpdateInterval) case "RELAY_TIMEOUT": return strconv.Itoa(config.RelayTimeout) case "RELAY_PROXY": return config.RelayProxy case "USER_CONTENT_REQUEST_PROXY": return config.UserContentRequestProxy case "USER_CONTENT_REQUEST_TIMEOUT": return strconv.Itoa(config.UserContentRequestTimeout) case "GEMINI_SAFETY_SETTING": return config.GeminiSafetySetting case "GEMINI_VERSION": return config.GeminiVersion case "THEME": return config.Theme case "GLOBAL_API_RATE_LIMIT": return strconv.Itoa(config.GlobalApiRateLimitNum) case "GLOBAL_WEB_RATE_LIMIT": return strconv.Itoa(config.GlobalWebRateLimitNum) case "ENABLE_METRIC": return strconv.FormatBool(config.EnableMetric) case "METRIC_QUEUE_SIZE": return strconv.Itoa(config.MetricQueueSize) case "METRIC_SUCCESS_RATE_THRESHOLD": return strconv.FormatFloat(config.MetricSuccessRateThreshold, 'f', 2, 64) case "ONLY_ONE_LOG_FILE": return strconv.FormatBool(config.OnlyOneLogFile) case "ENFORCE_INCLUDE_USAGE": return strconv.FormatBool(config.EnforceIncludeUsage) case "TEST_PROMPT": return config.TestPrompt } return "" }