Merge pull request #897 from restic/add-extended-options

Add extended options
This commit is contained in:
Alexander Neumann 2017-03-26 10:53:36 +02:00
commit 6935f82389
12 changed files with 702 additions and 202 deletions

View file

@ -27,7 +27,7 @@ func runInit(gopts GlobalOptions, args []string) error {
return errors.Fatal("Please specify repository location (-r)") return errors.Fatal("Please specify repository location (-r)")
} }
be, err := create(gopts.Repo) be, err := create(gopts.Repo, gopts.extended)
if err != nil { if err != nil {
return errors.Fatalf("create backend at %s failed: %v\n", gopts.Repo, err) return errors.Fatalf("create backend at %s failed: %v\n", gopts.Repo, err)
} }

View file

@ -12,11 +12,12 @@ import (
"syscall" "syscall"
"restic/backend/local" "restic/backend/local"
"restic/backend/location"
"restic/backend/rest" "restic/backend/rest"
"restic/backend/s3" "restic/backend/s3"
"restic/backend/sftp" "restic/backend/sftp"
"restic/debug" "restic/debug"
"restic/location" "restic/options"
"restic/repository" "restic/repository"
"restic/errors" "restic/errors"
@ -38,6 +39,10 @@ type GlobalOptions struct {
password string password string
stdout io.Writer stdout io.Writer
stderr io.Writer stderr io.Writer
Options []string
extended options.Options
} }
var globalOptions = GlobalOptions{ var globalOptions = GlobalOptions{
@ -65,6 +70,8 @@ func init() {
f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos") f.BoolVar(&globalOptions.NoLock, "no-lock", false, "do not lock the repo, this allows some operations on read-only repos")
f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it") f.BoolVarP(&globalOptions.JSON, "json", "", false, "set output mode to JSON for commands that support it")
f.StringSliceVarP(&globalOptions.Options, "option", "o", []string{}, "set extended option (`key=value`, can be specified multiple times)")
restoreTerminal() restoreTerminal()
} }
@ -287,7 +294,7 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
return nil, errors.Fatal("Please specify repository location (-r)") return nil, errors.Fatal("Please specify repository location (-r)")
} }
be, err := open(opts.Repo) be, err := open(opts.Repo, opts.extended)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -309,8 +316,61 @@ func OpenRepository(opts GlobalOptions) (*repository.Repository, error) {
return s, nil return s, nil
} }
func parseConfig(loc location.Location, opts options.Options) (interface{}, error) {
// only apply options for a particular backend here
opts = opts.Extract(loc.Scheme)
switch loc.Scheme {
case "local":
cfg := loc.Config.(local.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening local repository at %#v", cfg)
return cfg, nil
case "sftp":
cfg := loc.Config.(sftp.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening sftp repository at %#v", cfg)
return cfg, nil
case "s3":
cfg := loc.Config.(s3.Config)
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
}
if cfg.Secret == "" {
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening s3 repository at %#v", cfg)
return cfg, nil
case "rest":
cfg := loc.Config.(rest.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening rest repository at %#v", cfg)
return cfg, nil
}
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
}
// Open the backend specified by a location config. // Open the backend specified by a location config.
func open(s string) (restic.Backend, error) { func open(s string, opts options.Options) (restic.Backend, error) {
debug.Log("parsing location %v", s) debug.Log("parsing location %v", s)
loc, err := location.Parse(s) loc, err := location.Parse(s)
if err != nil { if err != nil {
@ -319,27 +379,21 @@ func open(s string) (restic.Backend, error) {
var be restic.Backend var be restic.Backend
cfg, err := parseConfig(loc, opts)
if err != nil {
return nil, err
}
switch loc.Scheme { switch loc.Scheme {
case "local": case "local":
debug.Log("opening local repository at %#v", loc.Config) be, err = local.Open(cfg.(local.Config))
be, err = local.Open(loc.Config.(string))
case "sftp": case "sftp":
debug.Log("opening sftp repository at %#v", loc.Config) be, err = sftp.OpenWithConfig(cfg.(sftp.Config))
be, err = sftp.OpenWithConfig(loc.Config.(sftp.Config))
case "s3": case "s3":
cfg := loc.Config.(s3.Config) be, err = s3.Open(cfg.(s3.Config))
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
}
if cfg.Secret == "" {
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
debug.Log("opening s3 repository at %#v", cfg)
be, err = s3.Open(cfg)
case "rest": case "rest":
be, err = rest.Open(loc.Config.(rest.Config)) be, err = rest.Open(cfg.(rest.Config))
default: default:
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme) return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
} }
@ -352,34 +406,27 @@ func open(s string) (restic.Backend, error) {
} }
// Create the backend specified by URI. // Create the backend specified by URI.
func create(s string) (restic.Backend, error) { func create(s string, opts options.Options) (restic.Backend, error) {
debug.Log("parsing location %v", s) debug.Log("parsing location %v", s)
loc, err := location.Parse(s) loc, err := location.Parse(s)
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg, err := parseConfig(loc, opts)
if err != nil {
return nil, err
}
switch loc.Scheme { switch loc.Scheme {
case "local": case "local":
debug.Log("create local repository at %#v", loc.Config) return local.Create(cfg.(local.Config))
return local.Create(loc.Config.(string))
case "sftp": case "sftp":
debug.Log("create sftp repository at %#v", loc.Config) return sftp.CreateWithConfig(cfg.(sftp.Config))
return sftp.CreateWithConfig(loc.Config.(sftp.Config))
case "s3": case "s3":
cfg := loc.Config.(s3.Config) return s3.Open(cfg.(s3.Config))
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv("AWS_ACCESS_KEY_ID")
}
if cfg.Secret == "" {
cfg.Secret = os.Getenv("AWS_SECRET_ACCESS_KEY")
}
debug.Log("create s3 repository at %#v", loc.Config)
return s3.Open(cfg)
case "rest": case "rest":
return rest.Create(loc.Config.(rest.Config)) return rest.Create(cfg.(rest.Config))
} }
debug.Log("invalid repository scheme: %v", s) debug.Log("invalid repository scheme: %v", s)

View file

@ -5,6 +5,7 @@ import (
"os" "os"
"restic" "restic"
"restic/debug" "restic/debug"
"restic/options"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -22,10 +23,21 @@ directories in an encrypted repository stored on different backends.
SilenceErrors: true, SilenceErrors: true,
SilenceUsage: true, SilenceUsage: true,
// run the debug functions for all subcommands (if build tag "debug" is
// enabled)
PersistentPreRunE: func(*cobra.Command, []string) error { PersistentPreRunE: func(*cobra.Command, []string) error {
return runDebug() // parse extended options
opts, err := options.Parse(globalOptions.Options)
if err != nil {
return err
}
globalOptions.extended = opts
// run the debug functions for all subcommands (if build tag "debug" is
// enabled)
if err := runDebug(); err != nil {
return err
}
return nil
}, },
PersistentPostRun: func(*cobra.Command, []string) { PersistentPostRun: func(*cobra.Command, []string) {
shutdownDebug() shutdownDebug()

View file

@ -6,11 +6,16 @@ import (
"restic/errors" "restic/errors"
) )
// Config holds all information needed to open a local repository.
type Config struct {
Path string
}
// ParseConfig parses a local backend config. // ParseConfig parses a local backend config.
func ParseConfig(cfg string) (interface{}, error) { func ParseConfig(cfg string) (interface{}, error) {
if !strings.HasPrefix(cfg, "local:") { if !strings.HasPrefix(cfg, "local:") {
return nil, errors.New(`invalid format, prefix "local" not found`) return nil, errors.New(`invalid format, prefix "local" not found`)
} }
return cfg[6:], nil return Config{Path: cfg[6:]}, nil
} }

View file

@ -16,7 +16,7 @@ import (
// Local is a backend in a local directory. // Local is a backend in a local directory.
type Local struct { type Local struct {
p string Config
} }
var _ restic.Backend = &Local{} var _ restic.Backend = &Local{}
@ -34,28 +34,28 @@ func paths(dir string) []string {
} }
// Open opens the local backend as specified by config. // Open opens the local backend as specified by config.
func Open(dir string) (*Local, error) { func Open(cfg Config) (*Local, error) {
// test if all necessary dirs are there // test if all necessary dirs are there
for _, d := range paths(dir) { for _, d := range paths(cfg.Path) {
if _, err := fs.Stat(d); err != nil { if _, err := fs.Stat(d); err != nil {
return nil, errors.Wrap(err, "Open") return nil, errors.Wrap(err, "Open")
} }
} }
return &Local{p: dir}, nil return &Local{Config: cfg}, nil
} }
// Create creates all the necessary files and directories for a new local // Create creates all the necessary files and directories for a new local
// backend at dir. Afterwards a new config blob should be created. // backend at dir. Afterwards a new config blob should be created.
func Create(dir string) (*Local, error) { func Create(cfg Config) (*Local, error) {
// test if config file already exists // test if config file already exists
_, err := fs.Lstat(filepath.Join(dir, backend.Paths.Config)) _, err := fs.Lstat(filepath.Join(cfg.Path, backend.Paths.Config))
if err == nil { if err == nil {
return nil, errors.New("config file already exists") return nil, errors.New("config file already exists")
} }
// create paths for data, refs and temp // create paths for data, refs and temp
for _, d := range paths(dir) { for _, d := range paths(cfg.Path) {
err := fs.MkdirAll(d, backend.Modes.Dir) err := fs.MkdirAll(d, backend.Modes.Dir)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "MkdirAll") return nil, errors.Wrap(err, "MkdirAll")
@ -63,12 +63,12 @@ func Create(dir string) (*Local, error) {
} }
// open backend // open backend
return Open(dir) return Open(cfg)
} }
// Location returns this backend's location (the directory name). // Location returns this backend's location (the directory name).
func (b *Local) Location() string { func (b *Local) Location() string {
return b.p return b.Path
} }
// Construct path for given Type and name. // Construct path for given Type and name.
@ -132,13 +132,13 @@ func (b *Local) Save(h restic.Handle, rd io.Reader) (err error) {
return err return err
} }
tmpfile, err := copyToTempfile(filepath.Join(b.p, backend.Paths.Temp), rd) tmpfile, err := copyToTempfile(filepath.Join(b.Path, backend.Paths.Temp), rd)
debug.Log("saved %v to %v", h, tmpfile) debug.Log("saved %v to %v", h, tmpfile)
if err != nil { if err != nil {
return err return err
} }
filename := filename(b.p, h.Type, h.Name) filename := filename(b.Path, h.Type, h.Name)
// test if new path already exists // test if new path already exists
if _, err := fs.Stat(filename); err == nil { if _, err := fs.Stat(filename); err == nil {
@ -183,7 +183,7 @@ func (b *Local) Load(h restic.Handle, length int, offset int64) (io.ReadCloser,
return nil, errors.New("offset is negative") return nil, errors.New("offset is negative")
} }
f, err := os.Open(filename(b.p, h.Type, h.Name)) f, err := os.Open(filename(b.Path, h.Type, h.Name))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -210,7 +210,7 @@ func (b *Local) Stat(h restic.Handle) (restic.FileInfo, error) {
return restic.FileInfo{}, err return restic.FileInfo{}, err
} }
fi, err := fs.Stat(filename(b.p, h.Type, h.Name)) fi, err := fs.Stat(filename(b.Path, h.Type, h.Name))
if err != nil { if err != nil {
return restic.FileInfo{}, errors.Wrap(err, "Stat") return restic.FileInfo{}, errors.Wrap(err, "Stat")
} }
@ -221,7 +221,7 @@ func (b *Local) Stat(h restic.Handle) (restic.FileInfo, error) {
// Test returns true if a blob of the given type and name exists in the backend. // Test returns true if a blob of the given type and name exists in the backend.
func (b *Local) Test(h restic.Handle) (bool, error) { func (b *Local) Test(h restic.Handle) (bool, error) {
debug.Log("Test %v", h) debug.Log("Test %v", h)
_, err := fs.Stat(filename(b.p, h.Type, h.Name)) _, err := fs.Stat(filename(b.Path, h.Type, h.Name))
if err != nil { if err != nil {
if os.IsNotExist(errors.Cause(err)) { if os.IsNotExist(errors.Cause(err)) {
return false, nil return false, nil
@ -235,7 +235,7 @@ func (b *Local) Test(h restic.Handle) (bool, error) {
// Remove removes the blob with the given name and type. // Remove removes the blob with the given name and type.
func (b *Local) Remove(h restic.Handle) error { func (b *Local) Remove(h restic.Handle) error {
debug.Log("Remove %v", h) debug.Log("Remove %v", h)
fn := filename(b.p, h.Type, h.Name) fn := filename(b.Path, h.Type, h.Name)
// reset read-only flag // reset read-only flag
err := fs.Chmod(fn, 0666) err := fs.Chmod(fn, 0666)
@ -316,7 +316,7 @@ func (b *Local) List(t restic.FileType, done <-chan struct{}) <-chan string {
} }
ch := make(chan string) ch := make(chan string)
items, err := lister(filepath.Join(dirname(b.p, t, ""))) items, err := lister(filepath.Join(dirname(b.Path, t, "")))
if err != nil { if err != nil {
close(ch) close(ch)
return ch return ch
@ -343,7 +343,7 @@ func (b *Local) List(t restic.FileType, done <-chan struct{}) <-chan string {
// Delete removes the repository and all files. // Delete removes the repository and all files.
func (b *Local) Delete() error { func (b *Local) Delete() error {
debug.Log("Delete()") debug.Log("Delete()")
return fs.RemoveAll(b.p) return fs.RemoveAll(b.Path)
} }
// Close closes all open files. // Close closes all open files.

View file

@ -35,7 +35,7 @@ func init() {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return local.Create(tempBackendDir) return local.Create(local.Config{Path: tempBackendDir})
} }
test.OpenFn = func() (restic.Backend, error) { test.OpenFn = func() (restic.Backend, error) {
@ -43,7 +43,7 @@ func init() {
if err != nil { if err != nil {
return nil, err return nil, err
} }
return local.Open(tempBackendDir) return local.Open(local.Config{Path: tempBackendDir})
} }
test.CleanupFn = func() error { test.CleanupFn = func() error {

View file

@ -0,0 +1,227 @@
package location
import (
"net/url"
"reflect"
"testing"
"restic/backend/local"
"restic/backend/rest"
"restic/backend/s3"
"restic/backend/sftp"
)
func parseURL(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}
var parseTests = []struct {
s string
u Location
}{
{
"local:/srv/repo",
Location{Scheme: "local",
Config: local.Config{
Path: "/srv/repo",
},
},
},
{
"local:dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Path: "dir1/dir2",
},
},
},
{
"local:dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Path: "dir1/dir2",
},
},
},
{
"dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Path: "dir1/dir2",
},
},
},
{
"local:../dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Path: "../dir1/dir2",
},
},
},
{
"/dir1/dir2",
Location{Scheme: "local",
Config: local.Config{
Path: "/dir1/dir2",
},
},
},
{
"sftp:user@host:/srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
User: "user",
Host: "host",
Dir: "/srv/repo",
},
},
},
{
"sftp:host:/srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
User: "",
Host: "host",
Dir: "/srv/repo",
},
},
},
{
"sftp://user@host/srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
User: "user",
Host: "host",
Dir: "srv/repo",
},
},
},
{
"sftp://user@host//srv/repo",
Location{Scheme: "sftp",
Config: sftp.Config{
User: "user",
Host: "host",
Dir: "/srv/repo",
},
},
},
{
"s3://eu-central-1/bucketname",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
},
},
},
{
"s3://hostname.foo/bucketname",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "restic",
},
},
},
{
"s3://hostname.foo/bucketname/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "prefix/directory",
},
},
},
{
"s3:eu-central-1/repo",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "restic",
},
},
},
{
"s3:eu-central-1/repo/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "prefix/directory",
},
},
},
{
"s3:https://hostname.foo/repo",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
},
},
},
{
"s3:https://hostname.foo/repo/prefix/directory",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "prefix/directory",
},
},
},
{
"s3:http://hostname.foo/repo",
Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
UseHTTP: true,
},
},
},
{
"rest:http://hostname.foo:1234/",
Location{Scheme: "rest",
Config: rest.Config{
URL: parseURL("http://hostname.foo:1234/"),
},
},
},
}
func TestParse(t *testing.T) {
for i, test := range parseTests {
t.Run(test.s, func(t *testing.T) {
u, err := Parse(test.s)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if test.u.Scheme != u.Scheme {
t.Errorf("test %d: scheme does not match, want %q, got %q",
i, test.u.Scheme, u.Scheme)
}
if !reflect.DeepEqual(test.u.Config, u.Config) {
t.Errorf("test %d: cfg map does not match, want:\n %#v\ngot: \n %#v",
i, test.u.Config, u.Config)
}
})
}
}

View file

@ -1,140 +0,0 @@
package location
import (
"net/url"
"reflect"
"testing"
"restic/backend/rest"
"restic/backend/s3"
"restic/backend/sftp"
)
func parseURL(s string) *url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return u
}
var parseTests = []struct {
s string
u Location
}{
{"local:/srv/repo", Location{Scheme: "local", Config: "/srv/repo"}},
{"local:dir1/dir2", Location{Scheme: "local", Config: "dir1/dir2"}},
{"local:dir1/dir2", Location{Scheme: "local", Config: "dir1/dir2"}},
{"dir1/dir2", Location{Scheme: "local", Config: "dir1/dir2"}},
{"local:../dir1/dir2", Location{Scheme: "local", Config: "../dir1/dir2"}},
{"/dir1/dir2", Location{Scheme: "local", Config: "/dir1/dir2"}},
{"sftp:user@host:/srv/repo", Location{Scheme: "sftp",
Config: sftp.Config{
User: "user",
Host: "host",
Dir: "/srv/repo",
}}},
{"sftp:host:/srv/repo", Location{Scheme: "sftp",
Config: sftp.Config{
User: "",
Host: "host",
Dir: "/srv/repo",
}}},
{"sftp://user@host/srv/repo", Location{Scheme: "sftp",
Config: sftp.Config{
User: "user",
Host: "host",
Dir: "srv/repo",
}}},
{"sftp://user@host//srv/repo", Location{Scheme: "sftp",
Config: sftp.Config{
User: "user",
Host: "host",
Dir: "/srv/repo",
}}},
{"s3://eu-central-1/bucketname", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "bucketname",
Prefix: "restic",
}},
},
{"s3://hostname.foo/bucketname", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "restic",
}},
},
{"s3://hostname.foo/bucketname/prefix/directory", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "bucketname",
Prefix: "prefix/directory",
}},
},
{"s3:eu-central-1/repo", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "restic",
}},
},
{"s3:eu-central-1/repo/prefix/directory", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "eu-central-1",
Bucket: "repo",
Prefix: "prefix/directory",
}},
},
{"s3:https://hostname.foo/repo", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
}},
},
{"s3:https://hostname.foo/repo/prefix/directory", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "prefix/directory",
}},
},
{"s3:http://hostname.foo/repo", Location{Scheme: "s3",
Config: s3.Config{
Endpoint: "hostname.foo",
Bucket: "repo",
Prefix: "restic",
UseHTTP: true,
}},
},
{"rest:http://hostname.foo:1234/", Location{Scheme: "rest",
Config: rest.Config{
URL: parseURL("http://hostname.foo:1234/"),
}},
},
}
func TestParse(t *testing.T) {
for i, test := range parseTests {
u, err := Parse(test.s)
if err != nil {
t.Errorf("unexpected error: %v", err)
continue
}
if test.u.Scheme != u.Scheme {
t.Errorf("test %d: scheme does not match, want %q, got %q",
i, test.u.Scheme, u.Scheme)
}
if !reflect.DeepEqual(test.u.Config, u.Config) {
t.Errorf("test %d: cfg map does not match, want:\n %#v\ngot: \n %#v",
i, test.u.Config, u.Config)
}
}
}

View file

@ -0,0 +1,129 @@
package options
import (
"reflect"
"restic/errors"
"strconv"
"strings"
"time"
)
// Options holds options in the form key=value.
type Options map[string]string
// splitKeyValue splits at the first equals (=) sign.
func splitKeyValue(s string) (key string, value string) {
data := strings.SplitN(s, "=", 2)
key = strings.ToLower(strings.TrimSpace(data[0]))
if len(data) == 1 {
// no equals sign is treated as the empty value
return key, ""
}
return key, strings.TrimSpace(data[1])
}
// Parse takes a slice of key=value pairs and returns an Options type.
// The key may include namespaces, separated by dots. Example: "foo.bar=value".
// Keys are converted to lower-case.
func Parse(in []string) (Options, error) {
opts := make(Options, len(in))
for _, opt := range in {
key, value := splitKeyValue(opt)
if key == "" {
return Options{}, errors.Fatalf("empty key is not a valid option")
}
if v, ok := opts[key]; ok && v != value {
return Options{}, errors.Fatalf("key %q present more than once", key)
}
opts[key] = value
}
return opts, nil
}
// Extract returns an Options type with all keys in namespace ns, which is
// also stripped from the keys. ns must end with a dot.
func (o Options) Extract(ns string) Options {
l := len(ns)
if ns[l-1] != '.' {
ns += "."
l++
}
opts := make(Options)
for k, v := range o {
if !strings.HasPrefix(k, ns) {
continue
}
opts[k[l:]] = v
}
return opts
}
// Apply sets the options on dst via reflection, using the struct tag `option`.
// The namespace argument (ns) is only used for error messages.
func (o Options) Apply(ns string, dst interface{}) error {
v := reflect.ValueOf(dst).Elem()
fields := make(map[string]reflect.StructField)
for i := 0; i < v.NumField(); i++ {
f := v.Type().Field(i)
tag := f.Tag.Get("option")
if tag == "" {
continue
}
if _, ok := fields[tag]; ok {
panic("option tag " + tag + " is not unique in " + v.Type().Name())
}
fields[tag] = f
}
for key, value := range o {
field, ok := fields[key]
if !ok {
if ns != "" {
key = ns + "." + key
}
return errors.Fatalf("option %v is not known", key)
}
i := field.Index[0]
switch v.Type().Field(i).Type.Name() {
case "string":
v.Field(i).SetString(value)
case "int":
vi, err := strconv.ParseInt(value, 0, 32)
if err != nil {
return err
}
v.Field(i).SetInt(vi)
case "Duration":
d, err := time.ParseDuration(value)
if err != nil {
return err
}
v.Field(i).SetInt(int64(d))
default:
panic("type " + v.Type().Field(i).Type.Name() + " not handled")
}
}
return nil
}

View file

@ -0,0 +1,220 @@
package options
import (
"fmt"
"reflect"
"testing"
"time"
)
var optsTests = []struct {
input []string
output Options
}{
{
[]string{"foo=bar", "bar=baz ", "k="},
Options{
"foo": "bar",
"bar": "baz",
"k": "",
},
},
{
[]string{"Foo=23", "baR", "k=thing with spaces"},
Options{
"foo": "23",
"bar": "",
"k": "thing with spaces",
},
},
{
[]string{"k=thing with spaces", "k2=more spaces = not evil"},
Options{
"k": "thing with spaces",
"k2": "more spaces = not evil",
},
},
{
[]string{"x=1", "foo=bar", "y=2", "foo=bar"},
Options{
"x": "1",
"y": "2",
"foo": "bar",
},
},
}
func TestParseOptions(t *testing.T) {
for i, test := range optsTests {
t.Run(fmt.Sprintf("test-%v", i), func(t *testing.T) {
opts, err := Parse(test.input)
if err != nil {
t.Fatalf("unable to parse options: %v", err)
}
if !reflect.DeepEqual(opts, test.output) {
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, opts)
}
})
}
}
var invalidOptsTests = []struct {
input []string
err string
}{
{
[]string{"=bar", "bar=baz", "k="},
"empty key is not a valid option",
},
{
[]string{"x=1", "foo=bar", "y=2", "foo=baz"},
`key "foo" present more than once`,
},
}
func TestParseInvalidOptions(t *testing.T) {
for _, test := range invalidOptsTests {
t.Run(test.err, func(t *testing.T) {
_, err := Parse(test.input)
if err == nil {
t.Fatalf("expected error (%v) not found, err is nil", test.err)
}
if err.Error() != test.err {
t.Fatalf("expected error %q, got %q", test.err, err.Error())
}
})
}
}
var extractTests = []struct {
input Options
ns string
output Options
}{
{
input: Options{
"foo.bar:": "baz",
"s3.timeout": "10s",
"sftp.timeout": "5s",
"global": "foobar",
},
ns: "s3",
output: Options{
"timeout": "10s",
},
},
}
func TestOptionsExtract(t *testing.T) {
for _, test := range extractTests {
t.Run(test.ns, func(t *testing.T) {
opts := test.input.Extract(test.ns)
if !reflect.DeepEqual(opts, test.output) {
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, opts)
}
})
}
}
// Target is used for Apply() tests
type Target struct {
Name string `option:"name"`
ID int `option:"id"`
Timeout time.Duration `option:"timeout"`
Other string
}
var setTests = []struct {
input Options
output Target
}{
{
Options{
"name": "foobar",
},
Target{
Name: "foobar",
},
},
{
Options{
"name": "foobar",
"id": "1234",
},
Target{
Name: "foobar",
ID: 1234,
},
},
{
Options{
"timeout": "10m3s",
},
Target{
Timeout: time.Duration(10*time.Minute + 3*time.Second),
},
},
}
func TestOptionsApply(t *testing.T) {
for i, test := range setTests {
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
var dst Target
err := test.input.Apply("", &dst)
if err != nil {
t.Fatal(err)
}
if dst != test.output {
t.Fatalf("wrong result, want:\n %#v\ngot:\n %#v", test.output, dst)
}
})
}
}
var invalidSetTests = []struct {
input Options
namespace string
err string
}{
{
Options{
"first_name": "foobar",
},
"ns",
"option ns.first_name is not known",
},
{
Options{
"id": "foobar",
},
"ns",
`strconv.ParseInt: parsing "foobar": invalid syntax`,
},
{
Options{
"timeout": "2134",
},
"ns",
`time: missing unit in duration 2134`,
},
}
func TestOptionsApplyInvalid(t *testing.T) {
for i, test := range invalidSetTests {
t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
var dst Target
err := test.input.Apply(test.namespace, &dst)
if err == nil {
t.Fatalf("expected error %v not found", test.err)
}
if err.Error() != test.err {
t.Fatalf("expected error %q, got %q", test.err, err.Error())
}
})
}
}

View file

@ -67,7 +67,7 @@ func TestRepository(t testing.TB) (r restic.Repository, cleanup func()) {
if dir != "" { if dir != "" {
_, err := os.Stat(dir) _, err := os.Stat(dir)
if err != nil { if err != nil {
be, err := local.Create(dir) be, err := local.Create(local.Config{Path: dir})
if err != nil { if err != nil {
t.Fatalf("error creating local backend at %v: %v", dir, err) t.Fatalf("error creating local backend at %v: %v", dir, err)
} }
@ -84,7 +84,7 @@ func TestRepository(t testing.TB) (r restic.Repository, cleanup func()) {
// TestOpenLocal opens a local repository. // TestOpenLocal opens a local repository.
func TestOpenLocal(t testing.TB, dir string) (r restic.Repository) { func TestOpenLocal(t testing.TB, dir string) (r restic.Repository) {
be, err := local.Open(dir) be, err := local.Open(local.Config{Path: dir})
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }