/* Copyright © 2023 Philipp Wolfer Scotty is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Scotty is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Scotty. If not, see . */ package config import ( "errors" "fmt" "os" "path" "path/filepath" "regexp" "strings" "github.com/pelletier/go-toml/v2" "github.com/spf13/cobra" "github.com/spf13/viper" "go.uploadedlobster.com/scotty/internal/i18n" "go.uploadedlobster.com/scotty/internal/version" ) const ( defaultDatabase = "scotty.sqlite3" defaultOAuthHost = "127.0.0.1:2369" fileMode = 0640 ) func DefaultConfigDir() string { configDir, err := os.UserConfigDir() cobra.CheckErr(err) return path.Join(configDir, version.AppName) } // initConfig reads in config file and ENV variables if set. func InitConfig(cfgFile string) error { configDir := DefaultConfigDir() if cfgFile != "" { // Use given config file viper.SetConfigFile(cfgFile) } else { viper.AddConfigPath(configDir) viper.SetConfigType("toml") viper.SetConfigName(version.AppName) viper.SetConfigPermissions(fileMode) } setDefaults() // Create global config if it does not exist if viper.ConfigFileUsed() == "" && cfgFile == "" { if err := os.MkdirAll(configDir, 0750); err == nil { // This call is expected to return an error if the file already exists viper.SafeWriteConfig() //nolint:errcheck } } // read in environment variables that match viper.AutomaticEnv() // If a config file is found, read it in. return viper.ReadInConfig() } // Write the configuration except for removedKeys func WriteConfig(removedKeys ...string) error { file := viper.ConfigFileUsed() if len(file) == 0 { return errors.New(i18n.Tr("no configuration file defined, cannot write config")) } configMap := viper.AllSettings() for _, key := range removedKeys { c := configMap var ok bool subKeys := strings.Split(key, ".") keyLen := len(subKeys) // Deep search the key in the config and delete the deepest key, if it exists for i, s := range subKeys { if i == keyLen-1 { // This is the final key, delete it from the map delete(c, s) } else { // Use the child for next iteration if it is a map c, ok = c[s].(map[string]any) if !ok { // Child is not a map, can't search deeper break } } } } content, err := toml.Marshal(configMap) if err != nil { return err } return os.WriteFile(file, content, fileMode) } func DatabasePath() string { path := viper.GetString("database") if filepath.IsAbs(path) { return path } return filepath.Join(getConfigDir(), path) } func ValidateKey(key string) error { found, err := regexp.MatchString("^[A-Za-z0-9_-]+$", key) if err != nil { return err } else if found { return nil } else { return fmt.Errorf(i18n.Tr("key must only consist of A-Za-z0-9_-")) } } func setDefaults() { viper.SetDefault("database", defaultDatabase) viper.SetDefault("oauth-host", defaultOAuthHost) // Always configure the dump backend as a default service viper.SetDefault("service.dump.backend", "dump") } func getConfigDir() string { return filepath.Dir(viper.ConfigFileUsed()) }