package config import ( "crypto/sha256" "fmt" "log" "os" "path/filepath" "runtime" "gopkg.in/yaml.v3" ) // Config holds all application configuration. type Config struct { Database DatabaseConfig `yaml:"database"` Port string `yaml:"port"` Path string `yaml:"path"` Secret string `yaml:"secret"` } // DatabaseConfig holds database-specific configuration. type DatabaseConfig struct { Type string `yaml:"type"` // "sqlite" (default) or "mysql" DSN string `yaml:"dsn"` // MySQL connection string (required when type is "mysql") } const defaultPort = "8080" // mysqlExampleDSN is written into new config files as a reference. const mysqlExampleDSN = "user:password@tcp(127.0.0.1:3306)/blog_go?charset=utf8mb4&parseTime=True&loc=Local" // getConfigPath returns the OS-aware config directory and config file path. func getConfigPath() (dir, file string) { switch runtime.GOOS { case "windows": dir = filepath.Join(".", "win", "etc", "blog_go") case "darwin": dir = filepath.Join(".", "mac", "etc", "blog_go") default: dir = filepath.Join("/", "etc", "blog_go") } file = filepath.Join(dir, "config.yaml") return } // getDefaultStoragePath returns the OS-aware default storage path. func getDefaultStoragePath() string { switch runtime.GOOS { case "windows": return filepath.Join(".", "win", "srv", "blog_go") case "darwin": return filepath.Join(".", "mac", "srv", "blog_go") default: return filepath.Join("/", "srv", "blog_go") } } // generateSecret returns a random-ish hex string for the session secret. func generateSecret() string { hostname, _ := os.Hostname() input := fmt.Sprintf("%s-%d", hostname, os.Getpid()) hash := sha256.Sum256([]byte(input)) return fmt.Sprintf("%x", hash) } // LoadConfig reads the config file or creates one with defaults. func LoadConfig() *Config { configDir, configFile := getConfigPath() defaultPath := getDefaultStoragePath() // Check if config file exists; create with defaults if not. if _, err := os.Stat(configFile); os.IsNotExist(err) { log.Printf("Config file not found at %s, creating with defaults...", configFile) if err := os.MkdirAll(configDir, 0755); err != nil { log.Fatalf("Failed to create config directory %s: %v", configDir, err) } cfg := &Config{ Database: DatabaseConfig{ Type: "sqlite", DSN: mysqlExampleDSN, }, Port: defaultPort, Path: defaultPath, Secret: generateSecret(), } data, err := yaml.Marshal(cfg) if err != nil { log.Fatalf("Failed to marshal default config: %v", err) } if err := os.WriteFile(configFile, data, 0644); err != nil { log.Fatalf("Failed to write config file %s: %v", configFile, err) } log.Printf("Default config created at %s", configFile) return cfg } // Read existing config file. data, err := os.ReadFile(configFile) if err != nil { log.Printf("Warning: could not read config file %s: %v, using defaults", configFile, err) cfg := &Config{} return applyDefaults(cfg, defaultPath) } cfg := &Config{} if err := yaml.Unmarshal(data, cfg); err != nil { log.Printf("Warning: malformed config file %s: %v, using defaults", configFile, err) } return applyDefaults(cfg, defaultPath) } // applyDefaults fills zero-value fields with sensible defaults. func applyDefaults(cfg *Config, defaultPath string) *Config { if cfg.Port == "" { cfg.Port = defaultPort } if cfg.Database.Type == "" { cfg.Database.Type = "sqlite" } if cfg.Path == "" { cfg.Path = defaultPath } if cfg.Secret == "" { cfg.Secret = generateSecret() } return cfg }