diff --git a/internal/ui/config/config.go b/internal/ui/config/config.go index acaafb124..042d14452 100644 --- a/internal/ui/config/config.go +++ b/internal/ui/config/config.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/hcl/hcl/token" "github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/errors" "github.com/spf13/pflag" @@ -19,8 +20,13 @@ type Config struct { Password string `hcl:"password" env:"RESTIC_PASSWORD"` PasswordFile string `hcl:"password_file" flag:"password-file" env:"RESTIC_PASSWORD_FILE"` - Backends map[string]interface{} `hcl:"backend"` - Backup Backup `hcl:"backup"` + Backends map[string]interface{} + Backup Backup `hcl:"backup"` +} + +// Backend configures a backend. +type Backend struct { + Type string `hcl:"type"` } // BackendLocal is a backend in a local directory. @@ -29,6 +35,13 @@ type BackendLocal struct { Path string `hcl:"path"` } +// BackendSFTP is a backend stored on a server via sftp. +type BackendSFTP struct { + Type string `hcl:"type"` + User string `hcl:"user"` + Host string `hcl:"host"` +} + // Backup sets the options for the "backup" command. type Backup struct { Target []string `hcl:"target"` @@ -76,11 +89,20 @@ func Parse(buf []byte) (cfg Config, err error) { return Config{}, err } - // check for additional unknown items root := parsed.Node.(*ast.ObjectList) + // load all 'backend' sections + cfg.Backends, err = parseBackends(root) + if err != nil { + return Config{}, err + } + + // check for additional unknown items + rootTags := listTags(cfg, "hcl") + rootTags["backend"] = struct{}{} + checks := map[string]map[string]struct{}{ - "": listTags(cfg, "hcl"), + "": rootTags, "backup": listTags(Backup{}, "hcl"), } @@ -109,6 +131,88 @@ func Parse(buf []byte) (cfg Config, err error) { return cfg, nil } +// parseBackends parses the backend configuration sections. +func parseBackends(root *ast.ObjectList) (map[string]interface{}, error) { + backends := make(map[string]interface{}) + + // find top-level backend objects + for _, item := range root.Items { + // is not an object block + if len(item.Keys) == 0 { + continue + } + + // does not start with an an identifier + if item.Keys[0].Token.Type != token.IDENT { + continue + } + + // something other than a backend section + if s, ok := item.Keys[0].Token.Value().(string); !ok || s != "backend" { + continue + } + + // missing name + if len(item.Keys) != 2 { + return nil, errors.Errorf("backend has no name at line %v, column %v", + item.Pos().Line, item.Pos().Column) + } + + // check that the name is not empty + name := item.Keys[1].Token.Value().(string) + if len(name) == 0 { + return nil, errors.Errorf("backend name is empty at line %v, column %v", + item.Pos().Line, item.Pos().Column) + } + + // get the type of the backend by decoding it into the Backend truct + var be Backend + err := hcl.DecodeObject(&be, item) + if err != nil { + return nil, err + } + + // then decode it into the right type + var target interface{} + switch be.Type { + case "local", "": + target = &BackendLocal{Type: "local"} + case "sftp": + target = &BackendSFTP{} + default: + return nil, errors.Errorf("backend type %q is unknown at line %v, column %v", + be.Type, item.Pos().Line, item.Pos().Column) + } + + err = hcl.DecodeObject(target, item) + if err != nil { + return nil, err + } + + if _, ok := backends[name]; ok { + return nil, errors.Errorf("backend %q at line %v, column %v already configured", + name, item.Pos().Line, item.Pos().Column) + } + + // check structure of the backend object + innerBlock, ok := item.Val.(*ast.ObjectType) + if !ok { + return nil, errors.Errorf("unable to verify structure of backend %q at line %v, column %v already configured", + name, item.Pos().Line, item.Pos().Column) + } + + // check allowed types + err = validateObjects(innerBlock.List, listTags(target, "hcl")) + if err != nil { + return nil, err + } + + backends[name] = target + } + + return backends, nil +} + // Load loads a config from a file. func Load(filename string) (Config, error) { buf, err := ioutil.ReadFile(filename) diff --git a/internal/ui/config/testdata/all.golden b/internal/ui/config/testdata/all.golden index 967451307..5c2de7b42 100644 --- a/internal/ui/config/testdata/all.golden +++ b/internal/ui/config/testdata/all.golden @@ -2,7 +2,7 @@ "Repo": "sftp:user@server:/srv/repo", "Password": "secret", "PasswordFile": "/root/secret.txt", - "Backends": null, + "Backends": {}, "Backup": { "Target": [ "/home/user/", diff --git a/internal/ui/config/testdata/backend-local.conf b/internal/ui/config/testdata/backend-local.conf index 008df90ca..cedc8788b 100644 --- a/internal/ui/config/testdata/backend-local.conf +++ b/internal/ui/config/testdata/backend-local.conf @@ -1,3 +1,5 @@ +password = "geheim" + backend "foo" { type = "local" path = "/srv/data/repo" diff --git a/internal/ui/config/testdata/backend-local.golden b/internal/ui/config/testdata/backend-local.golden index 27ec770c5..383c03bef 100644 --- a/internal/ui/config/testdata/backend-local.golden +++ b/internal/ui/config/testdata/backend-local.golden @@ -1,13 +1,15 @@ { "Repo": "", - "Password": "", + "Password": "geheim", "PasswordFile": "", "Backends": { "bar": { - "Type": "" + "Type": "local", + "Path": "/srv/data/repo" }, "foo": { - "Type": "local" + "Type": "local", + "Path": "/srv/data/repo" } }, "Backup": { diff --git a/internal/ui/config/testdata/backup.golden b/internal/ui/config/testdata/backup.golden index 7ae6e09ea..d2a487aa2 100644 --- a/internal/ui/config/testdata/backup.golden +++ b/internal/ui/config/testdata/backup.golden @@ -2,7 +2,7 @@ "Repo": "", "Password": "", "PasswordFile": "", - "Backends": null, + "Backends": {}, "Backup": { "Target": [ "foo", diff --git a/internal/ui/config/testdata/repo_local.golden b/internal/ui/config/testdata/repo_local.golden index fb93b985f..9adfddec2 100644 --- a/internal/ui/config/testdata/repo_local.golden +++ b/internal/ui/config/testdata/repo_local.golden @@ -4,7 +4,7 @@ "PasswordFile": "", "Backends": { "test": { - "Backend": "local", + "Type": "", "Path": "/foo/bar/baz" } },