diff --git a/appcontext/appcontext.go b/appcontext/appcontext.go index 4b3f9bc96..19b59b400 100644 --- a/appcontext/appcontext.go +++ b/appcontext/appcontext.go @@ -6,7 +6,6 @@ import ( "github.com/PlakarKorp/pkg" "github.com/PlakarKorp/plakar/config" "github.com/PlakarKorp/plakar/cookies" - "github.com/PlakarKorp/plakar/utils" ) type AppContext struct { @@ -97,7 +96,7 @@ func (c *AppContext) GetPkgManager() *pkg.Manager { } func (c *AppContext) ReloadConfig() error { - cfg, err := utils.LoadConfig(c.ConfigDir) + cfg, err := config.Load(c.ConfigDir) if err != nil { return err } diff --git a/config/config.go b/config/config.go index 1cc4d8bde..35a6ecd9b 100644 --- a/config/config.go +++ b/config/config.go @@ -3,8 +3,6 @@ package config import ( "fmt" "strings" - - "maps" ) type Config struct { @@ -42,11 +40,9 @@ func (c *Config) GetRepository(name string) (map[string]string, error) { } if _, ok := kv["location"]; !ok { return nil, fmt.Errorf("repository %s has no location", name) - } else { - res := make(map[string]string) - maps.Copy(res, kv) - return res, nil } + + return resolve(kv) } func (c *Config) HasSource(name string) bool { @@ -54,13 +50,11 @@ func (c *Config) HasSource(name string) bool { return ok } -func (c *Config) GetSource(name string) (map[string]string, bool) { +func (c *Config) GetSource(name string) (map[string]string, error) { if kv, ok := c.Sources[name]; !ok { - return nil, false + return nil, fmt.Errorf("failed to retrieve configuration for source %q", name) } else { - res := make(map[string]string) - maps.Copy(res, kv) - return res, ok + return resolve(kv) } } @@ -69,12 +63,10 @@ func (c *Config) HasDestination(name string) bool { return ok } -func (c *Config) GetDestination(name string) (map[string]string, bool) { +func (c *Config) GetDestination(name string) (map[string]string, error) { if kv, ok := c.Destinations[name]; !ok { - return nil, false + return nil, fmt.Errorf("failed to retrieve configuration for destination %q", name) } else { - res := make(map[string]string) - maps.Copy(res, kv) - return res, ok + return resolve(kv) } } diff --git a/config/config_test.go b/config/config_test.go index 748189da0..7e34c2698 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -64,13 +64,13 @@ func TestGetSource(t *testing.T) { } // Test non-existent source - source, ok := cfg.GetSource("test-source") - require.False(t, ok) + source, err := cfg.GetSource("test-source") + require.NotNil(t, err) require.Nil(t, source) // Test existing source cfg.Sources["test-source"] = SourceConfig{"url": "test://url"} - source, ok = cfg.GetSource("test-source") - require.True(t, ok) + source, err = cfg.GetSource("test-source") + require.Nil(t, err) require.Equal(t, "test://url", source["url"]) } diff --git a/config/load.go b/config/load.go new file mode 100644 index 000000000..332f8f424 --- /dev/null +++ b/config/load.go @@ -0,0 +1,202 @@ +package config + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "go.yaml.in/yaml/v3" +) + +const CONFIG_VERSION = "v1.0.0" + +type ( + storesConfig struct { + Version string `yaml:"version"` + Default string `yaml:"default,omitempty"` + Stores map[string]map[string]string `yaml:"stores"` + } + + sourcesConfig struct { + Version string `yaml:"version"` + Sources map[string]map[string]string `yaml:"sources"` + } + + destinationsConfig struct { + Version string `yaml:"version"` + Destinations map[string]map[string]string `yaml:"destinations"` + } +) + +func load(file string, dst any) error { + f, err := os.Open(file) + if err != nil { + if os.IsNotExist(err) { + return err + } + return fmt.Errorf("failed to open file %s: %w", file, err) + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return fmt.Errorf("failed to stat file %s: %w", file, err) + } + if info.Size() == 0 { + return nil + } + + // try to load the new format + err = yaml.NewDecoder(f).Decode(dst) + var version string + switch t := dst.(type) { + case *storesConfig: + version = t.Version + case *destinationsConfig: + version = t.Version + case *sourcesConfig: + version = t.Version + default: + return fmt.Errorf("invalid configuration type %v", t) + } + if err == nil && version == CONFIG_VERSION { + return nil + } + + // fallback to the previous format + _, err = f.Seek(0, io.SeekStart) + if err != nil { + return fmt.Errorf("failed to rewind config file: %w", err) + } + + switch t := dst.(type) { + case *storesConfig: + err = yaml.NewDecoder(f).Decode(&t.Stores) + if err == nil { + for k, v := range t.Stores { + if _, ok := v[".isDefault"]; ok { + if t.Default != "" { + return fmt.Errorf("multiple default store") + } + t.Default = k + delete(v, ".isDefault") + } + } + } + case *destinationsConfig: + err = yaml.NewDecoder(f).Decode(&t.Destinations) + case *sourcesConfig: + err = yaml.NewDecoder(f).Decode(&t.Sources) + } + if err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + return nil +} + +func loadFallback(dir string) (*Config, error) { + // Load old config if found + oldpath := filepath.Join(dir, "plakar.yml") + cfg, err := LoadOldConfigIfExists(oldpath) + if err != nil { + return nil, fmt.Errorf("error reading file %s: %w", oldpath, err) + } + + // Save the config in the new format right now + if err := Save(dir, cfg); err != nil { + return nil, fmt.Errorf("failed to update config file: %w", err) + } + // Do we want to remove the old file? + return cfg, nil +} + +func Load(dir string) (*Config, error) { + sources := sourcesConfig{} + destinations := destinationsConfig{} + stores := storesConfig{} + + err := load(filepath.Join(dir, "sources.yml"), &sources) + if err != nil { + if os.IsNotExist(err) { + return loadFallback(dir) + } + return nil, err + } + + err = load(filepath.Join(dir, "destinations.yml"), &destinations) + if err != nil { + if os.IsNotExist(err) { + return loadFallback(dir) + } + return nil, err + } + + err = load(filepath.Join(dir, "stores.yml"), &stores) + if err != nil && os.IsNotExist(err) { + // try to load former file + err = load("klosets.yml", &stores) + } + if err != nil { + if os.IsNotExist(err) { + return loadFallback(dir) + } + return nil, err + } + + cfg := NewConfig() + cfg.Sources = sources.Sources + cfg.Destinations = destinations.Destinations + cfg.Repositories = stores.Stores + cfg.DefaultRepository = stores.Default + + return cfg, nil +} + +func save(file string, src any) error { + tmpFile, err := os.CreateTemp(filepath.Dir(file), "config.*.yml") + if err != nil { + return err + } + + err = yaml.NewEncoder(tmpFile).Encode(src) + tmpFile.Close() + + if err == nil { + err = os.Rename(tmpFile.Name(), file) + } + + if err != nil { + os.Remove(tmpFile.Name()) + return err + } + + return nil +} + +func Save(dir string, cfg *Config) error { + err := save(filepath.Join(dir, "sources.yml"), sourcesConfig{ + Version: CONFIG_VERSION, + Sources: cfg.Sources, + }) + if err != nil { + return err + } + err = save(filepath.Join(dir, "destinations.yml"), destinationsConfig{ + Version: CONFIG_VERSION, + Destinations: cfg.Destinations, + }) + if err != nil { + return err + } + err = save(filepath.Join(dir, "stores.yml"), storesConfig{ + Version: CONFIG_VERSION, + Default: cfg.DefaultRepository, + Stores: cfg.Repositories, + }) + if err != nil { + return err + } + return nil +} diff --git a/utils/oldconfig.go b/config/old.go similarity index 60% rename from utils/oldconfig.go rename to config/old.go index 7c7ee2b18..a9f4c7351 100644 --- a/utils/oldconfig.go +++ b/config/old.go @@ -1,4 +1,4 @@ -package utils +package config import ( "fmt" @@ -6,18 +6,16 @@ import ( "os" "go.yaml.in/yaml/v3" - - "github.com/PlakarKorp/plakar/config" ) type OldConfig struct { - DefaultRepository string `yaml:"default-repo"` - Repositories map[string]config.RepositoryConfig `yaml:"repositories"` - Remotes map[string]config.SourceConfig `yaml:"remotes"` + DefaultRepository string `yaml:"default-repo"` + Repositories map[string]RepositoryConfig `yaml:"repositories"` + Remotes map[string]SourceConfig `yaml:"remotes"` } -func LoadOldConfigIfExists(configFile string) (*config.Config, error) { - cfg := config.NewConfig() +func LoadOldConfigIfExists(configFile string) (*Config, error) { + cfg := NewConfig() f, err := os.Open(configFile) if err != nil { @@ -36,7 +34,7 @@ func LoadOldConfigIfExists(configFile string) (*config.Config, error) { cfg.DefaultRepository = old.DefaultRepository cfg.Repositories = old.Repositories cfg.Sources = old.Remotes - cfg.Destinations = make(map[string]config.DestinationConfig) + cfg.Destinations = make(map[string]DestinationConfig) for key, val := range cfg.Sources { res := make(map[string]string) maps.Copy(res, val) diff --git a/config/resolve.go b/config/resolve.go new file mode 100644 index 000000000..dd43ce38a --- /dev/null +++ b/config/resolve.go @@ -0,0 +1,94 @@ +package config + +import ( + "bufio" + "fmt" + "io" + "os" + "os/exec" + "runtime" + "strings" +) + +func getPassphraseFromCommand(cmd string) (string, error) { + var c *exec.Cmd + switch runtime.GOOS { + case "windows": + c = exec.Command("cmd", "/C", cmd) + default: // assume unix-esque + c = exec.Command("/bin/sh", "-c", cmd) + } + + stdout, err := c.StdoutPipe() + if err != nil { + return "", err + } + + if err := c.Start(); err != nil { + return "", err + } + + var pass string + var lines int + scan := bufio.NewScanner(stdout) + for scan.Scan() { + pass = scan.Text() + lines++ + } + + // don't deadlock in case the scanner fails + io.Copy(io.Discard, stdout) + + if err := c.Wait(); err != nil { + return "", err + } + + if err := scan.Err(); err != nil { + return "", err + } + + if lines != 1 { + return "", fmt.Errorf("passphrase_cmd returned too many lines") + } + + return pass, nil +} + +func resolve(conf map[string]string) (map[string]string, error) { + ret := make(map[string]string, len(conf)) + + for k, v := range conf { + if r, ok := strings.CutPrefix(v, "env:"); ok { + v, ok := os.LookupEnv(r) + if !ok { + return nil, fmt.Errorf("env variable %q not defined", r) + } + ret[k] = v + } else if r, ok := strings.CutPrefix(v, "file:"); ok { + v, err := os.ReadFile(r) + if err != nil { + return nil, fmt.Errorf("failed to read %q: %w", r, err) + } + ret[k] = strings.TrimRight(string(v), "\r\n") + } else if r, ok := strings.CutPrefix(v, "cmd:"); ok { + v, err := getPassphraseFromCommand(r) + if err != nil { + return nil, err + } + ret[k] = v + } else if k == "passphrase_cmd" { + // XXX we used to use this field before we introduced the + // `cmd:' prefix, need to keep the compatibility. + v, err := getPassphraseFromCommand(v) + if err != nil { + return nil, err + } + ret["passphrase"] = v + } else { + // "raw:" is optional and implied + ret[k], _ = strings.CutPrefix(v, "raw:") + } + } + + return ret, nil +} diff --git a/main.go b/main.go index c81ca39f9..41dfdb889 100644 --- a/main.go +++ b/main.go @@ -555,11 +555,6 @@ func getPassphraseFromEnv(ctx *appcontext.AppContext, params map[string]string) return pass, nil } - if cmd, ok := params["passphrase_cmd"]; ok { - delete(params, "passphrase_cmd") - return utils.GetPassphraseFromCommand(cmd) - } - if pass, ok := os.LookupEnv("PLAKAR_PASSPHRASE"); ok { return pass, nil } diff --git a/subcommands/backup/backup.go b/subcommands/backup/backup.go index 699effa08..7bab2eb43 100644 --- a/subcommands/backup/backup.go +++ b/subcommands/backup/backup.go @@ -220,9 +220,9 @@ func (cmd *Backup) DoBackup(ctx *appcontext.AppContext, repo *repository.Reposit maps.Copy(cmdOptsCopy, cmd.Opts) if strings.HasPrefix(scanDir, "@") { - remote, ok := ctx.Config.GetSource(scanDir[1:]) - if !ok { - return 1, fmt.Errorf("could not resolve importer: %s", scanDir), objects.MAC{}, nil + remote, err := ctx.Config.GetSource(scanDir[1:]) + if err != nil { + return 1, err, objects.MAC{}, nil } if _, ok := remote["location"]; !ok { return 1, fmt.Errorf("could not resolve importer location: %s", scanDir), objects.MAC{}, nil diff --git a/subcommands/config/config.go b/subcommands/config/config.go index f3f3def53..516024c3e 100644 --- a/subcommands/config/config.go +++ b/subcommands/config/config.go @@ -30,6 +30,7 @@ import ( "github.com/PlakarKorp/kloset/connectors/importer" "github.com/PlakarKorp/kloset/connectors/storage" "github.com/PlakarKorp/plakar/appcontext" + "github.com/PlakarKorp/plakar/config" "github.com/PlakarKorp/plakar/subcommands" "github.com/PlakarKorp/plakar/utils" "go.yaml.in/yaml/v3" @@ -110,7 +111,7 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a } cfgMap[name][key] = val } - return utils.SaveConfig(ctx.ConfigDir, ctx.Config) + return config.Save(ctx.ConfigDir, ctx.Config) case "check": p := flag.NewFlagSet("check", flag.ExitOnError) @@ -137,9 +138,9 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a store.Close(ctx) case "source": - cfg, ok := ctx.Config.GetSource(name) - if !ok { - return fmt.Errorf("failed to retrieve configuration for source %q", name) + cfg, err := ctx.Config.GetSource(name) + if err != nil { + return err } imp, err := importer.NewImporter(ctx.GetInner(), ctx.ImporterOpts(), cfg) if err != nil { @@ -148,9 +149,9 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a imp.Close(ctx) case "destination": - cfg, ok := ctx.Config.GetDestination(name) - if !ok { - return fmt.Errorf("failed to retrieve configuration for destination %q", name) + cfg, err := ctx.Config.GetDestination(name) + if err != nil { + return err } exp, err := exporter.NewExporter(ctx.GetInner(), ctx.ExporterOpts(), cfg) if err != nil { @@ -240,7 +241,7 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a } } } - return utils.SaveConfig(ctx.ConfigDir, ctx.Config) + return config.Save(ctx.ConfigDir, ctx.Config) case "ping": p := flag.NewFlagSet("ping", flag.ExitOnError) @@ -271,9 +272,9 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a fmt.Println("configuration OK") case "source": - cfg, ok := ctx.Config.GetSource(name) - if !ok { - return fmt.Errorf("failed to retrieve configuration for source %q", name) + cfg, err := ctx.Config.GetSource(name) + if err != nil { + return err } imp, err := importer.NewImporter(ctx.GetInner(), ctx.ImporterOpts(), cfg) if err != nil { @@ -286,9 +287,9 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a fmt.Println("configuration OK") case "destination": - cfg, ok := ctx.Config.GetDestination(name) - if !ok { - return fmt.Errorf("failed to retrieve configuration for destination %q", name) + cfg, err := ctx.Config.GetDestination(name) + if err != nil { + return err } exp, err := exporter.NewExporter(ctx.GetInner(), ctx.ExporterOpts(), cfg) if err != nil { @@ -320,7 +321,7 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a return fmt.Errorf("%s %q does not exist", cmd, name) } delete(cfgMap, name) - return utils.SaveConfig(ctx.ConfigDir, ctx.Config) + return config.Save(ctx.ConfigDir, ctx.Config) case "set": p := flag.NewFlagSet("set", flag.ExitOnError) @@ -344,7 +345,7 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a } cfgMap[name][key] = val } - return utils.SaveConfig(ctx.ConfigDir, ctx.Config) + return config.Save(ctx.ConfigDir, ctx.Config) case "show": var opt_json bool @@ -447,7 +448,7 @@ func dispatchSubcommand(ctx *appcontext.AppContext, cmd string, subcmd string, a } delete(cfgMap[name], key) } - return utils.SaveConfig(ctx.ConfigDir, ctx.Config) + return config.Save(ctx.ConfigDir, ctx.Config) default: return fmt.Errorf("usage: plakar %s [add|check|import|ping|rm|set|show|unset]", cmd) diff --git a/subcommands/config/config_test.go b/subcommands/config/config_test.go index 7f6746704..d7d93bee6 100644 --- a/subcommands/config/config_test.go +++ b/subcommands/config/config_test.go @@ -8,7 +8,7 @@ import ( "github.com/PlakarKorp/kloset/repository" "github.com/PlakarKorp/plakar/appcontext" - "github.com/PlakarKorp/plakar/utils" + "github.com/PlakarKorp/plakar/config" "github.com/stretchr/testify/require" ) @@ -37,7 +37,7 @@ func TestConfigEmpty(t *testing.T) { }) configPath := filepath.Join(tmpDir, "config.yaml") - cfg, err := utils.LoadOldConfigIfExists(configPath) + cfg, err := config.LoadOldConfigIfExists(configPath) require.NoError(t, err) ctx := appcontext.NewAppContext() @@ -105,7 +105,7 @@ func TestCmdRemote(t *testing.T) { }) configPath := filepath.Join(tmpDir, "config.yaml") - cfg, err := utils.LoadOldConfigIfExists(configPath) + cfg, err := config.LoadOldConfigIfExists(configPath) require.NoError(t, err) ctx := appcontext.NewAppContext() ctx.Config = cfg @@ -157,7 +157,7 @@ func TestCmdRepository(t *testing.T) { }) configPath := filepath.Join(tmpDir, "config.yaml") - cfg, err := utils.LoadOldConfigIfExists(configPath) + cfg, err := config.LoadOldConfigIfExists(configPath) require.NoError(t, err) ctx := appcontext.NewAppContext() ctx.Config = cfg diff --git a/subcommands/ptar/ptar.go b/subcommands/ptar/ptar.go index 540eb944c..c37e156a8 100644 --- a/subcommands/ptar/ptar.go +++ b/subcommands/ptar/ptar.go @@ -335,9 +335,9 @@ func (cmd *Ptar) backup(ctx *appcontext.AppContext, repo *repository.RepositoryW "location": loc, } if strings.HasPrefix(loc, "@") { - remote, ok := ctx.Config.GetSource(loc[1:]) - if !ok { - return fmt.Errorf("could not resolve importer: %s", loc) + remote, err := ctx.Config.GetSource(loc[1:]) + if err != nil { + return err } if _, ok := remote["location"]; !ok { return fmt.Errorf("could not resolve importer location: %s", loc) diff --git a/subcommands/restore/restore.go b/subcommands/restore/restore.go index 82e55f730..2bd7eea81 100644 --- a/subcommands/restore/restore.go +++ b/subcommands/restore/restore.go @@ -151,9 +151,9 @@ func (cmd *Restore) Execute(ctx *appcontext.AppContext, repo *repository.Reposit "location": cmd.Target, } if strings.HasPrefix(cmd.Target, "@") { - remote, ok := ctx.Config.GetDestination(cmd.Target[1:]) - if !ok { - return 1, fmt.Errorf("could not resolve exporter: %s", cmd.Target) + remote, err := ctx.Config.GetDestination(cmd.Target[1:]) + if err != nil { + return 1, err } if _, ok := remote["location"]; !ok { return 1, fmt.Errorf("could not resolve exporter location: %s", cmd.Target) diff --git a/subcommands/sync/sync.go b/subcommands/sync/sync.go index 4cfc84437..e790fb5d8 100644 --- a/subcommands/sync/sync.go +++ b/subcommands/sync/sync.go @@ -119,19 +119,6 @@ func (cmd *Sync) Parse(ctx *appcontext.AppContext, args []string) error { return fmt.Errorf("invalid passphrase") } peerSecret = key - } else if cmd, ok := storeConfig["passphrase_cmd"]; ok { - passphrase, err := utils.GetPassphraseFromCommand(cmd) - if err != nil { - return fmt.Errorf("failed to read passphrase from command: %w", err) - } - key, err := encryption.DeriveKey(peerStoreConfig.Encryption.KDFParams, []byte(passphrase)) - if err != nil { - return err - } - if !encryption.VerifyCanary(peerStoreConfig.Encryption, key) { - return fmt.Errorf("invalid passphrase") - } - peerSecret = key } else { for { passphrase, err := utils.GetPassphrase("destination store") diff --git a/subcommands/sync/sync_test.go b/subcommands/sync/sync_test.go index b6c4888dd..31bd271e4 100644 --- a/subcommands/sync/sync_test.go +++ b/subcommands/sync/sync_test.go @@ -13,8 +13,8 @@ import ( "github.com/PlakarKorp/kloset/repository" "github.com/PlakarKorp/kloset/snapshot" "github.com/PlakarKorp/plakar/appcontext" + "github.com/PlakarKorp/plakar/config" ptesting "github.com/PlakarKorp/plakar/testing" - "github.com/PlakarKorp/plakar/utils" "github.com/stretchr/testify/require" ) @@ -107,13 +107,13 @@ func TestExecuteCmdSyncWithEncryption(t *testing.T) { // need to recreate configuration to store passphrase on peer repo opt_configfile := strings.TrimPrefix(peerRepo.Root(), "fs://") - cfg, err := utils.LoadConfig(opt_configfile) + cfg, err := config.Load(opt_configfile) require.NoError(t, err) lctx.Config = cfg lctx.Config.Repositories["peerRepo"] = make(map[string]string) lctx.Config.Repositories["peerRepo"]["passphrase"] = string(passphrase) lctx.Config.Repositories["peerRepo"]["location"] = peerRepo.Root() - err = utils.SaveConfig(opt_configfile, lctx.Config) + err = config.Save(opt_configfile, lctx.Config) require.NoError(t, err) indexId := snap.Header.GetIndexID() diff --git a/utils/config.go b/utils/config.go index 91fa2b2a9..1fa4d5a26 100644 --- a/utils/config.go +++ b/utils/config.go @@ -5,231 +5,13 @@ import ( "encoding/json" "fmt" "io" - "os" - "path/filepath" "slices" "gopkg.in/ini.v1" - "github.com/PlakarKorp/plakar/config" "go.yaml.in/yaml/v3" ) -type configHandler struct { - Path string -} - -const CONFIG_VERSION = "v1.0.0" - -type storesConfig struct { - Version string `yaml:"version"` - Default string `yaml:"default,omitempty"` - Stores map[string]map[string]string `yaml:"stores"` -} - -type sourcesConfig struct { - Version string `yaml:"version"` - Sources map[string]map[string]string `yaml:"sources"` -} - -type destinationsConfig struct { - Version string `yaml:"version"` - Destinations map[string]map[string]string `yaml:"destinations"` -} - -func newConfigHandler(path string) *configHandler { - return &configHandler{ - Path: path, - } -} - -func (cl *configHandler) Load() (*config.Config, error) { - sources := sourcesConfig{} - destinations := destinationsConfig{} - stores := storesConfig{} - - err := cl.load("sources.yml", &sources) - if err != nil { - if os.IsNotExist(err) { - return cl.LoadFallback() - } - return nil, err - } - - err = cl.load("destinations.yml", &destinations) - if err != nil { - if os.IsNotExist(err) { - return cl.LoadFallback() - } - return nil, err - } - - err = cl.load("stores.yml", &stores) - if err != nil && os.IsNotExist(err) { - // try to load former file - err = cl.load("klosets.yml", &stores) - } - if err != nil { - if os.IsNotExist(err) { - return cl.LoadFallback() - } - return nil, err - } - - cfg := config.NewConfig() - cfg.Sources = sources.Sources - cfg.Destinations = destinations.Destinations - cfg.Repositories = stores.Stores - cfg.DefaultRepository = stores.Default - return cfg, nil -} - -func (cl *configHandler) LoadFallback() (*config.Config, error) { - // Load old config if found - oldpath := filepath.Join(cl.Path, "plakar.yml") - cfg, err := LoadOldConfigIfExists(oldpath) - if err != nil { - return nil, fmt.Errorf("error reading file %s: %w", oldpath, err) - } - - // Save the config in the new format right now - err = SaveConfig(cl.Path, cfg) - if err != nil { - return nil, fmt.Errorf("failed to update config file: %w", err) - } - // Do we want to remove the old file? - return cfg, nil -} - -func (cl *configHandler) Save(cfg *config.Config) error { - err := cl.save("sources.yml", sourcesConfig{ - Version: CONFIG_VERSION, - Sources: cfg.Sources, - }) - if err != nil { - return err - } - err = cl.save("destinations.yml", destinationsConfig{ - Version: CONFIG_VERSION, - Destinations: cfg.Destinations, - }) - if err != nil { - return err - } - err = cl.save("stores.yml", storesConfig{ - Version: CONFIG_VERSION, - Default: cfg.DefaultRepository, - Stores: cfg.Repositories, - }) - if err != nil { - return err - } - return nil -} - -func (cl *configHandler) load(filename string, dst any) error { - path := filepath.Join(cl.Path, filename) - f, err := os.Open(path) - if err != nil { - if os.IsNotExist(err) { - return err - } - return fmt.Errorf("failed to open file %s: %w", path, err) - } - defer f.Close() - - info, err := f.Stat() - if err != nil { - return fmt.Errorf("failed to stat file %s: %w", path, err) - } - if info.Size() == 0 { - return nil - } - - // try to load the new format - err = yaml.NewDecoder(f).Decode(dst) - var version string - switch t := dst.(type) { - case *storesConfig: - version = t.Version - case *destinationsConfig: - version = t.Version - case *sourcesConfig: - version = t.Version - default: - return fmt.Errorf("invalid configuration type %v", t) - } - if err == nil && version == CONFIG_VERSION { - return nil - } - - // fallback to the previous format - _, err = f.Seek(0, io.SeekStart) - if err != nil { - return fmt.Errorf("failed to rewind config file: %w", err) - } - - switch t := dst.(type) { - case *storesConfig: - err = yaml.NewDecoder(f).Decode(&t.Stores) - if err == nil { - for k, v := range t.Stores { - if _, ok := v[".isDefault"]; ok { - if t.Default != "" { - return fmt.Errorf("multiple default store") - } - t.Default = k - delete(v, ".isDefault") - } - } - } - case *destinationsConfig: - err = yaml.NewDecoder(f).Decode(&t.Destinations) - case *sourcesConfig: - err = yaml.NewDecoder(f).Decode(&t.Sources) - } - if err != nil { - return fmt.Errorf("failed to parse config file: %w", err) - } - - return nil -} - -func (cl *configHandler) save(filename string, src any) error { - path := filepath.Join(cl.Path, filename) - tmpFile, err := os.CreateTemp(cl.Path, "config.*.yml") - if err != nil { - return err - } - - err = yaml.NewEncoder(tmpFile).Encode(src) - tmpFile.Close() - - if err == nil { - err = os.Rename(tmpFile.Name(), path) - } - - if err != nil { - os.Remove(tmpFile.Name()) - return err - } - - return nil -} - -func LoadConfig(configDir string) (*config.Config, error) { - cl := newConfigHandler(configDir) - cfg, err := cl.Load() - if err != nil { - return nil, err - } - return cfg, nil -} - -func SaveConfig(configDir string, cfg *config.Config) error { - return newConfigHandler(configDir).Save(cfg) -} - // toString converts various primitive types to string. func toString(v interface{}) string { switch t := v.(type) { diff --git a/utils/utils.go b/utils/utils.go index 93d179fa0..71a3ded13 100644 --- a/utils/utils.go +++ b/utils/utils.go @@ -17,15 +17,12 @@ package utils import ( - "bufio" "encoding/xml" "errors" "fmt" - "io" "net/http" "net/mail" "os" - "os/exec" "path" "path/filepath" "runtime" @@ -166,50 +163,6 @@ func GetPassphrase(prefix string) ([]byte, error) { return readpassphrase(in, out, prefix+" passphrase: ") } -func GetPassphraseFromCommand(cmd string) (string, error) { - var c *exec.Cmd - switch runtime.GOOS { - case "windows": - c = exec.Command("cmd", "/C", cmd) - default: // assume unix-esque - c = exec.Command("/bin/sh", "-c", cmd) - } - - stdout, err := c.StdoutPipe() - if err != nil { - return "", err - } - - if err := c.Start(); err != nil { - return "", err - } - - var pass string - var lines int - scan := bufio.NewScanner(stdout) - for scan.Scan() { - pass = scan.Text() - lines++ - } - - // don't deadlock in case the scanner fails - io.Copy(io.Discard, stdout) - - if err := c.Wait(); err != nil { - return "", err - } - - if err := scan.Err(); err != nil { - return "", err - } - - if lines != 1 { - return "", fmt.Errorf("passphrase_cmd returned too many lines") - } - - return pass, nil -} - func GetPassphraseConfirm(prefix string, minEntropyBits float64, retry int) ([]byte, error) { var in, out = os.Stdin, os.Stderr