diff --git a/src/cmds/restic/global.go b/src/cmds/restic/global.go index 8b4cdd81da3..8f491a4a57c 100644 --- a/src/cmds/restic/global.go +++ b/src/cmds/restic/global.go @@ -329,6 +329,17 @@ func open(s string) (restic.Backend, error) { debug.Log("opening s3 repository at %#v", cfg) be, err = s3.Open(cfg) + case "gs": + cfg := loc.Config.(s3.Config) + if cfg.KeyID == "" { + cfg.KeyID = os.Getenv("GS_ACCESS_KEY_ID") + + } + if cfg.Secret == "" { + cfg.Secret = os.Getenv("GS_SECRET_ACCESS_KEY") + } + debug.Log("open", "opening gcs repository at %#v", cfg) + be, err := s3.Open(cfg) case "rest": be, err = rest.Open(loc.Config.(rest.Config)) default: @@ -369,6 +380,18 @@ func create(s string) (restic.Backend, error) { debug.Log("create s3 repository at %#v", loc.Config) return s3.Open(cfg) + case "gs": + cfg := loc.Config.(s3.Config) + if cfg.KeyID == "" { + cfg.KeyID = os.Getenv("GS_ACCESS_KEY_ID") + + } + if cfg.Secret == "" { + cfg.Secret = os.Getenv("GS_SECRET_ACCESS_KEY") + } + + debug.Log("open", "create gcs repository at %#v", loc.Config) + return s3.Open(cfg) case "rest": return rest.Open(loc.Config.(rest.Config)) } diff --git a/src/restic/backend/gcs/config.go b/src/restic/backend/gcs/config.go new file mode 100644 index 00000000000..3687a491404 --- /dev/null +++ b/src/restic/backend/gcs/config.go @@ -0,0 +1,43 @@ +package gcs + +import ( + "errors" + "path" + "restic/backend/s3" + "strings" +) + +// The endpoint for all GCS operations. +const gcsEndpoint = "storage.googleapis.com" + +const defaultPrefix = "restic" + +// ParseConfig parses the string s and extracts the gcs config. The two +// supported configuration formats are gcs://bucketname/prefix and +// gcs:bucketname/prefix. +func ParseConfig(s string) (interface{}, error) { + switch { + case strings.HasPrefix(s, "gs://"): + s = s[5:] + case strings.HasPrefix(s, "gs:"): + s = s[3:] + default: + return nil, errors.New(`gcs: config does not start with "gs"`) + } + p := strings.SplitN(s, "/", 2) + var prefix string + switch { + case len(p) < 1: + return nil, errors.New("gcs: invalid format: bucket name not found") + case len(p) == 1 || p[1] == "": + prefix = defaultPrefix + default: + prefix = path.Clean(p[1]) + } + return s3.Config{ + Endpoint: gcsEndpoint, + UseHTTP: false, + Bucket: p[0], + Prefix: prefix, + }, nil +} diff --git a/src/restic/backend/gcs/config_test.go b/src/restic/backend/gcs/config_test.go new file mode 100644 index 00000000000..c5843e37169 --- /dev/null +++ b/src/restic/backend/gcs/config_test.go @@ -0,0 +1,68 @@ +package gcs + +import ( + "restic/backend/s3" + "testing" +) + +var configTests = []struct { + s string + cfg s3.Config +}{ + {"gs://bucketname", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "restic", + }}, + {"gs://bucketname/", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "restic", + }}, + {"gs://bucketname/prefix/dir", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "prefix/dir", + }}, + {"gs://bucketname/prefix/dir/", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "prefix/dir", + }}, + {"gs:bucketname", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "restic", + }}, + {"gs:bucketname/", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "restic", + }}, + {"gs:bucketname/prefix/dir", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "prefix/dir", + }}, + {"gs:bucketname/prefix/dir/", s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "prefix/dir", + }}, +} + +func TestParseConfig(t *testing.T) { + for i, test := range configTests { + cfg, err := ParseConfig(test.s) + if err != nil { + t.Errorf("test %d:%s failed: %v", i, test.s, err) + continue + } + + if cfg != test.cfg { + t.Errorf("test %d:\ninput:\n %s\n wrong config, want:\n %v\ngot:\n %v", + i, test.s, test.cfg, cfg) + continue + } + } +} diff --git a/src/restic/location/location.go b/src/restic/location/location.go index 23e0af37b8f..2c595512ced 100644 --- a/src/restic/location/location.go +++ b/src/restic/location/location.go @@ -4,6 +4,7 @@ package location import ( "strings" + "restic/backend/gcs" "restic/backend/local" "restic/backend/rest" "restic/backend/s3" @@ -28,6 +29,7 @@ var parsers = []parser{ {"local", local.ParseConfig}, {"sftp", sftp.ParseConfig}, {"s3", s3.ParseConfig}, + {"gs", gcs.ParseConfig}, {"rest", rest.ParseConfig}, } diff --git a/src/restic/location/location_test.go b/src/restic/location/location_test.go index bb4ac64c905..870ffdaad22 100644 --- a/src/restic/location/location_test.go +++ b/src/restic/location/location_test.go @@ -54,7 +54,34 @@ var parseTests = []struct { Host: "host", Dir: "/srv/repo", }}}, - + {"gs://bucketname", Location{Scheme: "gs", + Config: s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "restic", + }}, + }, + {"gs://bucketname/prefix", Location{Scheme: "gs", + Config: s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "prefix", + }}, + }, + {"gs:bucketname", Location{Scheme: "gs", + Config: s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "restic", + }}, + }, + {"gs:bucketname/prefix", Location{Scheme: "gs", + Config: s3.Config{ + Endpoint: "storage.googleapis.com", + Bucket: "bucketname", + Prefix: "prefix", + }}, + }, {"s3://eu-central-1/bucketname", Location{Scheme: "s3", Config: s3.Config{ Endpoint: "eu-central-1",