On range tout pour recommencer en rust
This commit is contained in:
@@ -1,410 +0,0 @@
|
||||
package pmoconfig
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gargoton.petite-maison-orange.fr/eric/pmomusic/fileutils"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
//go:embed pmomusic.yaml
|
||||
var defaultConfig []byte
|
||||
|
||||
type Config struct {
|
||||
path string
|
||||
mutex sync.Mutex
|
||||
config map[string]interface{}
|
||||
}
|
||||
|
||||
var _CONFIG *Config
|
||||
|
||||
const envConfigFile = "PMOMUSIC_CONFIG"
|
||||
const envPrefix = "PMOMUSIC_CONFIG__"
|
||||
|
||||
// LoadConfig loads a configuration file from the given path or a default
|
||||
// location.
|
||||
//
|
||||
// It prioritizes paths in this order:
|
||||
// - the provided path,
|
||||
// - the file specified by the environment variable PMOMUSIC_CONFIG
|
||||
// - the .pmomusic.yml file in the current directory
|
||||
// - the .pmomusic.yml file in the user's home directory, and . If no path is found
|
||||
//
|
||||
// or it fails to read any of these files, it falls back on a default
|
||||
// configuration.
|
||||
//
|
||||
// Parameters:
|
||||
// - path string: The path to the configuration file. If this
|
||||
// parameter is empty, the function will look for the configuration in the
|
||||
// current user's home directory and an environment variable.
|
||||
//
|
||||
// Returns: 1) Config: A struct containing the loaded configuration data.
|
||||
//
|
||||
// Side Effects:
|
||||
//
|
||||
// - This function reads from disk, logs informational messages, and may also
|
||||
// panic if there are issues unmarshalling the YAML config file.
|
||||
//
|
||||
// Errors:
|
||||
//
|
||||
// - The function will log a warning message and continue with a default
|
||||
// configuration if it fails to read or unmarshal any of the files. It does not
|
||||
// return an error in this case, as returning errors from within deferred
|
||||
// functions can cause unexpected behavior.
|
||||
//
|
||||
// - If there's an issue reading or unmarshalling the YAML file, the function
|
||||
// will panic as this is a fatal condition that should be addressed immediately.
|
||||
//
|
||||
// Edge Cases:
|
||||
//
|
||||
// - This function does not handle race conditions where the
|
||||
// configuration file could change between when it checks if the path is empty
|
||||
// and when it attempts to read from it. If the file changes in that time, an
|
||||
// error will occur.
|
||||
//
|
||||
// - This function assumes the YAML config files are formatted correctly. If
|
||||
// they're not, unmarshalling them into a struct may result in unexpected
|
||||
// behavior or errors.
|
||||
//
|
||||
// Usage: ``` go cfg := LoadConfig("/path/to/config") fmt.Println(cfg) ````
|
||||
func LoadConfig(filename string) *Config {
|
||||
var data []byte
|
||||
var err error
|
||||
var cfg = &Config{}
|
||||
|
||||
cfg.mutex.Lock()
|
||||
defer cfg.mutex.Unlock()
|
||||
|
||||
path := filename
|
||||
|
||||
if path != "" {
|
||||
log.Infof("✅ Trying to load config %s", path)
|
||||
data, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warnf("❌ cannot read config file %s", path)
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = os.Getenv(envConfigFile)
|
||||
if path != "" {
|
||||
log.Infof("✅ Trying to load config specified in env var %s", envConfigFile)
|
||||
data, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warnf("❌ cannot read config file %s specified in env var %s", path, envConfigFile)
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = ".pmomusic.yml"
|
||||
dir, err := os.Getwd()
|
||||
if err != nil {
|
||||
dir = "."
|
||||
}
|
||||
log.Infof("✅ Trying to load config file %s/.pmomusic.yml", dir)
|
||||
data, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warnf("❌ I cannot read config file %s/.pmomusic.yml", dir)
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
path = getHomeYmlPath()
|
||||
log.Infof("✅ Trying to load config file from user's home %s", path)
|
||||
data, err = os.ReadFile(path)
|
||||
if err != nil {
|
||||
log.Warnf("❌ I cannot read config file %s", path)
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
log.Infof("✅ Using default embeded config")
|
||||
data = defaultConfig
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(data, &cfg.config); err != nil {
|
||||
log.Panicf("invalid YAML config: %v", err)
|
||||
}
|
||||
|
||||
cfg.config = lowerKeysMap(cfg.config)
|
||||
|
||||
applyEnvOverrides(cfg)
|
||||
|
||||
if path == "" {
|
||||
switch {
|
||||
case filename != "" && fileutils.IsWriteable(filename):
|
||||
path = filename
|
||||
case os.Getenv(envConfigFile) != "" && fileutils.IsWriteable(os.Getenv(envConfigFile)):
|
||||
path = os.Getenv(envConfigFile)
|
||||
case fileutils.IsWriteable(".pmomusic.yml"):
|
||||
path = ".pmomusic.yml"
|
||||
case fileutils.IsWriteable(getHomeYmlPath()):
|
||||
path = getHomeYmlPath()
|
||||
}
|
||||
} else {
|
||||
if !fileutils.IsWriteable(path) {
|
||||
path = ""
|
||||
}
|
||||
}
|
||||
|
||||
if path == "" {
|
||||
log.Panic("I cannot find a place to store config file")
|
||||
}
|
||||
|
||||
log.Infof("✅ Config file will be stored in %s", path)
|
||||
|
||||
cfg.path = path
|
||||
cfg.mutex.Unlock()
|
||||
cfg.Save()
|
||||
cfg.mutex.Lock()
|
||||
return cfg
|
||||
|
||||
}
|
||||
|
||||
func (cfg *Config) Save() error {
|
||||
cfg.mutex.Lock()
|
||||
defer cfg.mutex.Unlock()
|
||||
|
||||
cfg.config = lowerKeysMap(cfg.config)
|
||||
|
||||
data, err := yaml.Marshal(cfg.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(cfg.path, data, 0644)
|
||||
}
|
||||
|
||||
func (cfg *Config) SetValue(path []string, value interface{}) {
|
||||
cfg.setValue(path, value)
|
||||
cfg.Save()
|
||||
}
|
||||
|
||||
func (cfg *Config) GetValue(path []string) (interface{}, error) {
|
||||
cfg.mutex.Lock()
|
||||
defer cfg.mutex.Unlock()
|
||||
|
||||
current := cfg.config
|
||||
for i, key := range path {
|
||||
key = strings.ToLower(key)
|
||||
|
||||
next, ok := current[key]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("path %s does not exist", strings.Join(path[:i+1], "."))
|
||||
}
|
||||
if i < len(path)-1 {
|
||||
current, ok = next.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("path %s is not a Config", strings.Join(path[:i+1], "."))
|
||||
}
|
||||
continue
|
||||
}
|
||||
return next, nil
|
||||
}
|
||||
return nil, fmt.Errorf("path %s does not exist", strings.Join(path[:], "."))
|
||||
}
|
||||
|
||||
// overrideConfig sets a value in a nested map[string]interface{} at the given path.
|
||||
func (cfg *Config) setValue(path []string, value interface{}) {
|
||||
cfg.mutex.Lock()
|
||||
defer cfg.mutex.Unlock()
|
||||
|
||||
current := cfg.config
|
||||
for i, key := range path {
|
||||
key = strings.ToLower(key)
|
||||
if i == len(path)-1 {
|
||||
current[key] = value
|
||||
return
|
||||
}
|
||||
// ensure intermediate maps exist
|
||||
if _, ok := current[key]; !ok {
|
||||
current[key] = make(map[string]interface{})
|
||||
}
|
||||
next, ok := current[key].(map[string]interface{})
|
||||
if !ok {
|
||||
// If the path conflicts with a non-object, overwrite it
|
||||
next = make(map[string]interface{})
|
||||
current[key] = next
|
||||
}
|
||||
current = next
|
||||
}
|
||||
}
|
||||
|
||||
// getHomeYmlPath constructs and returns the path to the home directory of the
|
||||
// current user followed by ".pmomusic.yml".
|
||||
//
|
||||
// This function does not take any parameters but it relies on the
|
||||
// `user.Current()` function, which can return an error if it fails to determine
|
||||
// the current user or their home directory. In such cases, a description of the
|
||||
// error is printed to standard output and the empty string is returned.
|
||||
//
|
||||
// The function returns a string representing the path to the file in the
|
||||
// following format: "$HOME/.pmomusic.yml". It does not return an error value
|
||||
// since no errors are expected to occur during normal operation.
|
||||
//
|
||||
// Side Effects: None. This is a pure function that only depends on input and
|
||||
// produces output without changing any state or causing side effects, except
|
||||
// for the printing of potential error messages.
|
||||
//
|
||||
// Edge Cases: If `user.Current()` fails to determine the current user's home
|
||||
// directory or the current user, an empty string is returned and a description
|
||||
// of the error is printed.
|
||||
//
|
||||
// Example usage:
|
||||
//
|
||||
// fmt.Println(getHomeYmlPath())
|
||||
//
|
||||
// This will print something like "/Users/username/.pmomusic.yml" on macOS or Linux, or "C:\Users\Username\" on Windows if the current user's home directory is "C:\Users\Username".
|
||||
func getHomeYmlPath() string {
|
||||
usr, err := user.Current()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return path.Join(usr.HomeDir, ".pmomusic.yml")
|
||||
}
|
||||
|
||||
func applyEnvOverrides(cfg *Config) {
|
||||
for _, env := range os.Environ() {
|
||||
if !strings.HasPrefix(env, envPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Split env var into key and value
|
||||
parts := strings.SplitN(env, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
keyPath := strings.Split(strings.TrimPrefix(parts[0], envPrefix), "__")
|
||||
value := parts[1]
|
||||
|
||||
overrideConfig(cfg, keyPath, value)
|
||||
}
|
||||
}
|
||||
|
||||
func convertYAMLScalar(s string) interface{} {
|
||||
var out interface{}
|
||||
err := yaml.Unmarshal([]byte(s), &out)
|
||||
if err != nil {
|
||||
// fallback: keep string if parsing failed
|
||||
return s
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func overrideConfig(cfg *Config, keyPath []string, value string) {
|
||||
iv := convertYAMLScalar(value)
|
||||
cfg.setValue(keyPath, iv)
|
||||
}
|
||||
|
||||
func lowerKeysMap(m map[string]interface{}) map[string]interface{} {
|
||||
out := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
lk := strings.ToLower(k)
|
||||
// si c'est une map imbriquée, traiter récursivement
|
||||
switch vv := v.(type) {
|
||||
case map[string]interface{}:
|
||||
out[lk] = lowerKeysMap(vv)
|
||||
default:
|
||||
out[lk] = v
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func GetConfig() *Config {
|
||||
|
||||
if _CONFIG == nil {
|
||||
_CONFIG = LoadConfig("")
|
||||
}
|
||||
|
||||
return _CONFIG
|
||||
}
|
||||
|
||||
func (conf *Config) GetBaseURL() string {
|
||||
url, _ := conf.GetValue([]string{"host", "base_url"})
|
||||
surl, ok := url.(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return surl
|
||||
}
|
||||
|
||||
func (conf *Config) GetHTTPPort() int {
|
||||
port, _ := conf.GetValue([]string{"host", "http_port"})
|
||||
|
||||
switch val := port.(type) {
|
||||
case int:
|
||||
return val
|
||||
case int64:
|
||||
return int(val)
|
||||
case float64:
|
||||
return int(val)
|
||||
case string:
|
||||
i, err := strconv.Atoi(val)
|
||||
if err == nil {
|
||||
return i
|
||||
}
|
||||
default:
|
||||
return 1900
|
||||
}
|
||||
|
||||
return 1900
|
||||
}
|
||||
|
||||
func (conf *Config) GetDeviceUDN(devtype, name string) string {
|
||||
udn, error := conf.GetValue([]string{"devices", string(devtype), name, "udn"})
|
||||
|
||||
if error != nil {
|
||||
udn = uuid.New().String()
|
||||
conf.SetValue([]string{"devices", string(devtype), name, "udn"}, udn)
|
||||
conf.Save()
|
||||
}
|
||||
|
||||
return udn.(string)
|
||||
}
|
||||
|
||||
func (conf *Config) GetCoverCacheDir() string {
|
||||
cachedir, _ := conf.GetValue([]string{"host", "cover_cache", "directory"})
|
||||
if cachedir == nil {
|
||||
cachedir = "./.pmomusic_covers"
|
||||
}
|
||||
|
||||
sdir, ok := cachedir.(string)
|
||||
|
||||
if ok && sdir != "" {
|
||||
return sdir
|
||||
}
|
||||
|
||||
return "./.pmomusic_covers"
|
||||
}
|
||||
|
||||
func (conf *Config) GetCoverCacheSize() int {
|
||||
size, _ := conf.GetValue([]string{"host", "cover_cache", "size"})
|
||||
if size == nil {
|
||||
return 2000
|
||||
}
|
||||
|
||||
switch v := size.(type) {
|
||||
case float64:
|
||||
return int(v)
|
||||
case int:
|
||||
return v
|
||||
default:
|
||||
return 2000
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
host:
|
||||
http_port: "8080"
|
||||
cover_cache:
|
||||
directory: "./.pmomusic_covers"
|
||||
size: 2000
|
||||
devices:
|
||||
mediarenderer:
|
||||
mpd_renderer:
|
||||
mediaserver:
|
||||
qobuz:
|
||||
udn: "uuid:28963b75-4c5f-4da7-b10e-ffafd"
|
||||
Reference in New Issue
Block a user