From 566a15285ae1796c52597e4beccb08e6104c480c Mon Sep 17 00:00:00 2001 From: Alexander Neumann Date: Mon, 28 Dec 2015 15:51:24 +0100 Subject: [PATCH] Add repository location parsing code --- backend/local/uri.go | 15 +++++++ backend/s3/uri.go | 51 ++++++++++++++++++++++++ backend/s3/uri_test.go | 33 ++++++++++++++++ backend/sftp/uri.go | 68 ++++++++++++++++++++++++++++++++ backend/sftp/uri_test.go | 52 ++++++++++++++++++++++++ uri/uri.go | 66 +++++++++++++++++++++++++++++++ uri/uri_test.go | 85 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 370 insertions(+) create mode 100644 backend/local/uri.go create mode 100644 backend/s3/uri.go create mode 100644 backend/s3/uri_test.go create mode 100644 backend/sftp/uri.go create mode 100644 backend/sftp/uri_test.go create mode 100644 uri/uri.go create mode 100644 uri/uri_test.go diff --git a/backend/local/uri.go b/backend/local/uri.go new file mode 100644 index 000000000..456f3c428 --- /dev/null +++ b/backend/local/uri.go @@ -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 +} diff --git a/backend/s3/uri.go b/backend/s3/uri.go new file mode 100644 index 000000000..808a9464b --- /dev/null +++ b/backend/s3/uri.go @@ -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 +} diff --git a/backend/s3/uri_test.go b/backend/s3/uri_test.go new file mode 100644 index 000000000..27f1f63a3 --- /dev/null +++ b/backend/s3/uri_test.go @@ -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 + } + } +} diff --git a/backend/sftp/uri.go b/backend/sftp/uri.go new file mode 100644 index 000000000..ee241facb --- /dev/null +++ b/backend/sftp/uri.go @@ -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 +} diff --git a/backend/sftp/uri_test.go b/backend/sftp/uri_test.go new file mode 100644 index 000000000..400660be6 --- /dev/null +++ b/backend/sftp/uri_test.go @@ -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 + } + } +} diff --git a/uri/uri.go b/uri/uri.go new file mode 100644 index 000000000..85d0c8a44 --- /dev/null +++ b/uri/uri.go @@ -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] +} diff --git a/uri/uri_test.go b/uri/uri_test.go new file mode 100644 index 000000000..8aff27b51 --- /dev/null +++ b/uri/uri_test.go @@ -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) + } + } +}