package config import ( "fmt" "os" "path/filepath" "strings" "github.com/neilotoole/sq/cli/buildinfo" "github.com/neilotoole/sq/libsq/core/errz" "github.com/neilotoole/sq/libsq/source" "gopkg.in/yaml.v3" ) // Store saves and loads config. type Store interface { // Save writes config to the store. Save(cfg *Config) error // Load reads config from the store. Load() (*Config, error) // Location returns the location of the store, typically // a file path. Location() string } // YAMLFileStore provides persistence of config via YAML file. type YAMLFileStore struct { // Path is the location of the config file Path string // If HookLoad is non-nil, it is invoked by Load // on Path's bytes before the YAML is unmarshalled. // This allows expansion of variables etc. HookLoad func(data []byte) ([]byte, error) // ExtPaths holds locations of potential ext config, both dirs and files (with suffix ".sq.yml") ExtPaths []string } func (fs *YAMLFileStore) String() string { return fmt.Sprintf("config filestore: %v", fs.Path) } // Location implements Store. func (fs *YAMLFileStore) Location() string { return fs.Path } // Load reads config from disk. func (fs *YAMLFileStore) Load() (*Config, error) { bytes, err := os.ReadFile(fs.Path) if err != nil { return nil, errz.Wrapf(err, "config: failed to load file: %s", fs.Path) } loadHookFn := fs.HookLoad if loadHookFn != nil { bytes, err = loadHookFn(bytes) if err != nil { return nil, err } } cfg := &Config{} err = yaml.Unmarshal(bytes, cfg) if err != nil { return nil, errz.Wrapf(err, "config: %s: failed to unmarshal config YAML", fs.Path) } initCfg(cfg) repaired, err := source.VerifySetIntegrity(cfg.Sources) if err != nil { if repaired { // The config was repaired. Save the changes. err = errz.Combine(err, fs.Save(cfg)) } return nil, errz.Wrapf(err, "config: %s", fs.Path) } err = fs.loadExt(cfg) if err != nil { return nil, err } return cfg, nil } // loadExt loads extension config files into cfg. func (fs *YAMLFileStore) loadExt(cfg *Config) error { const extSuffix = ".sq.yml" var extCfgCandidates []string for _, extPath := range fs.ExtPaths { // TODO: This seems overly complicated: could just use glob // for any files in the same or child dir? if fiExtPath, err := os.Stat(extPath); err == nil { // path exists if fiExtPath.IsDir() { files, err := os.ReadDir(extPath) if err != nil { // just continue; no means of logging this yet (logging may // not have bootstrapped), and we shouldn't stop bootstrap // because of bad sqext files. continue } for _, file := range files { if file.IsDir() { // We don't currently descend through sub dirs continue } if !strings.HasSuffix(file.Name(), extSuffix) { continue } extCfgCandidates = append(extCfgCandidates, filepath.Join(extPath, file.Name())) } continue } // it's a file if !strings.HasSuffix(fiExtPath.Name(), extSuffix) { continue } extCfgCandidates = append(extCfgCandidates, filepath.Join(extPath, fiExtPath.Name())) } } for _, fp := range extCfgCandidates { bytes, err := os.ReadFile(fp) if err != nil { return errz.Wrapf(err, "error reading config ext file: %s", fp) } ext := &Ext{} err = yaml.Unmarshal(bytes, ext) if err != nil { return errz.Wrapf(err, "error parsing config ext file: %s", fp) } cfg.Ext.UserDrivers = append(cfg.Ext.UserDrivers, ext.UserDrivers...) } return nil } // Save writes config to disk. func (fs *YAMLFileStore) Save(cfg *Config) error { if fs == nil { return errz.New("config file store is nil") } if buildinfo.Version != "" { cfg.Version = buildinfo.Version } data, err := yaml.Marshal(cfg) if err != nil { return errz.Wrap(err, "failed to marshal config to YAML") } // It's possible that the parent dir of fs.Path doesn't exist. dir := filepath.Dir(fs.Path) err = os.MkdirAll(dir, 0o750) if err != nil { return errz.Wrapf(err, "failed to make parent dir of sq config file: %s", dir) } err = os.WriteFile(fs.Path, data, 0o600) if err != nil { return errz.Wrap(err, "failed to save config file") } return nil } // FileExists returns true if the backing file can be accessed, false if it doesn't // exist or on any error. func (fs *YAMLFileStore) FileExists() bool { _, err := os.Stat(fs.Path) return err == nil } // DiscardStore implements Store but its Save method is no-op // and Load always returns a new empty Config. Useful for testing. type DiscardStore struct{} var _ Store = (*DiscardStore)(nil) // Load returns a new empty Config. func (DiscardStore) Load() (*Config, error) { return New(), nil } // Save is no-op. func (DiscardStore) Save(*Config) error { return nil } // Location returns /dev/null. func (DiscardStore) Location() string { return "/dev/null" }