feat: config command
This commit is contained in:
		| @@ -20,13 +20,12 @@ | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 	"reflect" | ||||
|  | ||||
| 	"github.com/caarlos0/env" | ||||
| 	"github.com/pelletier/go-toml/v2" | ||||
| 	"github.com/goccy/go-yaml" | ||||
| 	"github.com/knadh/koanf/providers/confmap" | ||||
| 	"github.com/knadh/koanf/v2" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||
| @@ -35,74 +34,65 @@ import ( | ||||
| type ALRConfig struct { | ||||
| 	cfg   *types.Config | ||||
| 	paths *Paths | ||||
| } | ||||
|  | ||||
| var defaultConfig = &types.Config{ | ||||
| 	RootCmd:          "sudo", | ||||
| 	UseRootCmd:       true, | ||||
| 	PagerStyle:       "native", | ||||
| 	IgnorePkgUpdates: []string{}, | ||||
| 	AutoPull:         true, | ||||
| 	Repos:            []types.Repo{}, | ||||
| 	System *SystemConfig | ||||
| 	env    *EnvConfig | ||||
| } | ||||
|  | ||||
| func New() *ALRConfig { | ||||
| 	return &ALRConfig{} | ||||
| 	return &ALRConfig{ | ||||
| 		System: NewSystemConfig(), | ||||
| 		env:    NewEnvConfig(), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func readConfig(path string) (*types.Config, error) { | ||||
| 	file, err := os.Open(path) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| func defaultConfigKoanf() *koanf.Koanf { | ||||
| 	k := koanf.New(".") | ||||
| 	defaults := map[string]interface{}{ | ||||
| 		"rootCmd":          "sudo", | ||||
| 		"useRootCmd":       true, | ||||
| 		"pagerStyle":       "native", | ||||
| 		"ignorePkgUpdates": []string{}, | ||||
| 		"logLevel":         "info", | ||||
| 		"autoPull":         true, | ||||
| 		"repos":            []types.Repo{}, | ||||
| 	} | ||||
| 	defer file.Close() | ||||
|  | ||||
| 	config := types.Config{} | ||||
|  | ||||
| 	if err := toml.NewDecoder(file).Decode(&config); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| 	return &config, nil | ||||
| } | ||||
|  | ||||
| func mergeStructs(dst, src interface{}) { | ||||
| 	srcVal := reflect.ValueOf(src) | ||||
| 	if srcVal.IsNil() { | ||||
| 		return | ||||
| 	} | ||||
| 	srcVal = srcVal.Elem() | ||||
| 	dstVal := reflect.ValueOf(dst).Elem() | ||||
|  | ||||
| 	for i := range srcVal.NumField() { | ||||
| 		srcField := srcVal.Field(i) | ||||
| 		srcFieldName := srcVal.Type().Field(i).Name | ||||
|  | ||||
| 		dstField := dstVal.FieldByName(srcFieldName) | ||||
| 		if dstField.IsValid() && dstField.CanSet() { | ||||
| 			dstField.Set(srcField) | ||||
| 		} | ||||
| 	if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil { | ||||
| 		panic(k) | ||||
| 	} | ||||
| 	return k | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) Load() error { | ||||
| 	systemConfig, err := readConfig( | ||||
| 		constants.SystemConfigPath, | ||||
| 	) | ||||
| 	if err != nil { | ||||
| 		slog.Debug("Cannot read system config", "err", err) | ||||
| 	config := types.Config{} | ||||
|  | ||||
| 	merged := koanf.New(".") | ||||
|  | ||||
| 	if err := c.System.Load(); err != nil { | ||||
| 		return fmt.Errorf("failed to load system config: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	config := &types.Config{} | ||||
|  | ||||
| 	mergeStructs(config, defaultConfig) | ||||
| 	mergeStructs(config, systemConfig) | ||||
| 	err = env.Parse(config) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	if err := c.env.Load(); err != nil { | ||||
| 		return fmt.Errorf("failed to load env config: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	c.cfg = config | ||||
| 	systemK := c.System.koanf() | ||||
| 	envK := c.env.koanf() | ||||
|  | ||||
| 	if err := merged.Merge(defaultConfigKoanf()); err != nil { | ||||
| 		return fmt.Errorf("failed to merge default config: %w", err) | ||||
| 	} | ||||
| 	if err := merged.Merge(systemK); err != nil { | ||||
| 		return fmt.Errorf("failed to merge system config: %w", err) | ||||
| 	} | ||||
| 	if err := merged.Merge(envK); err != nil { | ||||
| 		return fmt.Errorf("failed to merge env config: %w", err) | ||||
| 	} | ||||
| 	if err := merged.Unmarshal("", &config); err != nil { | ||||
| 		return fmt.Errorf("failed to unmarshal merged config: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	c.cfg = &config | ||||
|  | ||||
| 	c.paths = &Paths{} | ||||
| 	c.paths.UserConfigPath = constants.SystemConfigPath | ||||
| @@ -110,52 +100,24 @@ func (c *ALRConfig) Load() error { | ||||
| 	c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo") | ||||
| 	c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs") | ||||
| 	c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db") | ||||
| 	// c.initPaths() | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) RootCmd() string { | ||||
| 	return c.cfg.RootCmd | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) PagerStyle() string { | ||||
| 	return c.cfg.PagerStyle | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) AutoPull() bool { | ||||
| 	return c.cfg.AutoPull | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) Repos() []types.Repo { | ||||
| 	return c.cfg.Repos | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) SetRepos(repos []types.Repo) { | ||||
| 	c.cfg.Repos = repos | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) IgnorePkgUpdates() []string { | ||||
| 	return c.cfg.IgnorePkgUpdates | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) LogLevel() string { | ||||
| 	return c.cfg.LogLevel | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) UseRootCmd() bool { | ||||
| 	return c.cfg.UseRootCmd | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) GetPaths() *Paths { | ||||
| 	return c.paths | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) SaveUserConfig() error { | ||||
| 	f, err := os.Create(c.paths.UserConfigPath) | ||||
| func (c *ALRConfig) ToYAML() (string, error) { | ||||
| 	data, err := yaml.Marshal(c.cfg) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	return toml.NewEncoder(f).Encode(c.cfg) | ||||
| 	return string(data), nil | ||||
| } | ||||
|  | ||||
| func (c *ALRConfig) RootCmd() string             { return c.cfg.RootCmd } | ||||
| func (c *ALRConfig) PagerStyle() string          { return c.cfg.PagerStyle } | ||||
| func (c *ALRConfig) AutoPull() bool              { return c.cfg.AutoPull } | ||||
| func (c *ALRConfig) Repos() []types.Repo         { return c.cfg.Repos } | ||||
| func (c *ALRConfig) SetRepos(repos []types.Repo) { c.System.SetRepos(repos) } | ||||
| func (c *ALRConfig) IgnorePkgUpdates() []string  { return c.cfg.IgnorePkgUpdates } | ||||
| func (c *ALRConfig) LogLevel() string            { return c.cfg.LogLevel } | ||||
| func (c *ALRConfig) UseRootCmd() bool            { return c.cfg.UseRootCmd } | ||||
| func (c *ALRConfig) GetPaths() *Paths            { return c.paths } | ||||
|   | ||||
							
								
								
									
										76
									
								
								internal/config/env_config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								internal/config/env_config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| // ALR - Any Linux Repository | ||||
| // Copyright (C) 2025 The ALR Authors | ||||
| // | ||||
| // This program 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. | ||||
| // | ||||
| // This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/knadh/koanf/providers/env" | ||||
| 	"github.com/knadh/koanf/v2" | ||||
| 	"golang.org/x/text/cases" | ||||
| 	"golang.org/x/text/language" | ||||
| ) | ||||
|  | ||||
| type EnvConfig struct { | ||||
| 	k *koanf.Koanf | ||||
| } | ||||
|  | ||||
| func NewEnvConfig() *EnvConfig { | ||||
| 	return &EnvConfig{ | ||||
| 		k: koanf.New("."), | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *EnvConfig) koanf() *koanf.Koanf { | ||||
| 	return c.k | ||||
| } | ||||
|  | ||||
| func (c *EnvConfig) Load() error { | ||||
| 	allowedKeys := map[string]struct{}{ | ||||
| 		"ALR_LOG_LEVEL":   {}, | ||||
| 		"ALR_PAGER_STYLE": {}, | ||||
| 		"ALR_AUTO_PULL":   {}, | ||||
| 	} | ||||
| 	err := c.k.Load(env.Provider("ALR_", ".", func(s string) string { | ||||
| 		_, ok := allowedKeys[s] | ||||
| 		if !ok { | ||||
| 			return "" | ||||
| 		} | ||||
| 		withoutPrefix := strings.TrimPrefix(s, "ALR_") | ||||
| 		lowered := strings.ToLower(withoutPrefix) | ||||
| 		dotted := strings.ReplaceAll(lowered, "__", ".") | ||||
| 		parts := strings.Split(dotted, ".") | ||||
| 		for i, part := range parts { | ||||
| 			if strings.Contains(part, "_") { | ||||
| 				parts[i] = toCamelCase(part) | ||||
| 			} | ||||
| 		} | ||||
| 		return strings.Join(parts, ".") | ||||
| 	}), nil) | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| func toCamelCase(s string) string { | ||||
| 	parts := strings.Split(s, "_") | ||||
| 	for i := 1; i < len(parts); i++ { | ||||
| 		if len(parts[i]) > 0 { | ||||
| 			parts[i] = cases.Title(language.Und, cases.NoLower).String(parts[i]) | ||||
| 		} | ||||
| 	} | ||||
| 	return strings.Join(parts, "") | ||||
| } | ||||
| @@ -21,9 +21,10 @@ package config | ||||
|  | ||||
| // Paths contains various paths used by ALR | ||||
| type Paths struct { | ||||
| 	UserConfigPath string | ||||
| 	CacheDir       string | ||||
| 	RepoDir        string | ||||
| 	PkgsDir        string | ||||
| 	DBPath         string | ||||
| 	SystemConfigPath string | ||||
| 	UserConfigPath   string | ||||
| 	CacheDir         string | ||||
| 	RepoDir          string | ||||
| 	PkgsDir          string | ||||
| 	DBPath           string | ||||
| } | ||||
|   | ||||
							
								
								
									
										144
									
								
								internal/config/system_config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								internal/config/system_config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| // ALR - Any Linux Repository | ||||
| // Copyright (C) 2025 The ALR Authors | ||||
| // | ||||
| // This program 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. | ||||
| // | ||||
| // This program 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 this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| package config | ||||
|  | ||||
| import ( | ||||
| 	"encoding/json" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"os" | ||||
|  | ||||
| 	ktoml "github.com/knadh/koanf/parsers/toml/v2" | ||||
| 	"github.com/knadh/koanf/providers/file" | ||||
| 	"github.com/knadh/koanf/v2" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||
| ) | ||||
|  | ||||
| type SystemConfig struct { | ||||
| 	k   *koanf.Koanf | ||||
| 	cfg *types.Config | ||||
| } | ||||
|  | ||||
| func NewSystemConfig() *SystemConfig { | ||||
| 	return &SystemConfig{ | ||||
| 		k:   koanf.New("."), | ||||
| 		cfg: &types.Config{}, | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) koanf() *koanf.Koanf { | ||||
| 	return c.k | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) Load() error { | ||||
| 	if _, err := os.Stat(constants.SystemConfigPath); errors.Is(err, os.ErrNotExist) { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	if err := c.k.Load(file.Provider(constants.SystemConfigPath), ktoml.Parser()); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return c.k.Unmarshal("", c.cfg) | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) Save() error { | ||||
| 	bytes, err := c.k.Marshal(ktoml.Parser()) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to marshal config: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	file, err := os.Create(constants.SystemConfigPath) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("failed to create config file: %w", err) | ||||
| 	} | ||||
| 	defer func() { | ||||
| 		if cerr := file.Close(); cerr != nil && err == nil { | ||||
| 			err = cerr | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	if _, err := file.Write(bytes); err != nil { | ||||
| 		return fmt.Errorf("failed to write config: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	if err := file.Sync(); err != nil { | ||||
| 		return fmt.Errorf("failed to sync config: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) SetRootCmd(v string) { | ||||
| 	err := c.k.Set("rootCmd", v) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) SetUseRootCmd(v bool) { | ||||
| 	err := c.k.Set("useRootCmd", v) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) SetPagerStyle(v string) { | ||||
| 	err := c.k.Set("pagerStyle", v) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) SetIgnorePkgUpdates(v []string) { | ||||
| 	err := c.k.Set("ignorePkgUpdates", v) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) SetAutoPull(v bool) { | ||||
| 	err := c.k.Set("autoPull", v) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) SetLogLevel(v string) { | ||||
| 	err := c.k.Set("logLevel", v) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (c *SystemConfig) SetRepos(v []types.Repo) { | ||||
| 	b, err := json.Marshal(v) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	var m []interface{} | ||||
| 	err = json.Unmarshal(b, &m) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| 	err = c.k.Set("repo", m) | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| } | ||||
		Reference in New Issue
	
	Block a user