forked from TrueCloudLab/restic
Add repository location parsing code
This commit is contained in:
parent
43cf95e3c6
commit
566a15285a
7 changed files with 370 additions and 0 deletions
15
backend/local/uri.go
Normal file
15
backend/local/uri.go
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
package local
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseConfig parses a local backend config.
|
||||||
|
func ParseConfig(cfg string) (interface{}, error) {
|
||||||
|
if !strings.HasPrefix(cfg, "local:") {
|
||||||
|
return nil, errors.New(`invalid format, prefix "local" not found`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg[6:], nil
|
||||||
|
}
|
51
backend/s3/uri.go
Normal file
51
backend/s3/uri.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package s3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains all configuration necessary to connect to an s3 compatible
|
||||||
|
// server.
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
KeyID, Secret string
|
||||||
|
Bucket string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseConfig parses the string s and extracts the s3 config. The two
|
||||||
|
// supported configuration formats are s3://host/bucketname and
|
||||||
|
// s3:host:bucketname. The host can also be a valid s3 region name.
|
||||||
|
func ParseConfig(s string) (interface{}, error) {
|
||||||
|
if strings.HasPrefix(s, "s3://") {
|
||||||
|
s = s[5:]
|
||||||
|
|
||||||
|
data := strings.SplitN(s, "/", 2)
|
||||||
|
if len(data) != 2 {
|
||||||
|
return nil, errors.New("s3: invalid format, host/region or bucket name not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
Host: data[0],
|
||||||
|
Bucket: data[1],
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data := strings.SplitN(s, ":", 3)
|
||||||
|
if len(data) != 3 {
|
||||||
|
return nil, errors.New("s3: invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[0] != "s3" {
|
||||||
|
return nil, errors.New(`s3: config does not start with "s3"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
Host: data[1],
|
||||||
|
Bucket: data[2],
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
33
backend/s3/uri_test.go
Normal file
33
backend/s3/uri_test.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package s3
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var uriTests = []struct {
|
||||||
|
s string
|
||||||
|
cfg Config
|
||||||
|
}{
|
||||||
|
{"s3://eu-central-1/bucketname", Config{
|
||||||
|
Host: "eu-central-1",
|
||||||
|
Bucket: "bucketname",
|
||||||
|
}},
|
||||||
|
{"s3:hostname:foobar", Config{
|
||||||
|
Host: "hostname",
|
||||||
|
Bucket: "foobar",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConfig(t *testing.T) {
|
||||||
|
for i, test := range uriTests {
|
||||||
|
cfg, err := ParseConfig(test.s)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("test %d failed: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg != test.cfg {
|
||||||
|
t.Errorf("test %d: wrong config, want:\n %v\ngot:\n %v",
|
||||||
|
i, test.cfg, cfg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
68
backend/sftp/uri.go
Normal file
68
backend/sftp/uri.go
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config collects all information required to connect to an sftp server.
|
||||||
|
type Config struct {
|
||||||
|
User, Host, Dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseConfig extracts all information for the sftp connection from the string s.
|
||||||
|
func ParseConfig(s string) (interface{}, error) {
|
||||||
|
if strings.HasPrefix(s, "sftp://") {
|
||||||
|
return parseFormat1(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise parse in the sftp:user@host:path format, which means we'll get
|
||||||
|
// "user@host:path" in s
|
||||||
|
return parseFormat2(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFormat1 parses the first format, starting with a slash, so the user
|
||||||
|
// either specified "sftp://host/path", so we'll get everything after the first
|
||||||
|
// colon character
|
||||||
|
func parseFormat1(s string) (Config, error) {
|
||||||
|
url, err := url.Parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := Config{
|
||||||
|
Host: url.Host,
|
||||||
|
Dir: url.Path[1:],
|
||||||
|
}
|
||||||
|
if url.User != nil {
|
||||||
|
cfg.User = url.User.Username()
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseFormat2 parses the second format, sftp:user@host:path
|
||||||
|
func parseFormat2(s string) (cfg Config, err error) {
|
||||||
|
// split user/host and path at the second colon
|
||||||
|
data := strings.SplitN(s, ":", 3)
|
||||||
|
if len(data) < 3 {
|
||||||
|
return Config{}, errors.New("sftp: invalid format, hostname or path not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if data[0] != "sftp" {
|
||||||
|
return Config{}, errors.New(`invalid format, does not start with "sftp:"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
userhost := data[1]
|
||||||
|
cfg.Dir = data[2]
|
||||||
|
|
||||||
|
data = strings.SplitN(userhost, "@", 2)
|
||||||
|
if len(data) == 2 {
|
||||||
|
cfg.User = data[0]
|
||||||
|
cfg.Host = data[1]
|
||||||
|
} else {
|
||||||
|
cfg.Host = userhost
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
52
backend/sftp/uri_test.go
Normal file
52
backend/sftp/uri_test.go
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
package sftp
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
var uriTests = []struct {
|
||||||
|
s string
|
||||||
|
cfg Config
|
||||||
|
}{
|
||||||
|
// first form, user specified sftp://user@host/dir
|
||||||
|
{
|
||||||
|
"sftp://user@host/dir/subdir",
|
||||||
|
Config{User: "user", Host: "host", Dir: "dir/subdir"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sftp://host/dir/subdir",
|
||||||
|
Config{Host: "host", Dir: "dir/subdir"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sftp://host//dir/subdir",
|
||||||
|
Config{Host: "host", Dir: "/dir/subdir"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// second form, user specified sftp:user@host:/dir
|
||||||
|
{
|
||||||
|
"sftp:foo@bar:/baz/quux",
|
||||||
|
Config{User: "foo", Host: "bar", Dir: "/baz/quux"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sftp:bar:../baz/quux",
|
||||||
|
Config{Host: "bar", Dir: "../baz/quux"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"sftp:fux@bar:baz/qu:ux",
|
||||||
|
Config{User: "fux", Host: "bar", Dir: "baz/qu:ux"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseConfig(t *testing.T) {
|
||||||
|
for i, test := range uriTests {
|
||||||
|
cfg, err := ParseConfig(test.s)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("test %d failed: %v", i, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg != test.cfg {
|
||||||
|
t.Errorf("test %d: wrong config, want:\n %v\ngot:\n %v",
|
||||||
|
i, test.cfg, cfg)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
66
uri/uri.go
Normal file
66
uri/uri.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
// Package uri implements parsing the restic repository location from a string.
|
||||||
|
package uri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/restic/restic/backend/local"
|
||||||
|
"github.com/restic/restic/backend/s3"
|
||||||
|
"github.com/restic/restic/backend/sftp"
|
||||||
|
)
|
||||||
|
|
||||||
|
// URI specifies the location of a repository, including the method of access
|
||||||
|
// and (possibly) credentials needed for access.
|
||||||
|
type URI struct {
|
||||||
|
Scheme string
|
||||||
|
Config interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type parser struct {
|
||||||
|
scheme string
|
||||||
|
parse func(string) (interface{}, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsers is a list of valid config parsers for the backends. The first parser
|
||||||
|
// is the fallback and should always be set to the local backend.
|
||||||
|
var parsers = []parser{
|
||||||
|
{"local", local.ParseConfig},
|
||||||
|
{"sftp", sftp.ParseConfig},
|
||||||
|
{"s3", s3.ParseConfig},
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseURI parses a repository location from the string s. If s starts with a
|
||||||
|
// backend name followed by a colon, that backend's Parse() function is called.
|
||||||
|
// Otherwise, the local backend is used which interprets s as the name of a
|
||||||
|
// directory.
|
||||||
|
func ParseURI(s string) (u URI, err error) {
|
||||||
|
scheme := extractScheme(s)
|
||||||
|
u.Scheme = scheme
|
||||||
|
|
||||||
|
for _, parser := range parsers {
|
||||||
|
if parser.scheme != scheme {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u.Config, err = parser.parse(s)
|
||||||
|
if err != nil {
|
||||||
|
return URI{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// try again, with the local parser and the prefix "local:"
|
||||||
|
u.Scheme = "local"
|
||||||
|
u.Config, err = local.ParseConfig("local:" + s)
|
||||||
|
if err != nil {
|
||||||
|
return URI{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractScheme(s string) string {
|
||||||
|
data := strings.SplitN(s, ":", 2)
|
||||||
|
return data[0]
|
||||||
|
}
|
85
uri/uri_test.go
Normal file
85
uri/uri_test.go
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
package uri
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/restic/restic/backend/s3"
|
||||||
|
"github.com/restic/restic/backend/sftp"
|
||||||
|
)
|
||||||
|
|
||||||
|
var parseTests = []struct {
|
||||||
|
s string
|
||||||
|
u URI
|
||||||
|
}{
|
||||||
|
{"local:/srv/repo", URI{Scheme: "local", Config: "/srv/repo"}},
|
||||||
|
{"local:dir1/dir2", URI{Scheme: "local", Config: "dir1/dir2"}},
|
||||||
|
{"local:dir1/dir2", URI{Scheme: "local", Config: "dir1/dir2"}},
|
||||||
|
{"dir1/dir2", URI{Scheme: "local", Config: "dir1/dir2"}},
|
||||||
|
{"local:../dir1/dir2", URI{Scheme: "local", Config: "../dir1/dir2"}},
|
||||||
|
{"/dir1/dir2", URI{Scheme: "local", Config: "/dir1/dir2"}},
|
||||||
|
|
||||||
|
{"sftp:user@host:/srv/repo", URI{Scheme: "sftp",
|
||||||
|
Config: sftp.Config{
|
||||||
|
User: "user",
|
||||||
|
Host: "host",
|
||||||
|
Dir: "/srv/repo",
|
||||||
|
}}},
|
||||||
|
{"sftp:host:/srv/repo", URI{Scheme: "sftp",
|
||||||
|
Config: sftp.Config{
|
||||||
|
User: "",
|
||||||
|
Host: "host",
|
||||||
|
Dir: "/srv/repo",
|
||||||
|
}}},
|
||||||
|
{"sftp://user@host/srv/repo", URI{Scheme: "sftp",
|
||||||
|
Config: sftp.Config{
|
||||||
|
User: "user",
|
||||||
|
Host: "host",
|
||||||
|
Dir: "srv/repo",
|
||||||
|
}}},
|
||||||
|
{"sftp://user@host//srv/repo", URI{Scheme: "sftp",
|
||||||
|
Config: sftp.Config{
|
||||||
|
User: "user",
|
||||||
|
Host: "host",
|
||||||
|
Dir: "/srv/repo",
|
||||||
|
}}},
|
||||||
|
|
||||||
|
{"s3://eu-central-1/bucketname", URI{Scheme: "s3",
|
||||||
|
Config: s3.Config{
|
||||||
|
Host: "eu-central-1",
|
||||||
|
Bucket: "bucketname",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{"s3://hostname.foo/bucketname", URI{Scheme: "s3",
|
||||||
|
Config: s3.Config{
|
||||||
|
Host: "hostname.foo",
|
||||||
|
Bucket: "bucketname",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
{"s3:hostname.foo:repo", URI{Scheme: "s3",
|
||||||
|
Config: s3.Config{
|
||||||
|
Host: "hostname.foo",
|
||||||
|
Bucket: "repo",
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseURI(t *testing.T) {
|
||||||
|
for i, test := range parseTests {
|
||||||
|
u, err := ParseURI(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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue