package config import ( "fmt" "os" "path/filepath" "strings" "gopkg.in/yaml.v3" ) type Config struct { Version string `yaml:"-" json:"-"` DataDir string `yaml:"data_dir" env:"SYNCSERVER_DATA_DIR" default:"./data"` ConfigDir string `yaml:"config_dir" env:"SYNCSERVER_CONFIG_DIR" default:"./etc/syncserver"` Addr string `yaml:"addr" env:"SYNCSERVER_ADDR" default:":8080"` Auth AuthConfig `yaml:"auth"` Scheduler SchedulerConfig `yaml:"scheduler"` } type AuthConfig struct { JWTSecret string `yaml:"jwt_secret" env:"SYNCSERVER_JWT_SECRET"` JWTExpiryH int `yaml:"jwt_expiry_hours" env:"SYNCSERVER_JWT_EXPIRY_HOURS" default:"24"` AdminUser string `yaml:"-" env:"SYNCSERVER_ADMIN_USER"` AdminPass string `yaml:"-" env:"SYNCSERVER_ADMIN_PASSWORD"` } type SchedulerConfig struct { Timezone string `yaml:"timezone" env:"SYNCSERVER_SCHEDULER_TZ" default:"UTC"` } var globalCfg *Config func Load(configPath, dataDir, addr string) (*Config, error) { cfg := &Config{ DataDir: dataDir, ConfigDir: "./etc/syncserver", Addr: addr, Auth: AuthConfig{ JWTExpiryH: 24, }, Scheduler: SchedulerConfig{ Timezone: "UTC", }, } if configPath != "" { data, err := os.ReadFile(configPath) if err != nil && !os.IsNotExist(err) { return nil, fmt.Errorf("reading config: %w", err) } if err == nil { if err := yaml.Unmarshal(data, cfg); err != nil { return nil, fmt.Errorf("parsing config: %w", err) } } } prefix := "SYNCSERVER_" envs := []struct { key *string envName string required bool }{ {&cfg.Auth.JWTSecret, prefix + "JWT_SECRET", false}, {&cfg.Auth.AdminUser, prefix + "ADMIN_USER", false}, {&cfg.Auth.AdminPass, prefix + "ADMIN_PASSWORD", false}, {&cfg.DataDir, prefix + "DATA_DIR", false}, {&cfg.Addr, prefix + "ADDR", false}, {&cfg.Scheduler.Timezone, prefix + "SCHEDULER_TZ", false}, } for _, e := range envs { if v := os.Getenv(e.envName); v != "" { *e.key = v } } if cfg.Auth.JWTSecret == "" { b := make([]byte, 32) f, err := os.Open("/dev/urandom") if err == nil { defer f.Close() n, _ := f.Read(b) if n == 32 { cfg.Auth.JWTSecret = fmt.Sprintf("%x", b) } } if cfg.Auth.JWTSecret == "" { cfg.Auth.JWTSecret = "insecure-dev-secret-change-in-production" } } if dataDir := os.Getenv("SYNCSERVER_DATA_DIR"); dataDir != "" { cfg.DataDir = dataDir } if addr := os.Getenv("SYNCSERVER_ADDR"); addr != "" { cfg.Addr = addr } absDataDir, err := filepath.Abs(cfg.DataDir) if err != nil { return nil, err } cfg.DataDir = absDataDir globalCfg = cfg return cfg, nil } func Get() *Config { return globalCfg } func (c *Config) DBPath() string { return filepath.Join(c.DataDir, "app.db") } func (c *Config) SSHDir() string { return filepath.Join(c.DataDir, "ssh") } func (c *Config) LogsDir() string { return filepath.Join(c.DataDir, "logs") } func (c *Config) EnsureDirs() error { dirs := []string{c.DataDir, c.SSHDir(), c.LogsDir()} for _, d := range dirs { if err := os.MkdirAll(d, 0700); err != nil { return fmt.Errorf("creating dir %s: %w", d, err) } } return nil } func (c *Config) LogPath() string { return filepath.Join(c.LogsDir(), "app.log") } func NormalizeAddr(addr string) string { addr = strings.TrimSpace(addr) if !strings.Contains(addr, ":") { addr = ":" + addr } return addr }