forked from TrueCloudLab/restic
wip
This commit is contained in:
parent
722517c480
commit
aaef54559a
10 changed files with 224 additions and 42 deletions
|
@ -30,6 +30,7 @@ import (
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
"github.com/restic/restic/internal/textfile"
|
"github.com/restic/restic/internal/textfile"
|
||||||
|
"github.com/restic/restic/internal/ui/config"
|
||||||
|
|
||||||
"github.com/restic/restic/internal/errors"
|
"github.com/restic/restic/internal/errors"
|
||||||
|
|
||||||
|
@ -40,7 +41,8 @@ var version = "compiled manually"
|
||||||
|
|
||||||
// GlobalOptions hold all global options for restic.
|
// GlobalOptions hold all global options for restic.
|
||||||
type GlobalOptions struct {
|
type GlobalOptions struct {
|
||||||
Repo string
|
config.Config
|
||||||
|
|
||||||
PasswordFile string
|
PasswordFile string
|
||||||
Quiet bool
|
Quiet bool
|
||||||
Verbose int
|
Verbose int
|
||||||
|
@ -86,7 +88,10 @@ func init() {
|
||||||
})
|
})
|
||||||
|
|
||||||
f := cmdRoot.PersistentFlags()
|
f := cmdRoot.PersistentFlags()
|
||||||
f.StringVarP(&globalOptions.Repo, "repo", "r", os.Getenv("RESTIC_REPOSITORY"), "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
|
||||||
|
// these fields are embedded in config.Config and queried via f.Get[...]()
|
||||||
|
f.StringP("repo", "r", "", "repository to backup to or restore from (default: $RESTIC_REPOSITORY)")
|
||||||
|
|
||||||
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
|
f.StringVarP(&globalOptions.PasswordFile, "password-file", "p", os.Getenv("RESTIC_PASSWORD_FILE"), "read the repository password from a file (default: $RESTIC_PASSWORD_FILE)")
|
||||||
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
|
f.BoolVarP(&globalOptions.Quiet, "quiet", "q", false, "do not output comprehensive progress report")
|
||||||
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
|
f.CountVarP(&globalOptions.Verbose, "verbose", "v", "be verbose (specify --verbose multiple times or level `n`)")
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"github.com/restic/restic/internal/repository"
|
"github.com/restic/restic/internal/repository"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
rtest "github.com/restic/restic/internal/test"
|
rtest "github.com/restic/restic/internal/test"
|
||||||
|
"github.com/restic/restic/internal/ui/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type dirEntry struct {
|
type dirEntry struct {
|
||||||
|
@ -209,7 +210,9 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
|
||||||
rtest.OK(t, os.MkdirAll(env.repo, 0700))
|
rtest.OK(t, os.MkdirAll(env.repo, 0700))
|
||||||
|
|
||||||
env.gopts = GlobalOptions{
|
env.gopts = GlobalOptions{
|
||||||
Repo: env.repo,
|
Config: config.Config{
|
||||||
|
Repo: env.repo,
|
||||||
|
},
|
||||||
Quiet: true,
|
Quiet: true,
|
||||||
CacheDir: env.cache,
|
CacheDir: env.cache,
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/restic/restic/internal/debug"
|
"github.com/restic/restic/internal/debug"
|
||||||
"github.com/restic/restic/internal/options"
|
"github.com/restic/restic/internal/options"
|
||||||
"github.com/restic/restic/internal/restic"
|
"github.com/restic/restic/internal/restic"
|
||||||
|
"github.com/restic/restic/internal/ui/config"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
|
@ -29,7 +30,22 @@ directories in an encrypted repository stored on different backends.
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
|
|
||||||
PersistentPreRunE: func(c *cobra.Command, args []string) error {
|
PersistentPreRunE: func(c *cobra.Command, args []string) (err error) {
|
||||||
|
globalOptions.Config, err = config.Load("restic.conf")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = config.ApplyEnv(&globalOptions.Config, os.Environ())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = config.ApplyFlags(&globalOptions.Config, c.Flags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// set verbosity, default is one
|
// set verbosity, default is one
|
||||||
globalOptions.verbosity = 1
|
globalOptions.verbosity = 1
|
||||||
if globalOptions.Quiet && (globalOptions.Verbose > 1) {
|
if globalOptions.Quiet && (globalOptions.Verbose > 1) {
|
||||||
|
|
|
@ -2,25 +2,34 @@ package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/davecgh/go-spew/spew"
|
|
||||||
"github.com/hashicorp/hcl"
|
"github.com/hashicorp/hcl"
|
||||||
"github.com/hashicorp/hcl/hcl/ast"
|
"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/restic/restic/internal/errors"
|
||||||
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Repo is a configured repository
|
// Config contains configuration items read from a file.
|
||||||
type Repo struct {
|
type Config struct {
|
||||||
|
Repo string `hcl:"repo" flag:"repo" env:"RESTIC_REPOSITORY"`
|
||||||
|
|
||||||
|
Backends map[string]Backend `hcl:"backend"`
|
||||||
|
Backup *Backup `hcl:"backup"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backend is a configured backend to store a repository.
|
||||||
|
type Backend struct {
|
||||||
Backend string
|
Backend string
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config contains configuration items read from a file.
|
// Backup sets the options for the "backup" command.
|
||||||
type Config struct {
|
type Backup struct {
|
||||||
Quiet bool `hcl:"quiet"`
|
Target []string `hcl:"target"`
|
||||||
Repos map[string]Repo `hcl:"repo"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// listTags returns the all the top-level tags with the name tagname of obj.
|
// listTags returns the all the top-level tags with the name tagname of obj.
|
||||||
|
@ -40,6 +49,18 @@ func listTags(obj interface{}, tagname string) map[string]struct{} {
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateObjects(list *ast.ObjectList, validNames map[string]struct{}) error {
|
||||||
|
for _, item := range list.Items {
|
||||||
|
ident := item.Keys[0].Token.Value().(string)
|
||||||
|
if _, ok := validNames[ident]; !ok {
|
||||||
|
return errors.Errorf("unknown option %q found at line %v, column %v",
|
||||||
|
ident, item.Pos().Line, item.Pos().Column)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Parse parses a config file from buf.
|
// Parse parses a config file from buf.
|
||||||
func Parse(buf []byte) (cfg Config, err error) {
|
func Parse(buf []byte) (cfg Config, err error) {
|
||||||
parsed, err := hcl.ParseBytes(buf)
|
parsed, err := hcl.ParseBytes(buf)
|
||||||
|
@ -52,25 +73,154 @@ func Parse(buf []byte) (cfg Config, err error) {
|
||||||
return Config{}, err
|
return Config{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for additional top-level items
|
// check for additional unknown items
|
||||||
validNames := listTags(cfg, "hcl")
|
root := parsed.Node.(*ast.ObjectList)
|
||||||
for _, item := range parsed.Node.(*ast.ObjectList).Items {
|
|
||||||
fmt.Printf("-----------\n")
|
|
||||||
spew.Dump(item)
|
|
||||||
var ident string
|
|
||||||
for _, key := range item.Keys {
|
|
||||||
if key.Token.Type == token.IDENT {
|
|
||||||
ident = key.Token.Text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Printf("ident is %q\n", ident)
|
|
||||||
|
|
||||||
if _, ok := validNames[ident]; !ok {
|
checks := map[string]map[string]struct{}{
|
||||||
return Config{}, errors.Errorf("unknown option %q found at line %v, column %v: %v",
|
"": listTags(cfg, "hcl"),
|
||||||
ident, item.Pos().Line, item.Pos().Column)
|
"backup": listTags(Backup{}, "hcl"),
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, valid := range checks {
|
||||||
|
list := root
|
||||||
|
if name != "" {
|
||||||
|
if len(root.Filter(name).Items) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val := root.Filter(name).Items[0].Val
|
||||||
|
obj, ok := val.(*ast.ObjectType)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return Config{}, errors.Errorf("error in line %v, column %v: %q must be an object", val.Pos().Line, val.Pos().Column, name)
|
||||||
|
}
|
||||||
|
list = obj.List
|
||||||
|
}
|
||||||
|
|
||||||
|
err = validateObjects(list, valid)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// spew.Dump(cfg)
|
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load loads a config from a file.
|
||||||
|
func Load(filename string) (Config, error) {
|
||||||
|
buf, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return Parse(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getFieldsForTag(tagname string, target interface{}) map[string]reflect.Value {
|
||||||
|
v := reflect.ValueOf(target).Elem()
|
||||||
|
// resolve indirection
|
||||||
|
vi := reflect.Indirect(reflect.ValueOf(target))
|
||||||
|
|
||||||
|
attr := make(map[string]reflect.Value)
|
||||||
|
for i := 0; i < vi.NumField(); i++ {
|
||||||
|
typeField := vi.Type().Field(i)
|
||||||
|
tag := typeField.Tag.Get(tagname)
|
||||||
|
if tag == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
field := v.FieldByName(typeField.Name)
|
||||||
|
|
||||||
|
if !field.CanSet() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attr[tag] = field
|
||||||
|
}
|
||||||
|
|
||||||
|
return attr
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyFlags takes the values from the flag set and applies them to cfg.
|
||||||
|
func ApplyFlags(cfg interface{}, fset *pflag.FlagSet) error {
|
||||||
|
if reflect.TypeOf(cfg).Kind() != reflect.Ptr {
|
||||||
|
panic("target config is not a pointer")
|
||||||
|
}
|
||||||
|
|
||||||
|
attr := getFieldsForTag("flag", cfg)
|
||||||
|
|
||||||
|
var visitError error
|
||||||
|
fset.VisitAll(func(flag *pflag.Flag) {
|
||||||
|
if visitError != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
field, ok := attr[flag.Name]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !flag.Changed {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log("apply flag %v, to field %v\n", flag.Name, field.Type().Name())
|
||||||
|
|
||||||
|
switch flag.Value.Type() {
|
||||||
|
case "count":
|
||||||
|
v, err := fset.GetCount(flag.Name)
|
||||||
|
if err != nil {
|
||||||
|
visitError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
field.SetUint(uint64(v))
|
||||||
|
case "bool":
|
||||||
|
v, err := fset.GetBool(flag.Name)
|
||||||
|
if err != nil {
|
||||||
|
visitError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
field.SetBool(v)
|
||||||
|
case "string":
|
||||||
|
v, err := fset.GetString(flag.Name)
|
||||||
|
if err != nil {
|
||||||
|
visitError = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
field.SetString(v)
|
||||||
|
default:
|
||||||
|
visitError = errors.Errorf("flag %v has unknown type %v", flag.Name, flag.Value.Type())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return visitError
|
||||||
|
}
|
||||||
|
|
||||||
|
// ApplyEnv takes the list of environment variables and applies them to the
|
||||||
|
// config.
|
||||||
|
func ApplyEnv(cfg interface{}, env []string) error {
|
||||||
|
attr := getFieldsForTag("env", cfg)
|
||||||
|
|
||||||
|
for _, s := range env {
|
||||||
|
data := strings.SplitN(s, "=", 2)
|
||||||
|
if len(data) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name, value := data[0], data[1]
|
||||||
|
field, ok := attr[name]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Kind() != reflect.String {
|
||||||
|
panic(fmt.Sprintf("unsupported field type %v", field.Kind()))
|
||||||
|
}
|
||||||
|
|
||||||
|
debug.Log("apply env %v (%q) to %v\n", name, value, field.Type().Name())
|
||||||
|
field.SetString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
6
internal/ui/config/testdata/backup.conf
vendored
Normal file
6
internal/ui/config/testdata/backup.conf
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
backup {
|
||||||
|
target = [
|
||||||
|
"foo",
|
||||||
|
"/home/user",
|
||||||
|
]
|
||||||
|
}
|
10
internal/ui/config/testdata/backup.golden
vendored
Normal file
10
internal/ui/config/testdata/backup.golden
vendored
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"Backends": null,
|
||||||
|
"Repo": "",
|
||||||
|
"Backup": {
|
||||||
|
"Target": [
|
||||||
|
"foo",
|
||||||
|
"/home/user"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
1
internal/ui/config/testdata/quiet.conf
vendored
1
internal/ui/config/testdata/quiet.conf
vendored
|
@ -1 +0,0 @@
|
||||||
quiet = true
|
|
4
internal/ui/config/testdata/quiet.golden
vendored
4
internal/ui/config/testdata/quiet.golden
vendored
|
@ -1,4 +0,0 @@
|
||||||
{
|
|
||||||
"Quiet": true,
|
|
||||||
"Repos": null
|
|
||||||
}
|
|
8
internal/ui/config/testdata/repo_local.conf
vendored
8
internal/ui/config/testdata/repo_local.conf
vendored
|
@ -1,10 +1,6 @@
|
||||||
repo "test" {
|
backend "test" {
|
||||||
backend = "local"
|
backend = "local"
|
||||||
path = "/foo/bar/baz"
|
path = "/foo/bar/baz"
|
||||||
}
|
}
|
||||||
|
|
||||||
foobar "test" {
|
repo = "test"
|
||||||
x = "y"
|
|
||||||
}
|
|
||||||
|
|
||||||
quiet = false
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
{
|
{
|
||||||
"Quiet": false,
|
"Backends": {
|
||||||
"Repos": {
|
|
||||||
"test": {
|
"test": {
|
||||||
"Backend": "local",
|
"Backend": "local",
|
||||||
"Path": "/foo/bar/baz"
|
"Path": "/foo/bar/baz"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"Repo": "test",
|
||||||
|
"Backup": null
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue