// Package settings owns this project's configuration; it adds no business identity model. package settings import ( "errors" "fmt" "git.yuntuo.hk/k2safe/go-omni/config" "git.yuntuo.hk/k2safe/go-omni/database" "git.yuntuo.hk/k2safe/go-omni/logx" "git.yuntuo.hk/k2safe/go-omni/storage" "net/url" "os" "strconv" "strings" "time" ) type Config struct { Server config.Config `json:"server"` Database database.Config `json:"database"` Storage StorageConfig `json:"storage"` } func Default() Config { server := config.Default() server.Address = "127.0.0.1:8080" server.EnableLogger = true server.SlowRequestThreshold = 500 * time.Millisecond server.Log.Service = "example.com/userService" server.Log.Format = "json" server.Log.File = "./var/log/server.log" server.Log.Rotation = logx.RotationConfig{Enabled: true, MaxSizeMB: 100, MaxBackups: 10, MaxAgeDays: 7, Compress: true} return Config{Server: server, Database: database.DefaultConfig(), Storage: StorageConfig{ Provider: "local", Root: "./var/files", PublicURL: "http://127.0.0.1:8080/storage/object", Secure: true, MaxBytes: 32 << 20, ContentTypes: []string{"text/plain", "image/png", "image/jpeg", "application/pdf"}, LinkTTL: 5 * time.Minute, RequestTimeout: 30 * time.Second, }} } // Load uses defaults -> strict YAML/JSON -> nonempty environment variables. // Pass an empty path to use only defaults and environment. func Load(path string) (Config, error) { cfg := Default() var err error if path != "" { cfg, err = config.DecodeFile(path, cfg) if err != nil { return Config{}, err } } cfg.Server, err = config.ApplyEnv(cfg.Server, "") if err != nil { return Config{}, err } // A stream explicitly selected by environment overrides a file profile. if output := strings.TrimSpace(os.Getenv("GO_OMNI_LOG_OUTPUT")); output != "" && strings.TrimSpace(os.Getenv("GO_OMNI_LOG_FILE")) == "" { cfg.Server.Log.File = "" cfg.Server.Log.Rotation = logx.RotationConfig{} } if err = cfg.Storage.applyEnv(); err != nil { return Config{}, err } cfg.Database, err = database.ApplyEnv(cfg.Database, "") if err != nil { return Config{}, err } if err = cfg.Validate(); err != nil { return Config{}, err } return cfg, nil } func (c Config) Validate() error { if err := c.Database.Validate(); err != nil { return err } if c.Database.Driver != "mysql" || c.Database.MaxOpenConns < 1 { return errors.New("database requires driver mysql and a positive maxOpenConns") } if err := c.Server.Validate(); err != nil { return err } if c.Server.Log.Format != "json" && c.Server.Log.Format != "text" { return errors.New("log.format must be json or text") } switch strings.ToLower(c.Server.Log.Level) { case "debug", "info", "warn", "warning", "error": default: return errors.New("invalid log.level") } return c.Storage.Validate() } type StorageConfig struct { Provider string `json:"provider"` Root string `json:"root"` PublicURL string `json:"publicUrl"` Endpoint string `json:"endpoint"` Region string `json:"region"` Bucket string `json:"bucket"` AccountID string `json:"accountId"` Secure bool `json:"secure"` MaxBytes int64 `json:"maxBytes"` ContentTypes []string `json:"contentTypes"` LinkTTL time.Duration `json:"linkTtl"` RequestTimeout time.Duration `json:"requestTimeout"` // Secrets are environment-only and cannot be populated by config files. FilesToken string `json:"-"` SigningKey string `json:"-"` AccessKey string `json:"-"` SecretKey string `json:"-"` SessionToken string `json:"-"` } func (s StorageConfig) Policy() storage.UploadPolicy { return storage.UploadPolicy{MaxBytes: s.MaxBytes, ContentTypes: s.ContentTypes} } func (s *StorageConfig) applyEnv() error { values := map[string]*string{ "GO_OMNI_STORAGE_ROOT": &s.Root, "GO_OMNI_STORAGE_PUBLIC_URL": &s.PublicURL, "GO_OMNI_STORAGE_ENDPOINT": &s.Endpoint, "GO_OMNI_STORAGE_REGION": &s.Region, "GO_OMNI_STORAGE_BUCKET": &s.Bucket, "GO_OMNI_STORAGE_ACCOUNT_ID": &s.AccountID, "GO_OMNI_STORAGE_SIGNING_KEY": &s.SigningKey, "GO_OMNI_FILES_TOKEN": &s.FilesToken, "GO_OMNI_STORAGE_ACCESS_KEY": &s.AccessKey, "GO_OMNI_STORAGE_SECRET_KEY": &s.SecretKey, "GO_OMNI_STORAGE_SESSION_TOKEN": &s.SessionToken, } for name, target := range values { if value := strings.TrimSpace(os.Getenv(name)); value != "" { *target = value } } for name, target := range map[string]*time.Duration{"GO_OMNI_STORAGE_LINK_TTL": &s.LinkTTL, "GO_OMNI_STORAGE_REQUEST_TIMEOUT": &s.RequestTimeout} { if value := strings.TrimSpace(os.Getenv(name)); value != "" { v, err := time.ParseDuration(value) if err != nil { return fmt.Errorf("%s must be a duration", name) } *target = v } } if value := os.Getenv("GO_OMNI_STORAGE_MAX_BYTES"); value != "" { v, err := strconv.ParseInt(value, 10, 64) if err != nil { return errors.New("GO_OMNI_STORAGE_MAX_BYTES must be an integer") } s.MaxBytes = v } if value := os.Getenv("GO_OMNI_STORAGE_SECURE"); value != "" { v, err := strconv.ParseBool(value) if err != nil { return errors.New("GO_OMNI_STORAGE_SECURE must be a boolean") } s.Secure = v } if value := strings.TrimSpace(os.Getenv("GO_OMNI_STORAGE_CONTENT_TYPES")); value != "" { s.ContentTypes = strings.Split(value, ",") for i := range s.ContentTypes { s.ContentTypes[i] = strings.TrimSpace(s.ContentTypes[i]) } } return nil } func (s StorageConfig) Validate() error { if s.Provider != "local" { return errors.New("storage.provider must match the adapter selected at generation") } if err := s.Policy().Validate(); err != nil { return err } if s.LinkTTL < time.Second || s.LinkTTL > time.Hour { return errors.New("storage.linkTtl must be 1s..1h") } if s.RequestTimeout <= 0 || s.RequestTimeout > 5*time.Minute { return errors.New("storage.requestTimeout must be >0 and <=5m") } if s.Root == "" { return errors.New("storage.root is required") } u, err := url.Parse(s.PublicURL) if err != nil || u.Host == "" || u.Hostname() == "" || (u.Scheme != "http" && u.Scheme != "https") || u.User != nil || u.RawQuery != "" || u.Fragment != "" || u.Path != "/storage/object" { return errors.New("storage.publicUrl must be an absolute HTTP(S) URL ending in /storage/object") } if len(s.SigningKey) < 32 { return errors.New("set GO_OMNI_STORAGE_SIGNING_KEY to a stable secret of at least 32 bytes") } return nil }