3x-ui/config/config.go

425 lines
11 KiB
Go
Raw Normal View History

2025-09-20 07:35:50 +00:00
// Package config provides configuration management utilities for the 3x-ui panel,
// including version information, logging levels, database paths, and environment variable handling.
2023-02-09 19:18:06 +00:00
package config
import (
_ "embed"
"encoding/json"
2023-02-09 19:18:06 +00:00
"fmt"
"io"
2023-02-09 19:18:06 +00:00
"os"
"path/filepath"
"runtime"
"strconv"
2023-02-09 19:18:06 +00:00
"strings"
)
//go:embed version
var version string
//go:embed name
var name string
2025-09-20 07:35:50 +00:00
// LogLevel represents the logging level for the application.
2023-02-09 19:18:06 +00:00
type LogLevel string
2025-09-20 07:35:50 +00:00
// Logging level constants
2023-02-09 19:18:06 +00:00
const (
Debug LogLevel = "debug"
Info LogLevel = "info"
Notice LogLevel = "notice"
Warning LogLevel = "warning"
Error LogLevel = "error"
2023-02-09 19:18:06 +00:00
)
type NodeRole string
const (
NodeRoleMaster NodeRole = "master"
NodeRoleWorker NodeRole = "worker"
)
type NodeConfig struct {
Role NodeRole
NodeID string
SyncIntervalSeconds int
TrafficFlushSeconds int
}
2025-09-20 07:35:50 +00:00
// GetVersion returns the version string of the 3x-ui application.
2023-02-09 19:18:06 +00:00
func GetVersion() string {
return strings.TrimSpace(version)
}
2025-09-20 07:35:50 +00:00
// GetName returns the name of the 3x-ui application.
2023-02-09 19:18:06 +00:00
func GetName() string {
return strings.TrimSpace(name)
}
2025-09-20 07:35:50 +00:00
// GetLogLevel returns the current logging level based on environment variables or defaults to Info.
2023-02-09 19:18:06 +00:00
func GetLogLevel() LogLevel {
if IsDebug() {
return Debug
}
logLevel := os.Getenv("XUI_LOG_LEVEL")
if logLevel == "" {
return Info
}
return LogLevel(logLevel)
}
2025-09-20 07:35:50 +00:00
// IsDebug returns true if debug mode is enabled via the XUI_DEBUG environment variable.
2023-02-09 19:18:06 +00:00
func IsDebug() bool {
return os.Getenv("XUI_DEBUG") == "true"
}
2025-09-20 07:35:50 +00:00
// GetBinFolderPath returns the path to the binary folder, defaulting to "bin" if not set via XUI_BIN_FOLDER.
2023-04-13 19:33:46 +00:00
func GetBinFolderPath() string {
binFolderPath := os.Getenv("XUI_BIN_FOLDER")
if binFolderPath == "" {
binFolderPath = "bin"
}
return binFolderPath
}
func getBaseDir() string {
exePath, err := os.Executable()
if err != nil {
return "."
}
exeDir := filepath.Dir(exePath)
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
wd, err := os.Getwd()
if err != nil {
return "."
}
return wd
}
return exeDir
}
2025-09-20 07:35:50 +00:00
// GetDBFolderPath returns the path to the database folder based on environment variables or platform defaults.
2023-04-13 19:33:46 +00:00
func GetDBFolderPath() string {
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
if dbFolderPath != "" {
return dbFolderPath
}
if runtime.GOOS == "windows" {
return getBaseDir()
2023-04-13 19:33:46 +00:00
}
return "/etc/x-ui"
2023-04-13 19:33:46 +00:00
}
2025-09-20 07:35:50 +00:00
// GetDBPath returns the full path to the database file.
2023-02-09 19:18:06 +00:00
func GetDBPath() string {
2023-04-13 19:33:46 +00:00
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
2023-02-09 19:18:06 +00:00
}
// GetSettingPath returns the full path to the panel settings JSON file.
func GetSettingPath() string {
return fmt.Sprintf("%s/%s.json", GetDBFolderPath(), GetName())
}
func GetSharedCachePath() string {
return filepath.Join(GetDBFolderPath(), "shared-cache.json")
}
func GetTrafficPendingPath() string {
return filepath.Join(GetDBFolderPath(), "traffic-pending.json")
}
2025-09-20 07:35:50 +00:00
// GetLogFolder returns the path to the log folder based on environment variables or platform defaults.
func GetLogFolder() string {
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
if logFolderPath != "" {
return logFolderPath
}
if runtime.GOOS == "windows" {
2025-09-10 19:12:37 +00:00
return filepath.Join(".", "log")
}
return "/var/log/x-ui"
}
var settingGroupAliases = map[string][]string{
"dbType": {"databaseConnection", "other"},
"dbHost": {"databaseConnection", "other"},
"dbPort": {"databaseConnection", "other"},
"dbUser": {"databaseConnection", "other"},
"dbPassword": {"databaseConnection", "other"},
"dbName": {"databaseConnection", "other"},
"nodeRole": {"node", "other"},
"nodeId": {"node", "other"},
"syncInterval": {"node", "other"},
"trafficFlushInterval": {"node", "other"},
}
func readGroupedString(settings map[string]any, key string) string {
if groups, ok := settingGroupAliases[key]; ok {
for _, groupName := range groups {
if group, ok := settings[groupName].(map[string]any); ok {
if value, ok := group[key].(string); ok && value != "" {
return value
}
}
}
}
if value, ok := settings[key].(string); ok && value != "" {
return value
}
return ""
}
func readGroupedInt(settings map[string]any, key string, fallback int) int {
readInt := func(value any) (int, bool) {
switch v := value.(type) {
case float64:
return int(v), true
case int:
return v, true
case string:
i, err := strconv.Atoi(v)
if err == nil {
return i, true
}
}
return 0, false
}
if groups, ok := settingGroupAliases[key]; ok {
for _, groupName := range groups {
if group, ok := settings[groupName].(map[string]any); ok {
if value, ok := readInt(group[key]); ok {
return value
}
}
}
}
if value, ok := readInt(settings[key]); ok {
return value
}
return fallback
}
func settingsLayoutMeta() map[string]any {
return map[string]any{
"layout": "按模块-用途来归类",
"schema": "module-purpose-v1",
"description": "Top-level groups are organized by module and purpose for easier maintenance and development.",
}
}
func ensureDefaultNodeSettings(settings map[string]any) {
group, ok := settings["node"].(map[string]any)
if !ok {
group = make(map[string]any)
settings["node"] = group
}
defaults := map[string]string{
"nodeRole": string(NodeRoleMaster),
"nodeId": "",
"syncInterval": "30",
"trafficFlushInterval": "10",
}
for key, value := range defaults {
if existing, exists := group[key]; !exists || existing == nil {
// Also check "other" group for backward compatibility
if otherGroup, ok := settings["other"].(map[string]any); ok {
if val, ok := otherGroup[key].(string); ok && val != "" {
group[key] = val
continue
}
}
group[key] = value
}
}
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
if err != nil {
return err
}
return out.Sync()
}
func init() {
if runtime.GOOS != "windows" {
return
}
if os.Getenv("XUI_DB_FOLDER") != "" {
return
}
oldDBFolder := "/etc/x-ui"
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
newDBFolder := GetDBFolderPath()
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
_, err := os.Stat(newDBPath)
if err == nil {
return // new exists
}
_, err = os.Stat(oldDBPath)
if os.IsNotExist(err) {
return // old does not exist
}
_ = copyFile(oldDBPath, newDBPath) // ignore error
}
// GetDBTypeFromJSON reads the dbType setting directly from the JSON config file.
// This is needed before the database is initialized. Falls back to "sqlite".
func GetDBTypeFromJSON() string {
data, err := os.ReadFile(GetSettingPath())
if err != nil {
return "sqlite"
}
var settings map[string]any
if err := json.Unmarshal(data, &settings); err != nil {
return "sqlite"
}
if dbType := readGroupedString(settings, "dbType"); dbType != "" {
return dbType
}
return "sqlite"
}
// DBConfig holds MariaDB connection settings read from the JSON config file.
type DBConfig struct {
Type string
Host string
Port string
User string
Password string
Name string
}
// GetDBConfigFromJSON reads all MariaDB connection settings from the JSON config file.
func GetDBConfigFromJSON() DBConfig {
data, err := os.ReadFile(GetSettingPath())
if err != nil {
return DBConfig{Type: "sqlite", Host: "127.0.0.1", Port: "3306", Name: "3xui"}
}
var settings map[string]any
if err := json.Unmarshal(data, &settings); err != nil {
return DBConfig{Type: "sqlite", Host: "127.0.0.1", Port: "3306", Name: "3xui"}
}
dbType := "sqlite"
if t := readGroupedString(settings, "dbType"); t != "" {
dbType = t
}
return DBConfig{
Type: dbType,
Host: readGroupedString(settings, "dbHost"),
Port: readGroupedString(settings, "dbPort"),
User: readGroupedString(settings, "dbUser"),
Password: readGroupedString(settings, "dbPassword"),
Name: readGroupedString(settings, "dbName"),
}
}
func GetNodeConfigFromJSON() NodeConfig {
data, err := os.ReadFile(GetSettingPath())
if err != nil {
return NodeConfig{Role: NodeRoleMaster, SyncIntervalSeconds: 30, TrafficFlushSeconds: 10}
}
var settings map[string]any
if err := json.Unmarshal(data, &settings); err != nil {
return NodeConfig{Role: NodeRoleMaster, SyncIntervalSeconds: 30, TrafficFlushSeconds: 10}
}
role := readGroupedString(settings, "nodeRole")
if role == "" {
role = string(NodeRoleMaster)
}
return NodeConfig{
Role: NodeRole(role),
NodeID: readGroupedString(settings, "nodeId"),
SyncIntervalSeconds: readGroupedInt(settings, "syncInterval", 30),
TrafficFlushSeconds: readGroupedInt(settings, "trafficFlushInterval", 10),
}
}
func ValidateNodeConfig(nodeCfg NodeConfig, dbCfg DBConfig) error {
switch nodeCfg.Role {
case NodeRoleMaster, NodeRoleWorker:
default:
return fmt.Errorf("invalid nodeRole %q", nodeCfg.Role)
}
if nodeCfg.Role == NodeRoleWorker && nodeCfg.NodeID == "" {
return fmt.Errorf("worker mode requires nodeId")
}
if nodeCfg.Role == NodeRoleWorker && dbCfg.Type != "mariadb" {
return fmt.Errorf("worker mode requires mariadb")
}
if nodeCfg.SyncIntervalSeconds <= 0 {
return fmt.Errorf("syncInterval must be positive")
}
if nodeCfg.TrafficFlushSeconds <= 0 {
return fmt.Errorf("trafficFlushInterval must be positive")
}
return nil
}
// WriteSettingToJSON writes a single setting key to the JSON config file.
// It reads the existing file, updates the value, and writes back.
func WriteSettingToJSON(key, value string) error {
path := GetSettingPath()
var settings map[string]any
data, err := os.ReadFile(path)
if err == nil {
if err := json.Unmarshal(data, &settings); err != nil {
return err
}
} else if os.IsNotExist(err) {
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
settings = map[string]any{
"_meta": settingsLayoutMeta(),
}
} else {
return err
}
if _, exists := settings["_meta"]; !exists {
settings["_meta"] = settingsLayoutMeta()
}
ensureDefaultNodeSettings(settings)
// Check if the key lives in a nested group
if groups, ok := settingGroupAliases[key]; ok && len(groups) > 0 {
group := groups[0]
if _, exists := settings[group]; !exists {
settings[group] = make(map[string]any)
}
settings[group].(map[string]any)[key] = value
} else {
settings[key] = value
}
out, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, out, 0644)
}