2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-29 16:23:59 +00:00

Merge pull request #4298 from restic/backend-parseconfig-cleanup

Unified and slightly type-safer backend config parsing
This commit is contained in:
Michael Eischer 2023-06-08 12:02:27 +02:00 committed by GitHub
commit e14ccb1142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 511 additions and 637 deletions

View File

@ -535,147 +535,21 @@ func OpenRepository(ctx context.Context, opts GlobalOptions) (*repository.Reposi
} }
func parseConfig(loc location.Location, opts options.Options) (interface{}, error) { func parseConfig(loc location.Location, opts options.Options) (interface{}, error) {
// only apply options for a particular backend here cfg := loc.Config
opts = opts.Extract(loc.Scheme) if cfg, ok := cfg.(restic.ApplyEnvironmenter); ok {
if err := cfg.ApplyEnvironment(""); err != nil {
switch loc.Scheme {
case "local":
cfg := loc.Config.(local.Config)
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err 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.String() == "" {
cfg.Secret = options.NewSecretString(os.Getenv("AWS_SECRET_ACCESS_KEY"))
}
if cfg.KeyID == "" && cfg.Secret.String() != "" {
return nil, errors.Fatalf("unable to open S3 backend: Key ID ($AWS_ACCESS_KEY_ID) is empty")
} else if cfg.KeyID != "" && cfg.Secret.String() == "" {
return nil, errors.Fatalf("unable to open S3 backend: Secret ($AWS_SECRET_ACCESS_KEY) is empty")
}
if cfg.Region == "" {
cfg.Region = os.Getenv("AWS_DEFAULT_REGION")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening s3 repository at %#v", cfg)
return cfg, nil
case "gs":
cfg := loc.Config.(gs.Config)
if cfg.ProjectID == "" {
cfg.ProjectID = os.Getenv("GOOGLE_PROJECT_ID")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening gs repository at %#v", cfg)
return cfg, nil
case "azure":
cfg := loc.Config.(azure.Config)
if cfg.AccountName == "" {
cfg.AccountName = os.Getenv("AZURE_ACCOUNT_NAME")
}
if cfg.AccountKey.String() == "" {
cfg.AccountKey = options.NewSecretString(os.Getenv("AZURE_ACCOUNT_KEY"))
}
if cfg.AccountSAS.String() == "" {
cfg.AccountSAS = options.NewSecretString(os.Getenv("AZURE_ACCOUNT_SAS"))
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening gs repository at %#v", cfg)
return cfg, nil
case "swift":
cfg := loc.Config.(swift.Config)
if err := swift.ApplyEnvironment("", &cfg); err != nil {
return nil, err
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening swift repository at %#v", cfg)
return cfg, nil
case "b2":
cfg := loc.Config.(b2.Config)
if cfg.AccountID == "" {
cfg.AccountID = os.Getenv("B2_ACCOUNT_ID")
}
if cfg.AccountID == "" {
return nil, errors.Fatalf("unable to open B2 backend: Account ID ($B2_ACCOUNT_ID) is empty")
}
if cfg.Key.String() == "" {
cfg.Key = options.NewSecretString(os.Getenv("B2_ACCOUNT_KEY"))
}
if cfg.Key.String() == "" {
return nil, errors.Fatalf("unable to open B2 backend: Key ($B2_ACCOUNT_KEY) is empty")
}
if err := opts.Apply(loc.Scheme, &cfg); err != nil {
return nil, err
}
debug.Log("opening b2 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
case "rclone":
cfg := loc.Config.(rclone.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) // only apply options for a particular backend here
opts = opts.Extract(loc.Scheme)
if err := opts.Apply(loc.Scheme, cfg); err != nil {
return nil, err
}
debug.Log("opening %v repository at %#v", loc.Scheme, cfg)
return cfg, nil
} }
// Open the backend specified by a location config. // Open the backend specified by a location config.
@ -704,23 +578,23 @@ func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Optio
switch loc.Scheme { switch loc.Scheme {
case "local": case "local":
be, err = local.Open(ctx, cfg.(local.Config)) be, err = local.Open(ctx, *cfg.(*local.Config))
case "sftp": case "sftp":
be, err = sftp.Open(ctx, cfg.(sftp.Config)) be, err = sftp.Open(ctx, *cfg.(*sftp.Config))
case "s3": case "s3":
be, err = s3.Open(ctx, cfg.(s3.Config), rt) be, err = s3.Open(ctx, *cfg.(*s3.Config), rt)
case "gs": case "gs":
be, err = gs.Open(cfg.(gs.Config), rt) be, err = gs.Open(*cfg.(*gs.Config), rt)
case "azure": case "azure":
be, err = azure.Open(ctx, cfg.(azure.Config), rt) be, err = azure.Open(ctx, *cfg.(*azure.Config), rt)
case "swift": case "swift":
be, err = swift.Open(ctx, cfg.(swift.Config), rt) be, err = swift.Open(ctx, *cfg.(*swift.Config), rt)
case "b2": case "b2":
be, err = b2.Open(ctx, cfg.(b2.Config), rt) be, err = b2.Open(ctx, *cfg.(*b2.Config), rt)
case "rest": case "rest":
be, err = rest.Open(cfg.(rest.Config), rt) be, err = rest.Open(*cfg.(*rest.Config), rt)
case "rclone": case "rclone":
be, err = rclone.Open(cfg.(rclone.Config), lim) be, err = rclone.Open(*cfg.(*rclone.Config), lim)
default: default:
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme) return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
@ -780,23 +654,23 @@ func create(ctx context.Context, s string, opts options.Options) (restic.Backend
var be restic.Backend var be restic.Backend
switch loc.Scheme { switch loc.Scheme {
case "local": case "local":
be, err = local.Create(ctx, cfg.(local.Config)) be, err = local.Create(ctx, *cfg.(*local.Config))
case "sftp": case "sftp":
be, err = sftp.Create(ctx, cfg.(sftp.Config)) be, err = sftp.Create(ctx, *cfg.(*sftp.Config))
case "s3": case "s3":
be, err = s3.Create(ctx, cfg.(s3.Config), rt) be, err = s3.Create(ctx, *cfg.(*s3.Config), rt)
case "gs": case "gs":
be, err = gs.Create(ctx, cfg.(gs.Config), rt) be, err = gs.Create(ctx, *cfg.(*gs.Config), rt)
case "azure": case "azure":
be, err = azure.Create(ctx, cfg.(azure.Config), rt) be, err = azure.Create(ctx, *cfg.(*azure.Config), rt)
case "swift": case "swift":
be, err = swift.Open(ctx, cfg.(swift.Config), rt) be, err = swift.Open(ctx, *cfg.(*swift.Config), rt)
case "b2": case "b2":
be, err = b2.Create(ctx, cfg.(b2.Config), rt) be, err = b2.Create(ctx, *cfg.(*b2.Config), rt)
case "rest": case "rest":
be, err = rest.Create(ctx, cfg.(rest.Config), rt) be, err = rest.Create(ctx, *cfg.(*rest.Config), rt)
case "rclone": case "rclone":
be, err = rclone.Create(ctx, cfg.(rclone.Config)) be, err = rclone.Create(ctx, *cfg.(*rclone.Config))
default: default:
debug.Log("invalid repository scheme: %v", s) debug.Log("invalid repository scheme: %v", s)
return nil, errors.Fatalf("invalid scheme %q", loc.Scheme) return nil, errors.Fatalf("invalid scheme %q", loc.Scheme)

View File

@ -18,34 +18,34 @@ import (
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func newAzureTestSuite(t testing.TB) *test.Suite { func newAzureTestSuite(t testing.TB) *test.Suite[azure.Config] {
tr, err := backend.Transport(backend.TransportOptions{}) tr, err := backend.Transport(backend.TransportOptions{})
if err != nil { if err != nil {
t.Fatalf("cannot create transport for tests: %v", err) t.Fatalf("cannot create transport for tests: %v", err)
} }
return &test.Suite{ return &test.Suite[azure.Config]{
// do not use excessive data // do not use excessive data
MinimalData: true, MinimalData: true,
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*azure.Config, error) {
azcfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY")) cfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY"))
if err != nil {
return nil, err
}
err = cfg.ApplyEnvironment("RESTIC_TEST_")
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg := azcfg.(azure.Config)
cfg.AccountName = os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_NAME")
cfg.AccountKey = options.NewSecretString(os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_KEY"))
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano()) cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
return cfg, nil return cfg, nil
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg azure.Config) (restic.Backend, error) {
cfg := config.(azure.Config)
ctx := context.TODO() ctx := context.TODO()
be, err := azure.Create(ctx, cfg, tr) be, err := azure.Create(ctx, cfg, tr)
if err != nil { if err != nil {
@ -65,15 +65,13 @@ func newAzureTestSuite(t testing.TB) *test.Suite {
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg azure.Config) (restic.Backend, error) {
cfg := config.(azure.Config)
ctx := context.TODO() ctx := context.TODO()
return azure.Open(ctx, cfg, tr) return azure.Open(ctx, cfg, tr)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg azure.Config) error {
cfg := config.(azure.Config)
ctx := context.TODO() ctx := context.TODO()
be, err := azure.Open(ctx, cfg, tr) be, err := azure.Open(ctx, cfg, tr)
if err != nil { if err != nil {
@ -141,12 +139,11 @@ func TestUploadLargeFile(t *testing.T) {
return return
} }
azcfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY")) cfg, err := azure.ParseConfig(os.Getenv("RESTIC_TEST_AZURE_REPOSITORY"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
cfg := azcfg.(azure.Config)
cfg.AccountName = os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_NAME") cfg.AccountName = os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_NAME")
cfg.AccountKey = options.NewSecretString(os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_KEY")) cfg.AccountKey = options.NewSecretString(os.Getenv("RESTIC_TEST_AZURE_ACCOUNT_KEY"))
cfg.Prefix = fmt.Sprintf("test-upload-large-%d", time.Now().UnixNano()) cfg.Prefix = fmt.Sprintf("test-upload-large-%d", time.Now().UnixNano())
@ -156,7 +153,7 @@ func TestUploadLargeFile(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
be, err := azure.Create(ctx, cfg, tr) be, err := azure.Create(ctx, *cfg, tr)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@ -1,11 +1,13 @@
package azure package azure
import ( import (
"os"
"path" "path"
"strings" "strings"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options" "github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
) )
// Config contains all configuration necessary to connect to an azure compatible // Config contains all configuration necessary to connect to an azure compatible
@ -33,7 +35,7 @@ func init() {
// ParseConfig parses the string s and extracts the azure config. The // ParseConfig parses the string s and extracts the azure config. The
// configuration format is azure:containerName:/[prefix]. // configuration format is azure:containerName:/[prefix].
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "azure:") { if !strings.HasPrefix(s, "azure:") {
return nil, errors.New("azure: invalid format") return nil, errors.New("azure: invalid format")
} }
@ -51,5 +53,23 @@ func ParseConfig(s string) (interface{}, error) {
cfg := NewConfig() cfg := NewConfig()
cfg.Container = container cfg.Container = container
cfg.Prefix = prefix cfg.Prefix = prefix
return cfg, nil return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.AccountName == "" {
cfg.AccountName = os.Getenv(prefix + "AZURE_ACCOUNT_NAME")
}
if cfg.AccountKey.String() == "" {
cfg.AccountKey = options.NewSecretString(os.Getenv(prefix + "AZURE_ACCOUNT_KEY"))
}
if cfg.AccountSAS.String() == "" {
cfg.AccountSAS = options.NewSecretString(os.Getenv(prefix + "AZURE_ACCOUNT_SAS"))
}
return nil
} }

View File

@ -1,22 +1,23 @@
package azure package azure
import "testing" import (
"testing"
var configTests = []struct { "github.com/restic/restic/internal/backend/test"
s string )
cfg Config
}{ var configTests = []test.ConfigTestData[Config]{
{"azure:container-name:/", Config{ {S: "azure:container-name:/", Cfg: Config{
Container: "container-name", Container: "container-name",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"azure:container-name:/prefix/directory", Config{ {S: "azure:container-name:/prefix/directory", Cfg: Config{
Container: "container-name", Container: "container-name",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
}}, }},
{"azure:container-name:/prefix/directory/", Config{ {S: "azure:container-name:/prefix/directory/", Cfg: Config{
Container: "container-name", Container: "container-name",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
@ -24,17 +25,5 @@ var configTests = []struct {
} }
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
for i, test := range configTests { test.ParseConfigTester(t, ParseConfig, 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
}
}
} }

View File

@ -10,19 +10,18 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/b2" "github.com/restic/restic/internal/backend/b2"
"github.com/restic/restic/internal/backend/test" "github.com/restic/restic/internal/backend/test"
"github.com/restic/restic/internal/options"
"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"
) )
func newB2TestSuite(t testing.TB) *test.Suite { func newB2TestSuite(t testing.TB) *test.Suite[b2.Config] {
tr, err := backend.Transport(backend.TransportOptions{}) tr, err := backend.Transport(backend.TransportOptions{})
if err != nil { if err != nil {
t.Fatalf("cannot create transport for tests: %v", err) t.Fatalf("cannot create transport for tests: %v", err)
} }
return &test.Suite{ return &test.Suite[b2.Config]{
// do not use excessive data // do not use excessive data
MinimalData: true, MinimalData: true,
@ -30,34 +29,33 @@ func newB2TestSuite(t testing.TB) *test.Suite {
WaitForDelayedRemoval: 10 * time.Second, WaitForDelayedRemoval: 10 * time.Second,
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*b2.Config, error) {
b2cfg, err := b2.ParseConfig(os.Getenv("RESTIC_TEST_B2_REPOSITORY")) cfg, err := b2.ParseConfig(os.Getenv("RESTIC_TEST_B2_REPOSITORY"))
if err != nil {
return nil, err
}
err = cfg.ApplyEnvironment("RESTIC_TEST_")
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg := b2cfg.(b2.Config)
cfg.AccountID = os.Getenv("RESTIC_TEST_B2_ACCOUNT_ID")
cfg.Key = options.NewSecretString(os.Getenv("RESTIC_TEST_B2_ACCOUNT_KEY"))
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano()) cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
return cfg, nil return cfg, nil
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg b2.Config) (restic.Backend, error) {
cfg := config.(b2.Config)
return b2.Create(context.Background(), cfg, tr) return b2.Create(context.Background(), cfg, tr)
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg b2.Config) (restic.Backend, error) {
cfg := config.(b2.Config)
return b2.Open(context.Background(), cfg, tr) return b2.Open(context.Background(), cfg, tr)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg b2.Config) error {
cfg := config.(b2.Config)
be, err := b2.Open(context.Background(), cfg, tr) be, err := b2.Open(context.Background(), cfg, tr)
if err != nil { if err != nil {
return err return err

View File

@ -1,12 +1,14 @@
package b2 package b2
import ( import (
"os"
"path" "path"
"regexp" "regexp"
"strings" "strings"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options" "github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
) )
// Config contains all configuration necessary to connect to an b2 compatible // Config contains all configuration necessary to connect to an b2 compatible
@ -58,7 +60,7 @@ func checkBucketName(name string) error {
// ParseConfig parses the string s and extracts the b2 config. The supported // ParseConfig parses the string s and extracts the b2 config. The supported
// configuration format is b2:bucketname/prefix. If no prefix is given the // configuration format is b2:bucketname/prefix. If no prefix is given the
// prefix "restic" will be used. // prefix "restic" will be used.
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "b2:") { if !strings.HasPrefix(s, "b2:") {
return nil, errors.New("invalid format, want: b2:bucket-name[:path]") return nil, errors.New("invalid format, want: b2:bucket-name[:path]")
} }
@ -77,5 +79,27 @@ func ParseConfig(s string) (interface{}, error) {
cfg.Bucket = bucket cfg.Bucket = bucket
cfg.Prefix = prefix cfg.Prefix = prefix
return cfg, nil return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.AccountID == "" {
cfg.AccountID = os.Getenv(prefix + "B2_ACCOUNT_ID")
}
if cfg.AccountID == "" {
return errors.Fatalf("unable to open B2 backend: Account ID ($B2_ACCOUNT_ID) is empty")
}
if cfg.Key.String() == "" {
cfg.Key = options.NewSecretString(os.Getenv(prefix + "B2_ACCOUNT_KEY"))
}
if cfg.Key.String() == "" {
return errors.Fatalf("unable to open B2 backend: Key ($B2_ACCOUNT_KEY) is empty")
}
return nil
} }

View File

@ -1,37 +1,38 @@
package b2 package b2
import "testing" import (
"testing"
var configTests = []struct { "github.com/restic/restic/internal/backend/test"
s string )
cfg Config
}{ var configTests = []test.ConfigTestData[Config]{
{"b2:bucketname", Config{ {S: "b2:bucketname", Cfg: Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"b2:bucketname:", Config{ {S: "b2:bucketname:", Cfg: Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"b2:bucketname:/prefix/directory", Config{ {S: "b2:bucketname:/prefix/directory", Cfg: Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
}}, }},
{"b2:foobar", Config{ {S: "b2:foobar", Cfg: Config{
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"b2:foobar:", Config{ {S: "b2:foobar:", Cfg: Config{
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"b2:foobar:/", Config{ {S: "b2:foobar:/", Cfg: Config{
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
@ -39,19 +40,7 @@ var configTests = []struct {
} }
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
for _, test := range configTests { test.ParseConfigTester(t, ParseConfig, configTests)
t.Run("", func(t *testing.T) {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("%s failed: %v", test.s, err)
}
if cfg != test.cfg {
t.Fatalf("input: %s\n wrong config, want:\n %#v\ngot:\n %#v",
test.s, test.cfg, cfg)
}
})
}
} }
var invalidConfigTests = []struct { var invalidConfigTests = []struct {

View File

@ -1,11 +1,13 @@
package gs package gs
import ( import (
"os"
"path" "path"
"strings" "strings"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options" "github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
) )
// Config contains all configuration necessary to connect to a Google Cloud Storage // Config contains all configuration necessary to connect to a Google Cloud Storage
@ -34,7 +36,7 @@ func init() {
// ParseConfig parses the string s and extracts the gcs config. The // ParseConfig parses the string s and extracts the gcs config. The
// supported configuration format is gs:bucketName:/[prefix]. // supported configuration format is gs:bucketName:/[prefix].
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "gs:") { if !strings.HasPrefix(s, "gs:") {
return nil, errors.New("gs: invalid format") return nil, errors.New("gs: invalid format")
} }
@ -54,5 +56,15 @@ func ParseConfig(s string) (interface{}, error) {
cfg := NewConfig() cfg := NewConfig()
cfg.Bucket = bucket cfg.Bucket = bucket
cfg.Prefix = prefix cfg.Prefix = prefix
return cfg, nil return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.ProjectID == "" {
cfg.ProjectID = os.Getenv(prefix + "GOOGLE_PROJECT_ID")
}
return nil
} }

View File

@ -1,24 +1,25 @@
package gs package gs
import "testing" import (
"testing"
var configTests = []struct { "github.com/restic/restic/internal/backend/test"
s string )
cfg Config
}{ var configTests = []test.ConfigTestData[Config]{
{"gs:bucketname:/", Config{ {S: "gs:bucketname:/", Cfg: Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
Region: "us", Region: "us",
}}, }},
{"gs:bucketname:/prefix/directory", Config{ {S: "gs:bucketname:/prefix/directory", Cfg: Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
Region: "us", Region: "us",
}}, }},
{"gs:bucketname:/prefix/directory/", Config{ {S: "gs:bucketname:/prefix/directory/", Cfg: Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
@ -27,17 +28,5 @@ var configTests = []struct {
} }
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
for i, test := range configTests { test.ParseConfigTester(t, ParseConfig, 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
}
}
} }

View File

@ -15,33 +15,30 @@ import (
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func newGSTestSuite(t testing.TB) *test.Suite { func newGSTestSuite(t testing.TB) *test.Suite[gs.Config] {
tr, err := backend.Transport(backend.TransportOptions{}) tr, err := backend.Transport(backend.TransportOptions{})
if err != nil { if err != nil {
t.Fatalf("cannot create transport for tests: %v", err) t.Fatalf("cannot create transport for tests: %v", err)
} }
return &test.Suite{ return &test.Suite[gs.Config]{
// do not use excessive data // do not use excessive data
MinimalData: true, MinimalData: true,
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*gs.Config, error) {
gscfg, err := gs.ParseConfig(os.Getenv("RESTIC_TEST_GS_REPOSITORY")) cfg, err := gs.ParseConfig(os.Getenv("RESTIC_TEST_GS_REPOSITORY"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg := gscfg.(gs.Config)
cfg.ProjectID = os.Getenv("RESTIC_TEST_GS_PROJECT_ID") cfg.ProjectID = os.Getenv("RESTIC_TEST_GS_PROJECT_ID")
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano()) cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
return cfg, nil return cfg, nil
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg gs.Config) (restic.Backend, error) {
cfg := config.(gs.Config)
be, err := gs.Create(context.Background(), cfg, tr) be, err := gs.Create(context.Background(), cfg, tr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -60,15 +57,12 @@ func newGSTestSuite(t testing.TB) *test.Suite {
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg gs.Config) (restic.Backend, error) {
cfg := config.(gs.Config)
return gs.Open(cfg, tr) return gs.Open(cfg, tr)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg gs.Config) error {
cfg := config.(gs.Config)
be, err := gs.Open(cfg, tr) be, err := gs.Open(cfg, tr)
if err != nil { if err != nil {
return err return err

View File

@ -27,12 +27,12 @@ func init() {
} }
// ParseConfig parses a local backend config. // ParseConfig parses a local backend config.
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "local:") { if !strings.HasPrefix(s, "local:") {
return nil, errors.New(`invalid format, prefix "local" not found`) return nil, errors.New(`invalid format, prefix "local" not found`)
} }
cfg := NewConfig() cfg := NewConfig()
cfg.Path = s[6:] cfg.Path = s[6:]
return cfg, nil return &cfg, nil
} }

View File

@ -0,0 +1,18 @@
package local
import (
"testing"
"github.com/restic/restic/internal/backend/test"
)
var configTests = []test.ConfigTestData[Config]{
{S: "local:/some/path", Cfg: Config{
Path: "/some/path",
Connections: 2,
}},
}
func TestParseConfig(t *testing.T) {
test.ParseConfigTester(t, ParseConfig, configTests)
}

View File

@ -12,10 +12,10 @@ import (
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func newTestSuite(t testing.TB) *test.Suite { func newTestSuite(t testing.TB) *test.Suite[local.Config] {
return &test.Suite{ return &test.Suite[local.Config]{
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*local.Config, error) {
dir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-local-") dir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-local-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -23,7 +23,7 @@ func newTestSuite(t testing.TB) *test.Suite {
t.Logf("create new backend at %v", dir) t.Logf("create new backend at %v", dir)
cfg := local.Config{ cfg := &local.Config{
Path: dir, Path: dir,
Connections: 2, Connections: 2,
} }
@ -31,20 +31,17 @@ func newTestSuite(t testing.TB) *test.Suite {
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg local.Config) (restic.Backend, error) {
cfg := config.(local.Config)
return local.Create(context.TODO(), cfg) return local.Create(context.TODO(), cfg)
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg local.Config) (restic.Backend, error) {
cfg := config.(local.Config)
return local.Open(context.TODO(), cfg) return local.Open(context.TODO(), cfg)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg local.Config) error {
cfg := config.(local.Config)
if !rtest.TestCleanupTempDirs { if !rtest.TestCleanupTempDirs {
t.Logf("leaving test backend dir at %v", cfg.Path) t.Logf("leaving test backend dir at %v", cfg.Path)
} }

View File

@ -29,18 +29,24 @@ type parser struct {
stripPassword func(string) string stripPassword func(string) string
} }
func configToAny[C any](parser func(string) (*C, error)) func(string) (interface{}, error) {
return func(s string) (interface{}, error) {
return parser(s)
}
}
// parsers is a list of valid config parsers for the backends. The first parser // 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. // is the fallback and should always be set to the local backend.
var parsers = []parser{ var parsers = []parser{
{"b2", b2.ParseConfig, noPassword}, {"b2", configToAny(b2.ParseConfig), noPassword},
{"local", local.ParseConfig, noPassword}, {"local", configToAny(local.ParseConfig), noPassword},
{"sftp", sftp.ParseConfig, noPassword}, {"sftp", configToAny(sftp.ParseConfig), noPassword},
{"s3", s3.ParseConfig, noPassword}, {"s3", configToAny(s3.ParseConfig), noPassword},
{"gs", gs.ParseConfig, noPassword}, {"gs", configToAny(gs.ParseConfig), noPassword},
{"azure", azure.ParseConfig, noPassword}, {"azure", configToAny(azure.ParseConfig), noPassword},
{"swift", swift.ParseConfig, noPassword}, {"swift", configToAny(swift.ParseConfig), noPassword},
{"rest", rest.ParseConfig, rest.StripPassword}, {"rest", configToAny(rest.ParseConfig), rest.StripPassword},
{"rclone", rclone.ParseConfig, noPassword}, {"rclone", configToAny(rclone.ParseConfig), noPassword},
} }
// noPassword returns the repository location unchanged (there's no sensitive information there) // noPassword returns the repository location unchanged (there's no sensitive information there)

View File

@ -29,7 +29,7 @@ var parseTests = []struct {
{ {
"local:/srv/repo", "local:/srv/repo",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "/srv/repo", Path: "/srv/repo",
Connections: 2, Connections: 2,
}, },
@ -38,7 +38,7 @@ var parseTests = []struct {
{ {
"local:dir1/dir2", "local:dir1/dir2",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "dir1/dir2", Path: "dir1/dir2",
Connections: 2, Connections: 2,
}, },
@ -47,7 +47,7 @@ var parseTests = []struct {
{ {
"local:dir1/dir2", "local:dir1/dir2",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "dir1/dir2", Path: "dir1/dir2",
Connections: 2, Connections: 2,
}, },
@ -56,7 +56,7 @@ var parseTests = []struct {
{ {
"dir1/dir2", "dir1/dir2",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "dir1/dir2", Path: "dir1/dir2",
Connections: 2, Connections: 2,
}, },
@ -65,7 +65,7 @@ var parseTests = []struct {
{ {
"/dir1/dir2", "/dir1/dir2",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "/dir1/dir2", Path: "/dir1/dir2",
Connections: 2, Connections: 2,
}, },
@ -74,7 +74,7 @@ var parseTests = []struct {
{ {
"local:../dir1/dir2", "local:../dir1/dir2",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "../dir1/dir2", Path: "../dir1/dir2",
Connections: 2, Connections: 2,
}, },
@ -83,7 +83,7 @@ var parseTests = []struct {
{ {
"/dir1/dir2", "/dir1/dir2",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "/dir1/dir2", Path: "/dir1/dir2",
Connections: 2, Connections: 2,
}, },
@ -92,7 +92,7 @@ var parseTests = []struct {
{ {
"/dir1:foobar/dir2", "/dir1:foobar/dir2",
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: "/dir1:foobar/dir2", Path: "/dir1:foobar/dir2",
Connections: 2, Connections: 2,
}, },
@ -101,7 +101,7 @@ var parseTests = []struct {
{ {
`\dir1\foobar\dir2`, `\dir1\foobar\dir2`,
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: `\dir1\foobar\dir2`, Path: `\dir1\foobar\dir2`,
Connections: 2, Connections: 2,
}, },
@ -110,7 +110,7 @@ var parseTests = []struct {
{ {
`c:\dir1\foobar\dir2`, `c:\dir1\foobar\dir2`,
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: `c:\dir1\foobar\dir2`, Path: `c:\dir1\foobar\dir2`,
Connections: 2, Connections: 2,
}, },
@ -119,7 +119,7 @@ var parseTests = []struct {
{ {
`C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`, `C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`,
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: `C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`, Path: `C:\Users\appveyor\AppData\Local\Temp\1\restic-test-879453535\repo`,
Connections: 2, Connections: 2,
}, },
@ -128,7 +128,7 @@ var parseTests = []struct {
{ {
`c:/dir1/foobar/dir2`, `c:/dir1/foobar/dir2`,
Location{Scheme: "local", Location{Scheme: "local",
Config: local.Config{ Config: &local.Config{
Path: `c:/dir1/foobar/dir2`, Path: `c:/dir1/foobar/dir2`,
Connections: 2, Connections: 2,
}, },
@ -137,7 +137,7 @@ var parseTests = []struct {
{ {
"sftp:user@host:/srv/repo", "sftp:user@host:/srv/repo",
Location{Scheme: "sftp", Location{Scheme: "sftp",
Config: sftp.Config{ Config: &sftp.Config{
User: "user", User: "user",
Host: "host", Host: "host",
Path: "/srv/repo", Path: "/srv/repo",
@ -148,7 +148,7 @@ var parseTests = []struct {
{ {
"sftp:host:/srv/repo", "sftp:host:/srv/repo",
Location{Scheme: "sftp", Location{Scheme: "sftp",
Config: sftp.Config{ Config: &sftp.Config{
User: "", User: "",
Host: "host", Host: "host",
Path: "/srv/repo", Path: "/srv/repo",
@ -159,7 +159,7 @@ var parseTests = []struct {
{ {
"sftp://user@host/srv/repo", "sftp://user@host/srv/repo",
Location{Scheme: "sftp", Location{Scheme: "sftp",
Config: sftp.Config{ Config: &sftp.Config{
User: "user", User: "user",
Host: "host", Host: "host",
Path: "srv/repo", Path: "srv/repo",
@ -170,7 +170,7 @@ var parseTests = []struct {
{ {
"sftp://user@host//srv/repo", "sftp://user@host//srv/repo",
Location{Scheme: "sftp", Location{Scheme: "sftp",
Config: sftp.Config{ Config: &sftp.Config{
User: "user", User: "user",
Host: "host", Host: "host",
Path: "/srv/repo", Path: "/srv/repo",
@ -182,7 +182,7 @@ var parseTests = []struct {
{ {
"s3://eu-central-1/bucketname", "s3://eu-central-1/bucketname",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
@ -193,7 +193,7 @@ var parseTests = []struct {
{ {
"s3://hostname.foo/bucketname", "s3://hostname.foo/bucketname",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
@ -204,7 +204,7 @@ var parseTests = []struct {
{ {
"s3://hostname.foo/bucketname/prefix/directory", "s3://hostname.foo/bucketname/prefix/directory",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix/directory", Prefix: "prefix/directory",
@ -215,7 +215,7 @@ var parseTests = []struct {
{ {
"s3:eu-central-1/repo", "s3:eu-central-1/repo",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "repo", Bucket: "repo",
Prefix: "", Prefix: "",
@ -226,7 +226,7 @@ var parseTests = []struct {
{ {
"s3:eu-central-1/repo/prefix/directory", "s3:eu-central-1/repo/prefix/directory",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "repo", Bucket: "repo",
Prefix: "prefix/directory", Prefix: "prefix/directory",
@ -237,7 +237,7 @@ var parseTests = []struct {
{ {
"s3:https://hostname.foo/repo", "s3:https://hostname.foo/repo",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "repo", Bucket: "repo",
Prefix: "", Prefix: "",
@ -248,7 +248,7 @@ var parseTests = []struct {
{ {
"s3:https://hostname.foo/repo/prefix/directory", "s3:https://hostname.foo/repo/prefix/directory",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "repo", Bucket: "repo",
Prefix: "prefix/directory", Prefix: "prefix/directory",
@ -259,7 +259,7 @@ var parseTests = []struct {
{ {
"s3:http://hostname.foo/repo", "s3:http://hostname.foo/repo",
Location{Scheme: "s3", Location{Scheme: "s3",
Config: s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "repo", Bucket: "repo",
Prefix: "", Prefix: "",
@ -271,7 +271,7 @@ var parseTests = []struct {
{ {
"swift:container17:/", "swift:container17:/",
Location{Scheme: "swift", Location{Scheme: "swift",
Config: swift.Config{ Config: &swift.Config{
Container: "container17", Container: "container17",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
@ -281,7 +281,7 @@ var parseTests = []struct {
{ {
"swift:container17:/prefix97", "swift:container17:/prefix97",
Location{Scheme: "swift", Location{Scheme: "swift",
Config: swift.Config{ Config: &swift.Config{
Container: "container17", Container: "container17",
Prefix: "prefix97", Prefix: "prefix97",
Connections: 5, Connections: 5,
@ -291,7 +291,7 @@ var parseTests = []struct {
{ {
"rest:http://hostname.foo:1234/", "rest:http://hostname.foo:1234/",
Location{Scheme: "rest", Location{Scheme: "rest",
Config: rest.Config{ Config: &rest.Config{
URL: parseURL("http://hostname.foo:1234/"), URL: parseURL("http://hostname.foo:1234/"),
Connections: 5, Connections: 5,
}, },
@ -299,7 +299,7 @@ var parseTests = []struct {
}, },
{ {
"b2:bucketname:/prefix", Location{Scheme: "b2", "b2:bucketname:/prefix", Location{Scheme: "b2",
Config: b2.Config{ Config: &b2.Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix", Prefix: "prefix",
Connections: 5, Connections: 5,
@ -308,7 +308,7 @@ var parseTests = []struct {
}, },
{ {
"b2:bucketname", Location{Scheme: "b2", "b2:bucketname", Location{Scheme: "b2",
Config: b2.Config{ Config: &b2.Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,

View File

@ -15,19 +15,19 @@ type memConfig struct {
be restic.Backend be restic.Backend
} }
func newTestSuite() *test.Suite { func newTestSuite() *test.Suite[*memConfig] {
return &test.Suite{ return &test.Suite[*memConfig]{
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (**memConfig, error) {
return &memConfig{}, nil cfg := &memConfig{}
return &cfg, nil
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(cfg interface{}) (restic.Backend, error) { Create: func(cfg *memConfig) (restic.Backend, error) {
c := cfg.(*memConfig) if cfg.be != nil {
if c.be != nil { _, err := cfg.be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile})
_, err := c.be.Stat(context.TODO(), restic.Handle{Type: restic.ConfigFile}) if err != nil && !cfg.be.IsNotExist(err) {
if err != nil && !c.be.IsNotExist(err) {
return nil, err return nil, err
} }
@ -36,21 +36,20 @@ func newTestSuite() *test.Suite {
} }
} }
c.be = mem.New() cfg.be = mem.New()
return c.be, nil return cfg.be, nil
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(cfg interface{}) (restic.Backend, error) { Open: func(cfg *memConfig) (restic.Backend, error) {
c := cfg.(*memConfig) if cfg.be == nil {
if c.be == nil { cfg.be = mem.New()
c.be = mem.New()
} }
return c.be, nil return cfg.be, nil
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(cfg interface{}) error { Cleanup: func(cfg *memConfig) error {
// no cleanup needed // no cleanup needed
return nil return nil
}, },

View File

@ -12,22 +12,21 @@ import (
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func newTestSuite(t testing.TB) *test.Suite { func newTestSuite(t testing.TB) *test.Suite[rclone.Config] {
dir := rtest.TempDir(t) dir := rtest.TempDir(t)
return &test.Suite{ return &test.Suite[rclone.Config]{
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*rclone.Config, error) {
t.Logf("use backend at %v", dir) t.Logf("use backend at %v", dir)
cfg := rclone.NewConfig() cfg := rclone.NewConfig()
cfg.Remote = dir cfg.Remote = dir
return cfg, nil return &cfg, nil
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg rclone.Config) (restic.Backend, error) {
t.Logf("Create()") t.Logf("Create()")
cfg := config.(rclone.Config)
be, err := rclone.Create(context.TODO(), cfg) be, err := rclone.Create(context.TODO(), cfg)
var e *exec.Error var e *exec.Error
if errors.As(err, &e) && e.Err == exec.ErrNotFound { if errors.As(err, &e) && e.Err == exec.ErrNotFound {
@ -38,9 +37,8 @@ func newTestSuite(t testing.TB) *test.Suite {
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg rclone.Config) (restic.Backend, error) {
t.Logf("Open()") t.Logf("Open()")
cfg := config.(rclone.Config)
return rclone.Open(cfg, nil) return rclone.Open(cfg, nil)
}, },
} }

View File

@ -34,7 +34,7 @@ func NewConfig() Config {
} }
// ParseConfig parses the string s and extracts the remote server URL. // ParseConfig parses the string s and extracts the remote server URL.
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "rclone:") { if !strings.HasPrefix(s, "rclone:") {
return nil, errors.New("invalid rclone backend specification") return nil, errors.New("invalid rclone backend specification")
} }
@ -42,5 +42,5 @@ func ParseConfig(s string) (interface{}, error) {
s = s[7:] s = s[7:]
cfg := NewConfig() cfg := NewConfig()
cfg.Remote = s cfg.Remote = s
return cfg, nil return &cfg, nil
} }

View File

@ -1,37 +1,24 @@
package rclone package rclone
import ( import (
"reflect"
"testing" "testing"
"github.com/restic/restic/internal/backend/test"
) )
func TestParseConfig(t *testing.T) { var configTests = []test.ConfigTestData[Config]{
var tests = []struct { {
s string S: "rclone:local:foo:/bar",
cfg Config Cfg: Config{
}{ Remote: "local:foo:/bar",
{ Program: defaultConfig.Program,
"rclone:local:foo:/bar", Args: defaultConfig.Args,
Config{ Connections: defaultConfig.Connections,
Remote: "local:foo:/bar", Timeout: defaultConfig.Timeout,
Program: defaultConfig.Program,
Args: defaultConfig.Args,
Connections: defaultConfig.Connections,
Timeout: defaultConfig.Timeout,
},
}, },
} },
}
for _, test := range tests {
t.Run("", func(t *testing.T) { func TestParseConfig(t *testing.T) {
cfg, err := ParseConfig(test.s) test.ParseConfigTester(t, ParseConfig, configTests)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(cfg, test.cfg) {
t.Fatalf("wrong config, want:\n %v\ngot:\n %v", test.cfg, cfg)
}
})
}
} }

View File

@ -26,7 +26,7 @@ func NewConfig() Config {
} }
// ParseConfig parses the string s and extracts the REST server URL. // ParseConfig parses the string s and extracts the REST server URL.
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "rest:") { if !strings.HasPrefix(s, "rest:") {
return nil, errors.New("invalid REST backend specification") return nil, errors.New("invalid REST backend specification")
} }
@ -40,7 +40,7 @@ func ParseConfig(s string) (interface{}, error) {
cfg := NewConfig() cfg := NewConfig()
cfg.URL = u cfg.URL = u
return cfg, nil return &cfg, nil
} }
// StripPassword removes the password from the URL // StripPassword removes the password from the URL

View File

@ -2,8 +2,9 @@ package rest
import ( import (
"net/url" "net/url"
"reflect"
"testing" "testing"
"github.com/restic/restic/internal/backend/test"
) )
func parseURL(s string) *url.URL { func parseURL(s string) *url.URL {
@ -15,20 +16,17 @@ func parseURL(s string) *url.URL {
return u return u
} }
var configTests = []struct { var configTests = []test.ConfigTestData[Config]{
s string
cfg Config
}{
{ {
s: "rest:http://localhost:1234", S: "rest:http://localhost:1234",
cfg: Config{ Cfg: Config{
URL: parseURL("http://localhost:1234/"), URL: parseURL("http://localhost:1234/"),
Connections: 5, Connections: 5,
}, },
}, },
{ {
s: "rest:http://localhost:1234/", S: "rest:http://localhost:1234/",
cfg: Config{ Cfg: Config{
URL: parseURL("http://localhost:1234/"), URL: parseURL("http://localhost:1234/"),
Connections: 5, Connections: 5,
}, },
@ -36,17 +34,5 @@ var configTests = []struct {
} }
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
for _, test := range configTests { test.ParseConfigTester(t, ParseConfig, configTests)
t.Run("", func(t *testing.T) {
cfg, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("%s failed: %v", test.s, err)
}
if !reflect.DeepEqual(cfg, test.cfg) {
t.Fatalf("\ninput: %s\n wrong config, want:\n %v\ngot:\n %v",
test.s, test.cfg, cfg)
}
})
}
} }

View File

@ -67,36 +67,34 @@ func runRESTServer(ctx context.Context, t testing.TB, dir string) (*url.URL, fun
return url, cleanup return url, cleanup
} }
func newTestSuite(_ context.Context, t testing.TB, url *url.URL, minimalData bool) *test.Suite { func newTestSuite(_ context.Context, t testing.TB, url *url.URL, minimalData bool) *test.Suite[rest.Config] {
tr, err := backend.Transport(backend.TransportOptions{}) tr, err := backend.Transport(backend.TransportOptions{})
if err != nil { if err != nil {
t.Fatalf("cannot create transport for tests: %v", err) t.Fatalf("cannot create transport for tests: %v", err)
} }
return &test.Suite{ return &test.Suite[rest.Config]{
MinimalData: minimalData, MinimalData: minimalData,
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*rest.Config, error) {
cfg := rest.NewConfig() cfg := rest.NewConfig()
cfg.URL = url cfg.URL = url
return cfg, nil return &cfg, nil
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg rest.Config) (restic.Backend, error) {
cfg := config.(rest.Config)
return rest.Create(context.TODO(), cfg, tr) return rest.Create(context.TODO(), cfg, tr)
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg rest.Config) (restic.Backend, error) {
cfg := config.(rest.Config)
return rest.Open(cfg, tr) return rest.Open(cfg, tr)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg rest.Config) error {
return nil return nil
}, },
} }
@ -130,12 +128,10 @@ func TestBackendRESTExternalServer(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
c := cfg.(rest.Config)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
defer cancel() defer cancel()
newTestSuite(ctx, t, c.URL, true).RunTests(t) newTestSuite(ctx, t, cfg.URL, true).RunTests(t)
} }
func BenchmarkBackendREST(t *testing.B) { func BenchmarkBackendREST(t *testing.B) {

View File

@ -2,11 +2,13 @@ package s3
import ( import (
"net/url" "net/url"
"os"
"path" "path"
"strings" "strings"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options" "github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
) )
// Config contains all configuration necessary to connect to an s3 compatible // Config contains all configuration necessary to connect to an s3 compatible
@ -44,7 +46,7 @@ func init() {
// supported configuration formats are s3://host/bucketname/prefix and // supported configuration formats are s3://host/bucketname/prefix and
// s3:host/bucketname/prefix. The host can also be a valid s3 region // s3:host/bucketname/prefix. The host can also be a valid s3 region
// name. If no prefix is given the prefix "restic" will be used. // name. If no prefix is given the prefix "restic" will be used.
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
switch { switch {
case strings.HasPrefix(s, "s3:http"): case strings.HasPrefix(s, "s3:http"):
// assume that a URL has been specified, parse it and // assume that a URL has been specified, parse it and
@ -75,7 +77,7 @@ func ParseConfig(s string) (interface{}, error) {
return createConfig(endpoint, bucket, prefix, false) return createConfig(endpoint, bucket, prefix, false)
} }
func createConfig(endpoint, bucket, prefix string, useHTTP bool) (interface{}, error) { func createConfig(endpoint, bucket, prefix string, useHTTP bool) (*Config, error) {
if endpoint == "" { if endpoint == "" {
return nil, errors.New("s3: invalid format, host/region or bucket name not found") return nil, errors.New("s3: invalid format, host/region or bucket name not found")
} }
@ -89,5 +91,30 @@ func createConfig(endpoint, bucket, prefix string, useHTTP bool) (interface{}, e
cfg.UseHTTP = useHTTP cfg.UseHTTP = useHTTP
cfg.Bucket = bucket cfg.Bucket = bucket
cfg.Prefix = prefix cfg.Prefix = prefix
return cfg, nil return &cfg, nil
}
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config.
func (cfg *Config) ApplyEnvironment(prefix string) error {
if cfg.KeyID == "" {
cfg.KeyID = os.Getenv(prefix + "AWS_ACCESS_KEY_ID")
}
if cfg.Secret.String() == "" {
cfg.Secret = options.NewSecretString(os.Getenv(prefix + "AWS_SECRET_ACCESS_KEY"))
}
if cfg.KeyID == "" && cfg.Secret.String() != "" {
return errors.Fatalf("unable to open S3 backend: Key ID ($AWS_ACCESS_KEY_ID) is empty")
} else if cfg.KeyID != "" && cfg.Secret.String() == "" {
return errors.Fatalf("unable to open S3 backend: Secret ($AWS_SECRET_ACCESS_KEY) is empty")
}
if cfg.Region == "" {
cfg.Region = os.Getenv(prefix + "AWS_DEFAULT_REGION")
}
return nil
} }

View File

@ -3,94 +3,93 @@ package s3
import ( import (
"strings" "strings"
"testing" "testing"
"github.com/restic/restic/internal/backend/test"
) )
var configTests = []struct { var configTests = []test.ConfigTestData[Config]{
s string {S: "s3://eu-central-1/bucketname", Cfg: Config{
cfg Config
}{
{"s3://eu-central-1/bucketname", Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"s3://eu-central-1/bucketname/", Config{ {S: "s3://eu-central-1/bucketname/", Cfg: Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"s3://eu-central-1/bucketname/prefix/directory", Config{ {S: "s3://eu-central-1/bucketname/prefix/directory", Cfg: Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
}}, }},
{"s3://eu-central-1/bucketname/prefix/directory/", Config{ {S: "s3://eu-central-1/bucketname/prefix/directory/", Cfg: Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
}}, }},
{"s3:eu-central-1/foobar", Config{ {S: "s3:eu-central-1/foobar", Cfg: Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"s3:eu-central-1/foobar/", Config{ {S: "s3:eu-central-1/foobar/", Cfg: Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"s3:eu-central-1/foobar/prefix/directory", Config{ {S: "s3:eu-central-1/foobar/prefix/directory", Cfg: Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "foobar", Bucket: "foobar",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
}}, }},
{"s3:eu-central-1/foobar/prefix/directory/", Config{ {S: "s3:eu-central-1/foobar/prefix/directory/", Cfg: Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "foobar", Bucket: "foobar",
Prefix: "prefix/directory", Prefix: "prefix/directory",
Connections: 5, Connections: 5,
}}, }},
{"s3:https://hostname:9999/foobar", Config{ {S: "s3:https://hostname:9999/foobar", Cfg: Config{
Endpoint: "hostname:9999", Endpoint: "hostname:9999",
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"s3:https://hostname:9999/foobar/", Config{ {S: "s3:https://hostname:9999/foobar/", Cfg: Config{
Endpoint: "hostname:9999", Endpoint: "hostname:9999",
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}}, }},
{"s3:http://hostname:9999/foobar", Config{ {S: "s3:http://hostname:9999/foobar", Cfg: Config{
Endpoint: "hostname:9999", Endpoint: "hostname:9999",
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
UseHTTP: true, UseHTTP: true,
Connections: 5, Connections: 5,
}}, }},
{"s3:http://hostname:9999/foobar/", Config{ {S: "s3:http://hostname:9999/foobar/", Cfg: Config{
Endpoint: "hostname:9999", Endpoint: "hostname:9999",
Bucket: "foobar", Bucket: "foobar",
Prefix: "", Prefix: "",
UseHTTP: true, UseHTTP: true,
Connections: 5, Connections: 5,
}}, }},
{"s3:http://hostname:9999/bucket/prefix/directory", Config{ {S: "s3:http://hostname:9999/bucket/prefix/directory", Cfg: Config{
Endpoint: "hostname:9999", Endpoint: "hostname:9999",
Bucket: "bucket", Bucket: "bucket",
Prefix: "prefix/directory", Prefix: "prefix/directory",
UseHTTP: true, UseHTTP: true,
Connections: 5, Connections: 5,
}}, }},
{"s3:http://hostname:9999/bucket/prefix/directory/", Config{ {S: "s3:http://hostname:9999/bucket/prefix/directory/", Cfg: Config{
Endpoint: "hostname:9999", Endpoint: "hostname:9999",
Bucket: "bucket", Bucket: "bucket",
Prefix: "prefix/directory", Prefix: "prefix/directory",
@ -100,19 +99,7 @@ var configTests = []struct {
} }
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
for i, test := range configTests { test.ParseConfigTester(t, ParseConfig, 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
}
}
} }
func TestParseError(t *testing.T) { func TestParseError(t *testing.T) {

View File

@ -120,15 +120,15 @@ func createS3(t testing.TB, cfg MinioTestConfig, tr http.RoundTripper) (be resti
return be, err return be, err
} }
func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite { func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite[MinioTestConfig] {
tr, err := backend.Transport(backend.TransportOptions{}) tr, err := backend.Transport(backend.TransportOptions{})
if err != nil { if err != nil {
t.Fatalf("cannot create transport for tests: %v", err) t.Fatalf("cannot create transport for tests: %v", err)
} }
return &test.Suite{ return &test.Suite[MinioTestConfig]{
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*MinioTestConfig, error) {
cfg := MinioTestConfig{} cfg := MinioTestConfig{}
cfg.tempdir = rtest.TempDir(t) cfg.tempdir = rtest.TempDir(t)
@ -142,13 +142,11 @@ func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
cfg.Config.UseHTTP = true cfg.Config.UseHTTP = true
cfg.Config.KeyID = key cfg.Config.KeyID = key
cfg.Config.Secret = options.NewSecretString(secret) cfg.Config.Secret = options.NewSecretString(secret)
return cfg, nil return &cfg, nil
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg MinioTestConfig) (restic.Backend, error) {
cfg := config.(MinioTestConfig)
be, err := createS3(t, cfg, tr) be, err := createS3(t, cfg, tr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -167,14 +165,12 @@ func newMinioTestSuite(ctx context.Context, t testing.TB) *test.Suite {
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg MinioTestConfig) (restic.Backend, error) {
cfg := config.(MinioTestConfig)
return s3.Open(ctx, cfg.Config, tr) return s3.Open(ctx, cfg.Config, tr)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg MinioTestConfig) error {
cfg := config.(MinioTestConfig)
if cfg.stopServer != nil { if cfg.stopServer != nil {
cfg.stopServer() cfg.stopServer()
} }
@ -217,24 +213,23 @@ func BenchmarkBackendMinio(t *testing.B) {
newMinioTestSuite(ctx, t).RunBenchmarks(t) newMinioTestSuite(ctx, t).RunBenchmarks(t)
} }
func newS3TestSuite(t testing.TB) *test.Suite { func newS3TestSuite(t testing.TB) *test.Suite[s3.Config] {
tr, err := backend.Transport(backend.TransportOptions{}) tr, err := backend.Transport(backend.TransportOptions{})
if err != nil { if err != nil {
t.Fatalf("cannot create transport for tests: %v", err) t.Fatalf("cannot create transport for tests: %v", err)
} }
return &test.Suite{ return &test.Suite[s3.Config]{
// do not use excessive data // do not use excessive data
MinimalData: true, MinimalData: true,
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*s3.Config, error) {
s3cfg, err := s3.ParseConfig(os.Getenv("RESTIC_TEST_S3_REPOSITORY")) cfg, err := s3.ParseConfig(os.Getenv("RESTIC_TEST_S3_REPOSITORY"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg := s3cfg.(s3.Config)
cfg.KeyID = os.Getenv("RESTIC_TEST_S3_KEY") cfg.KeyID = os.Getenv("RESTIC_TEST_S3_KEY")
cfg.Secret = options.NewSecretString(os.Getenv("RESTIC_TEST_S3_SECRET")) cfg.Secret = options.NewSecretString(os.Getenv("RESTIC_TEST_S3_SECRET"))
cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano()) cfg.Prefix = fmt.Sprintf("test-%d", time.Now().UnixNano())
@ -242,9 +237,7 @@ func newS3TestSuite(t testing.TB) *test.Suite {
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg s3.Config) (restic.Backend, error) {
cfg := config.(s3.Config)
be, err := s3.Create(context.TODO(), cfg, tr) be, err := s3.Create(context.TODO(), cfg, tr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -263,15 +256,12 @@ func newS3TestSuite(t testing.TB) *test.Suite {
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg s3.Config) (restic.Backend, error) {
cfg := config.(s3.Config)
return s3.Open(context.TODO(), cfg, tr) return s3.Open(context.TODO(), cfg, tr)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg s3.Config) error {
cfg := config.(s3.Config)
be, err := s3.Open(context.TODO(), cfg, tr) be, err := s3.Open(context.TODO(), cfg, tr)
if err != nil { if err != nil {
return err return err

View File

@ -35,7 +35,7 @@ func init() {
// and sftp:user@host:directory. The directory will be path Cleaned and can // and sftp:user@host:directory. The directory will be path Cleaned and can
// be an absolute path if it starts with a '/' (e.g. // be an absolute path if it starts with a '/' (e.g.
// sftp://user@host//absolute and sftp:user@host:/absolute). // sftp://user@host//absolute and sftp:user@host:/absolute).
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
var user, host, port, dir string var user, host, port, dir string
switch { switch {
case strings.HasPrefix(s, "sftp://"): case strings.HasPrefix(s, "sftp://"):
@ -89,5 +89,5 @@ func ParseConfig(s string) (interface{}, error) {
cfg.Port = port cfg.Port = port
cfg.Path = p cfg.Path = p
return cfg, nil return &cfg, nil
} }

View File

@ -2,94 +2,81 @@ package sftp
import ( import (
"testing" "testing"
"github.com/restic/restic/internal/backend/test"
) )
var configTests = []struct { var configTests = []test.ConfigTestData[Config]{
in string
cfg Config
}{
// first form, user specified sftp://user@host/dir // first form, user specified sftp://user@host/dir
{ {
"sftp://user@host/dir/subdir", S: "sftp://user@host/dir/subdir",
Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5}, Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
}, },
{ {
"sftp://host/dir/subdir", S: "sftp://host/dir/subdir",
Config{Host: "host", Path: "dir/subdir", Connections: 5}, Cfg: Config{Host: "host", Path: "dir/subdir", Connections: 5},
}, },
{ {
"sftp://host//dir/subdir", S: "sftp://host//dir/subdir",
Config{Host: "host", Path: "/dir/subdir", Connections: 5}, Cfg: Config{Host: "host", Path: "/dir/subdir", Connections: 5},
}, },
{ {
"sftp://host:10022//dir/subdir", S: "sftp://host:10022//dir/subdir",
Config{Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5}, Cfg: Config{Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
}, },
{ {
"sftp://user@host:10022//dir/subdir", S: "sftp://user@host:10022//dir/subdir",
Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5}, Cfg: Config{User: "user", Host: "host", Port: "10022", Path: "/dir/subdir", Connections: 5},
}, },
{ {
"sftp://user@host/dir/subdir/../other", S: "sftp://user@host/dir/subdir/../other",
Config{User: "user", Host: "host", Path: "dir/other", Connections: 5}, Cfg: Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
}, },
{ {
"sftp://user@host/dir///subdir", S: "sftp://user@host/dir///subdir",
Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5}, Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
}, },
// IPv6 address. // IPv6 address.
{ {
"sftp://user@[::1]/dir", S: "sftp://user@[::1]/dir",
Config{User: "user", Host: "::1", Path: "dir", Connections: 5}, Cfg: Config{User: "user", Host: "::1", Path: "dir", Connections: 5},
}, },
// IPv6 address with port. // IPv6 address with port.
{ {
"sftp://user@[::1]:22/dir", S: "sftp://user@[::1]:22/dir",
Config{User: "user", Host: "::1", Port: "22", Path: "dir", Connections: 5}, Cfg: Config{User: "user", Host: "::1", Port: "22", Path: "dir", Connections: 5},
}, },
// second form, user specified sftp:user@host:/dir // second form, user specified sftp:user@host:/dir
{ {
"sftp:user@host:/dir/subdir", S: "sftp:user@host:/dir/subdir",
Config{User: "user", Host: "host", Path: "/dir/subdir", Connections: 5}, Cfg: Config{User: "user", Host: "host", Path: "/dir/subdir", Connections: 5},
}, },
{ {
"sftp:user@domain@host:/dir/subdir", S: "sftp:user@domain@host:/dir/subdir",
Config{User: "user@domain", Host: "host", Path: "/dir/subdir", Connections: 5}, Cfg: Config{User: "user@domain", Host: "host", Path: "/dir/subdir", Connections: 5},
}, },
{ {
"sftp:host:../dir/subdir", S: "sftp:host:../dir/subdir",
Config{Host: "host", Path: "../dir/subdir", Connections: 5}, Cfg: Config{Host: "host", Path: "../dir/subdir", Connections: 5},
}, },
{ {
"sftp:user@host:dir/subdir:suffix", S: "sftp:user@host:dir/subdir:suffix",
Config{User: "user", Host: "host", Path: "dir/subdir:suffix", Connections: 5}, Cfg: Config{User: "user", Host: "host", Path: "dir/subdir:suffix", Connections: 5},
}, },
{ {
"sftp:user@host:dir/subdir/../other", S: "sftp:user@host:dir/subdir/../other",
Config{User: "user", Host: "host", Path: "dir/other", Connections: 5}, Cfg: Config{User: "user", Host: "host", Path: "dir/other", Connections: 5},
}, },
{ {
"sftp:user@host:dir///subdir", S: "sftp:user@host:dir///subdir",
Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5}, Cfg: Config{User: "user", Host: "host", Path: "dir/subdir", Connections: 5},
}, },
} }
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
for i, test := range configTests { test.ParseConfigTester(t, ParseConfig, configTests)
cfg, err := ParseConfig(test.in)
if err != nil {
t.Errorf("test %d:%s failed: %v", i, test.in, err)
continue
}
if cfg != test.cfg {
t.Errorf("test %d:\ninput:\n %s\n wrong config, want:\n %v\ngot:\n %v",
i, test.in, test.cfg, cfg)
continue
}
}
} }
var configTestsInvalid = []string{ var configTestsInvalid = []string{

View File

@ -29,10 +29,10 @@ func findSFTPServerBinary() string {
var sftpServer = findSFTPServerBinary() var sftpServer = findSFTPServerBinary()
func newTestSuite(t testing.TB) *test.Suite { func newTestSuite(t testing.TB) *test.Suite[sftp.Config] {
return &test.Suite{ return &test.Suite[sftp.Config]{
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*sftp.Config, error) {
dir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-sftp-") dir, err := os.MkdirTemp(rtest.TestTempDir, "restic-test-sftp-")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -40,7 +40,7 @@ func newTestSuite(t testing.TB) *test.Suite {
t.Logf("create new backend at %v", dir) t.Logf("create new backend at %v", dir)
cfg := sftp.Config{ cfg := &sftp.Config{
Path: dir, Path: dir,
Command: fmt.Sprintf("%q -e", sftpServer), Command: fmt.Sprintf("%q -e", sftpServer),
Connections: 5, Connections: 5,
@ -49,20 +49,17 @@ func newTestSuite(t testing.TB) *test.Suite {
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg sftp.Config) (restic.Backend, error) {
cfg := config.(sftp.Config)
return sftp.Create(context.TODO(), cfg) return sftp.Create(context.TODO(), cfg)
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg sftp.Config) (restic.Backend, error) {
cfg := config.(sftp.Config)
return sftp.Open(context.TODO(), cfg) return sftp.Open(context.TODO(), cfg)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg sftp.Config) error {
cfg := config.(sftp.Config)
if !rtest.TestCleanupTempDirs { if !rtest.TestCleanupTempDirs {
t.Logf("leaving test backend dir at %v", cfg.Path) t.Logf("leaving test backend dir at %v", cfg.Path)
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/options" "github.com/restic/restic/internal/options"
"github.com/restic/restic/internal/restic"
) )
// Config contains basic configuration needed to specify swift location for a swift server // Config contains basic configuration needed to specify swift location for a swift server
@ -50,7 +51,7 @@ func NewConfig() Config {
} }
// ParseConfig parses the string s and extract swift's container name and prefix. // ParseConfig parses the string s and extract swift's container name and prefix.
func ParseConfig(s string) (interface{}, error) { func ParseConfig(s string) (*Config, error) {
if !strings.HasPrefix(s, "swift:") { if !strings.HasPrefix(s, "swift:") {
return nil, errors.New("invalid URL, expected: swift:container-name:/[prefix]") return nil, errors.New("invalid URL, expected: swift:container-name:/[prefix]")
} }
@ -70,48 +71,49 @@ func ParseConfig(s string) (interface{}, error) {
cfg.Container = container cfg.Container = container
cfg.Prefix = prefix cfg.Prefix = prefix
return cfg, nil return &cfg, nil
} }
var _ restic.ApplyEnvironmenter = &Config{}
// ApplyEnvironment saves values from the environment to the config. // ApplyEnvironment saves values from the environment to the config.
func ApplyEnvironment(prefix string, cfg interface{}) error { func (cfg *Config) ApplyEnvironment(prefix string) error {
c := cfg.(*Config)
for _, val := range []struct { for _, val := range []struct {
s *string s *string
env string env string
}{ }{
// v2/v3 specific // v2/v3 specific
{&c.UserName, prefix + "OS_USERNAME"}, {&cfg.UserName, prefix + "OS_USERNAME"},
{&c.APIKey, prefix + "OS_PASSWORD"}, {&cfg.APIKey, prefix + "OS_PASSWORD"},
{&c.Region, prefix + "OS_REGION_NAME"}, {&cfg.Region, prefix + "OS_REGION_NAME"},
{&c.AuthURL, prefix + "OS_AUTH_URL"}, {&cfg.AuthURL, prefix + "OS_AUTH_URL"},
// v3 specific // v3 specific
{&c.UserID, prefix + "OS_USER_ID"}, {&cfg.UserID, prefix + "OS_USER_ID"},
{&c.Domain, prefix + "OS_USER_DOMAIN_NAME"}, {&cfg.Domain, prefix + "OS_USER_DOMAIN_NAME"},
{&c.DomainID, prefix + "OS_USER_DOMAIN_ID"}, {&cfg.DomainID, prefix + "OS_USER_DOMAIN_ID"},
{&c.Tenant, prefix + "OS_PROJECT_NAME"}, {&cfg.Tenant, prefix + "OS_PROJECT_NAME"},
{&c.TenantDomain, prefix + "OS_PROJECT_DOMAIN_NAME"}, {&cfg.TenantDomain, prefix + "OS_PROJECT_DOMAIN_NAME"},
{&c.TenantDomainID, prefix + "OS_PROJECT_DOMAIN_ID"}, {&cfg.TenantDomainID, prefix + "OS_PROJECT_DOMAIN_ID"},
{&c.TrustID, prefix + "OS_TRUST_ID"}, {&cfg.TrustID, prefix + "OS_TRUST_ID"},
// v2 specific // v2 specific
{&c.TenantID, prefix + "OS_TENANT_ID"}, {&cfg.TenantID, prefix + "OS_TENANT_ID"},
{&c.Tenant, prefix + "OS_TENANT_NAME"}, {&cfg.Tenant, prefix + "OS_TENANT_NAME"},
// v1 specific // v1 specific
{&c.AuthURL, prefix + "ST_AUTH"}, {&cfg.AuthURL, prefix + "ST_AUTH"},
{&c.UserName, prefix + "ST_USER"}, {&cfg.UserName, prefix + "ST_USER"},
{&c.APIKey, prefix + "ST_KEY"}, {&cfg.APIKey, prefix + "ST_KEY"},
// Application Credential auth // Application Credential auth
{&c.ApplicationCredentialID, prefix + "OS_APPLICATION_CREDENTIAL_ID"}, {&cfg.ApplicationCredentialID, prefix + "OS_APPLICATION_CREDENTIAL_ID"},
{&c.ApplicationCredentialName, prefix + "OS_APPLICATION_CREDENTIAL_NAME"}, {&cfg.ApplicationCredentialName, prefix + "OS_APPLICATION_CREDENTIAL_NAME"},
// Manual authentication // Manual authentication
{&c.StorageURL, prefix + "OS_STORAGE_URL"}, {&cfg.StorageURL, prefix + "OS_STORAGE_URL"},
{&c.DefaultContainerPolicy, prefix + "SWIFT_DEFAULT_CONTAINER_POLICY"}, {&cfg.DefaultContainerPolicy, prefix + "SWIFT_DEFAULT_CONTAINER_POLICY"},
} { } {
if *val.s == "" { if *val.s == "" {
*val.s = os.Getenv(val.env) *val.s = os.Getenv(val.env)
@ -121,8 +123,8 @@ func ApplyEnvironment(prefix string, cfg interface{}) error {
s *options.SecretString s *options.SecretString
env string env string
}{ }{
{&c.ApplicationCredentialSecret, prefix + "OS_APPLICATION_CREDENTIAL_SECRET"}, {&cfg.ApplicationCredentialSecret, prefix + "OS_APPLICATION_CREDENTIAL_SECRET"},
{&c.AuthToken, prefix + "OS_AUTH_TOKEN"}, {&cfg.AuthToken, prefix + "OS_AUTH_TOKEN"},
} { } {
if val.s.String() == "" { if val.s.String() == "" {
*val.s = options.NewSecretString(os.Getenv(val.env)) *val.s = options.NewSecretString(os.Getenv(val.env))

View File

@ -1,29 +1,30 @@
package swift package swift
import "testing" import (
"testing"
var configTests = []struct { "github.com/restic/restic/internal/backend/test"
s string )
cfg Config
}{ var configTests = []test.ConfigTestData[Config]{
{ {
"swift:cnt1:/", S: "swift:cnt1:/",
Config{ Cfg: Config{
Container: "cnt1", Container: "cnt1",
Prefix: "", Prefix: "",
Connections: 5, Connections: 5,
}, },
}, },
{ {
"swift:cnt2:/prefix", S: "swift:cnt2:/prefix",
Config{Container: "cnt2", Cfg: Config{Container: "cnt2",
Prefix: "prefix", Prefix: "prefix",
Connections: 5, Connections: 5,
}, },
}, },
{ {
"swift:cnt3:/prefix/longer", S: "swift:cnt3:/prefix/longer",
Config{Container: "cnt3", Cfg: Config{Container: "cnt3",
Prefix: "prefix/longer", Prefix: "prefix/longer",
Connections: 5, Connections: 5,
}, },
@ -31,24 +32,7 @@ var configTests = []struct {
} }
func TestParseConfig(t *testing.T) { func TestParseConfig(t *testing.T) {
for _, test := range configTests { test.ParseConfigTester(t, ParseConfig, configTests)
t.Run("", func(t *testing.T) {
v, err := ParseConfig(test.s)
if err != nil {
t.Fatalf("parsing %q failed: %v", test.s, err)
}
cfg, ok := v.(Config)
if !ok {
t.Fatalf("wrong type returned, want Config, got %T", cfg)
}
if cfg != test.cfg {
t.Fatalf("wrong output for %q, want:\n %#v\ngot:\n %#v",
test.s, test.cfg, cfg)
}
})
}
} }
var configTestsInvalid = []string{ var configTestsInvalid = []string{

View File

@ -15,13 +15,13 @@ import (
rtest "github.com/restic/restic/internal/test" rtest "github.com/restic/restic/internal/test"
) )
func newSwiftTestSuite(t testing.TB) *test.Suite { func newSwiftTestSuite(t testing.TB) *test.Suite[swift.Config] {
tr, err := backend.Transport(backend.TransportOptions{}) tr, err := backend.Transport(backend.TransportOptions{})
if err != nil { if err != nil {
t.Fatalf("cannot create transport for tests: %v", err) t.Fatalf("cannot create transport for tests: %v", err)
} }
return &test.Suite{ return &test.Suite[swift.Config]{
// do not use excessive data // do not use excessive data
MinimalData: true, MinimalData: true,
@ -42,14 +42,13 @@ func newSwiftTestSuite(t testing.TB) *test.Suite {
}, },
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig: func() (interface{}, error) { NewConfig: func() (*swift.Config, error) {
swiftcfg, err := swift.ParseConfig(os.Getenv("RESTIC_TEST_SWIFT")) cfg, err := swift.ParseConfig(os.Getenv("RESTIC_TEST_SWIFT"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
cfg := swiftcfg.(swift.Config) if err = cfg.ApplyEnvironment("RESTIC_TEST_"); err != nil {
if err = swift.ApplyEnvironment("RESTIC_TEST_", &cfg); err != nil {
return nil, err return nil, err
} }
cfg.Prefix += fmt.Sprintf("/test-%d", time.Now().UnixNano()) cfg.Prefix += fmt.Sprintf("/test-%d", time.Now().UnixNano())
@ -58,9 +57,7 @@ func newSwiftTestSuite(t testing.TB) *test.Suite {
}, },
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create: func(config interface{}) (restic.Backend, error) { Create: func(cfg swift.Config) (restic.Backend, error) {
cfg := config.(swift.Config)
be, err := swift.Open(context.TODO(), cfg, tr) be, err := swift.Open(context.TODO(), cfg, tr)
if err != nil { if err != nil {
return nil, err return nil, err
@ -79,15 +76,12 @@ func newSwiftTestSuite(t testing.TB) *test.Suite {
}, },
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open: func(config interface{}) (restic.Backend, error) { Open: func(cfg swift.Config) (restic.Backend, error) {
cfg := config.(swift.Config)
return swift.Open(context.TODO(), cfg, tr) return swift.Open(context.TODO(), cfg, tr)
}, },
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup: func(config interface{}) error { Cleanup: func(cfg swift.Config) error {
cfg := config.(swift.Config)
be, err := swift.Open(context.TODO(), cfg, tr) be, err := swift.Open(context.TODO(), cfg, tr)
if err != nil { if err != nil {
return err return err

View File

@ -29,7 +29,7 @@ func remove(t testing.TB, be restic.Backend, h restic.Handle) {
// BenchmarkLoadFile benchmarks the Load() method of a backend by // BenchmarkLoadFile benchmarks the Load() method of a backend by
// loading a complete file. // loading a complete file.
func (s *Suite) BenchmarkLoadFile(t *testing.B) { func (s *Suite[C]) BenchmarkLoadFile(t *testing.B) {
be := s.open(t) be := s.open(t)
defer s.close(t, be) defer s.close(t, be)
@ -64,7 +64,7 @@ func (s *Suite) BenchmarkLoadFile(t *testing.B) {
// BenchmarkLoadPartialFile benchmarks the Load() method of a backend by // BenchmarkLoadPartialFile benchmarks the Load() method of a backend by
// loading the remainder of a file starting at a given offset. // loading the remainder of a file starting at a given offset.
func (s *Suite) BenchmarkLoadPartialFile(t *testing.B) { func (s *Suite[C]) BenchmarkLoadPartialFile(t *testing.B) {
be := s.open(t) be := s.open(t)
defer s.close(t, be) defer s.close(t, be)
@ -101,7 +101,7 @@ func (s *Suite) BenchmarkLoadPartialFile(t *testing.B) {
// BenchmarkLoadPartialFileOffset benchmarks the Load() method of a // BenchmarkLoadPartialFileOffset benchmarks the Load() method of a
// backend by loading a number of bytes of a file starting at a given offset. // backend by loading a number of bytes of a file starting at a given offset.
func (s *Suite) BenchmarkLoadPartialFileOffset(t *testing.B) { func (s *Suite[C]) BenchmarkLoadPartialFileOffset(t *testing.B) {
be := s.open(t) be := s.open(t)
defer s.close(t, be) defer s.close(t, be)
@ -139,7 +139,7 @@ func (s *Suite) BenchmarkLoadPartialFileOffset(t *testing.B) {
} }
// BenchmarkSave benchmarks the Save() method of a backend. // BenchmarkSave benchmarks the Save() method of a backend.
func (s *Suite) BenchmarkSave(t *testing.B) { func (s *Suite[C]) BenchmarkSave(t *testing.B) {
be := s.open(t) be := s.open(t)
defer s.close(t, be) defer s.close(t, be)

View File

@ -0,0 +1,28 @@
package test
import (
"fmt"
"reflect"
"testing"
)
type ConfigTestData[C comparable] struct {
S string
Cfg C
}
func ParseConfigTester[C comparable](t *testing.T, parser func(s string) (*C, error), tests []ConfigTestData[C]) {
for i, test := range tests {
t.Run(fmt.Sprint(i), func(t *testing.T) {
cfg, err := parser(test.S)
if err != nil {
t.Fatalf("%s failed: %v", test.S, err)
}
if !reflect.DeepEqual(*cfg, test.Cfg) {
t.Fatalf("input: %s\n wrong config, want:\n %#v\ngot:\n %#v",
test.S, test.Cfg, *cfg)
}
})
}
}

View File

@ -11,21 +11,21 @@ import (
) )
// Suite implements a test suite for restic backends. // Suite implements a test suite for restic backends.
type Suite struct { type Suite[C any] struct {
// Config should be used to configure the backend. // Config should be used to configure the backend.
Config interface{} Config *C
// NewConfig returns a config for a new temporary backend that will be used in tests. // NewConfig returns a config for a new temporary backend that will be used in tests.
NewConfig func() (interface{}, error) NewConfig func() (*C, error)
// CreateFn is a function that creates a temporary repository for the tests. // CreateFn is a function that creates a temporary repository for the tests.
Create func(cfg interface{}) (restic.Backend, error) Create func(cfg C) (restic.Backend, error)
// OpenFn is a function that opens a previously created temporary repository. // OpenFn is a function that opens a previously created temporary repository.
Open func(cfg interface{}) (restic.Backend, error) Open func(cfg C) (restic.Backend, error)
// CleanupFn removes data created during the tests. // CleanupFn removes data created during the tests.
Cleanup func(cfg interface{}) error Cleanup func(cfg C) error
// MinimalData instructs the tests to not use excessive data. // MinimalData instructs the tests to not use excessive data.
MinimalData bool MinimalData bool
@ -40,7 +40,7 @@ type Suite struct {
} }
// RunTests executes all defined tests as subtests of t. // RunTests executes all defined tests as subtests of t.
func (s *Suite) RunTests(t *testing.T) { func (s *Suite[C]) RunTests(t *testing.T) {
var err error var err error
s.Config, err = s.NewConfig() s.Config, err = s.NewConfig()
if err != nil { if err != nil {
@ -61,7 +61,7 @@ func (s *Suite) RunTests(t *testing.T) {
} }
if s.Cleanup != nil { if s.Cleanup != nil {
if err = s.Cleanup(s.Config); err != nil { if err = s.Cleanup(*s.Config); err != nil {
t.Fatal(err) t.Fatal(err)
} }
} }
@ -72,7 +72,7 @@ type testFunction struct {
Fn func(*testing.T) Fn func(*testing.T)
} }
func (s *Suite) testFuncs(t testing.TB) (funcs []testFunction) { func (s *Suite[C]) testFuncs(t testing.TB) (funcs []testFunction) {
tpe := reflect.TypeOf(s) tpe := reflect.TypeOf(s)
v := reflect.ValueOf(s) v := reflect.ValueOf(s)
@ -107,7 +107,7 @@ type benchmarkFunction struct {
Fn func(*testing.B) Fn func(*testing.B)
} }
func (s *Suite) benchmarkFuncs(t testing.TB) (funcs []benchmarkFunction) { func (s *Suite[C]) benchmarkFuncs(t testing.TB) (funcs []benchmarkFunction) {
tpe := reflect.TypeOf(s) tpe := reflect.TypeOf(s)
v := reflect.ValueOf(s) v := reflect.ValueOf(s)
@ -138,7 +138,7 @@ func (s *Suite) benchmarkFuncs(t testing.TB) (funcs []benchmarkFunction) {
} }
// RunBenchmarks executes all defined benchmarks as subtests of b. // RunBenchmarks executes all defined benchmarks as subtests of b.
func (s *Suite) RunBenchmarks(b *testing.B) { func (s *Suite[C]) RunBenchmarks(b *testing.B) {
var err error var err error
s.Config, err = s.NewConfig() s.Config, err = s.NewConfig()
if err != nil { if err != nil {
@ -158,28 +158,28 @@ func (s *Suite) RunBenchmarks(b *testing.B) {
return return
} }
if err = s.Cleanup(s.Config); err != nil { if err = s.Cleanup(*s.Config); err != nil {
b.Fatal(err) b.Fatal(err)
} }
} }
func (s *Suite) create(t testing.TB) restic.Backend { func (s *Suite[C]) create(t testing.TB) restic.Backend {
be, err := s.Create(s.Config) be, err := s.Create(*s.Config)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return be return be
} }
func (s *Suite) open(t testing.TB) restic.Backend { func (s *Suite[C]) open(t testing.TB) restic.Backend {
be, err := s.Open(s.Config) be, err := s.Open(*s.Config)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return be return be
} }
func (s *Suite) close(t testing.TB, be restic.Backend) { func (s *Suite[C]) close(t testing.TB, be restic.Backend) {
err := be.Close() err := be.Close()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -38,7 +38,7 @@ func beTest(ctx context.Context, be restic.Backend, h restic.Handle) (bool, erro
// TestCreateWithConfig tests that creating a backend in a location which already // TestCreateWithConfig tests that creating a backend in a location which already
// has a config file fails. // has a config file fails.
func (s *Suite) TestCreateWithConfig(t *testing.T) { func (s *Suite[C]) TestCreateWithConfig(t *testing.T) {
b := s.open(t) b := s.open(t)
defer s.close(t, b) defer s.close(t, b)
@ -57,7 +57,7 @@ func (s *Suite) TestCreateWithConfig(t *testing.T) {
store(t, b, restic.ConfigFile, []byte("test config")) store(t, b, restic.ConfigFile, []byte("test config"))
// now create the backend again, this must fail // now create the backend again, this must fail
_, err = s.Create(s.Config) _, err = s.Create(*s.Config)
if err == nil { if err == nil {
t.Fatalf("expected error not found for creating a backend with an existing config file") t.Fatalf("expected error not found for creating a backend with an existing config file")
} }
@ -70,7 +70,7 @@ func (s *Suite) TestCreateWithConfig(t *testing.T) {
} }
// TestLocation tests that a location string is returned. // TestLocation tests that a location string is returned.
func (s *Suite) TestLocation(t *testing.T) { func (s *Suite[C]) TestLocation(t *testing.T) {
b := s.open(t) b := s.open(t)
defer s.close(t, b) defer s.close(t, b)
@ -81,7 +81,7 @@ func (s *Suite) TestLocation(t *testing.T) {
} }
// TestConfig saves and loads a config from the backend. // TestConfig saves and loads a config from the backend.
func (s *Suite) TestConfig(t *testing.T) { func (s *Suite[C]) TestConfig(t *testing.T) {
b := s.open(t) b := s.open(t)
defer s.close(t, b) defer s.close(t, b)
@ -118,7 +118,7 @@ func (s *Suite) TestConfig(t *testing.T) {
} }
// TestLoad tests the backend's Load function. // TestLoad tests the backend's Load function.
func (s *Suite) TestLoad(t *testing.T) { func (s *Suite[C]) TestLoad(t *testing.T) {
seedRand(t) seedRand(t)
b := s.open(t) b := s.open(t)
@ -222,8 +222,12 @@ func (s *Suite) TestLoad(t *testing.T) {
test.OK(t, b.Remove(context.TODO(), handle)) test.OK(t, b.Remove(context.TODO(), handle))
} }
type setter interface {
SetListMaxItems(int)
}
// TestList makes sure that the backend implements List() pagination correctly. // TestList makes sure that the backend implements List() pagination correctly.
func (s *Suite) TestList(t *testing.T) { func (s *Suite[C]) TestList(t *testing.T) {
seedRand(t) seedRand(t)
numTestFiles := rand.Intn(20) + 20 numTestFiles := rand.Intn(20) + 20
@ -269,10 +273,6 @@ func (s *Suite) TestList(t *testing.T) {
t.Run(fmt.Sprintf("max-%v", test.maxItems), func(t *testing.T) { t.Run(fmt.Sprintf("max-%v", test.maxItems), func(t *testing.T) {
list2 := make(map[restic.ID]int64) list2 := make(map[restic.ID]int64)
type setter interface {
SetListMaxItems(int)
}
if s, ok := b.(setter); ok { if s, ok := b.(setter); ok {
t.Logf("setting max list items to %d", test.maxItems) t.Logf("setting max list items to %d", test.maxItems)
s.SetListMaxItems(test.maxItems) s.SetListMaxItems(test.maxItems)
@ -326,7 +326,7 @@ func (s *Suite) TestList(t *testing.T) {
} }
// TestListCancel tests that the context is respected and the error is returned by List. // TestListCancel tests that the context is respected and the error is returned by List.
func (s *Suite) TestListCancel(t *testing.T) { func (s *Suite[C]) TestListCancel(t *testing.T) {
seedRand(t) seedRand(t)
numTestFiles := 5 numTestFiles := 5
@ -466,7 +466,7 @@ func (ec errorCloser) Rewind() error {
} }
// TestSave tests saving data in the backend. // TestSave tests saving data in the backend.
func (s *Suite) TestSave(t *testing.T) { func (s *Suite[C]) TestSave(t *testing.T) {
seedRand(t) seedRand(t)
b := s.open(t) b := s.open(t)
@ -582,7 +582,7 @@ func (r *incompleteByteReader) Length() int64 {
} }
// TestSaveError tests saving data in the backend. // TestSaveError tests saving data in the backend.
func (s *Suite) TestSaveError(t *testing.T) { func (s *Suite[C]) TestSaveError(t *testing.T) {
seedRand(t) seedRand(t)
b := s.open(t) b := s.open(t)
@ -621,7 +621,7 @@ func (b *wrongByteReader) Hash() []byte {
} }
// TestSaveWrongHash tests that uploads with a wrong hash fail // TestSaveWrongHash tests that uploads with a wrong hash fail
func (s *Suite) TestSaveWrongHash(t *testing.T) { func (s *Suite[C]) TestSaveWrongHash(t *testing.T) {
seedRand(t) seedRand(t)
b := s.open(t) b := s.open(t)
@ -679,7 +679,7 @@ func testLoad(b restic.Backend, h restic.Handle) error {
}) })
} }
func (s *Suite) delayedRemove(t testing.TB, be restic.Backend, handles ...restic.Handle) error { func (s *Suite[C]) delayedRemove(t testing.TB, be restic.Backend, handles ...restic.Handle) error {
// Some backend (swift, I'm looking at you) may implement delayed // Some backend (swift, I'm looking at you) may implement delayed
// removal of data. Let's wait a bit if this happens. // removal of data. Let's wait a bit if this happens.
@ -746,7 +746,7 @@ func delayedList(t testing.TB, b restic.Backend, tpe restic.FileType, max int, m
} }
// TestBackend tests all functions of the backend. // TestBackend tests all functions of the backend.
func (s *Suite) TestBackend(t *testing.T) { func (s *Suite[C]) TestBackend(t *testing.T) {
b := s.open(t) b := s.open(t)
defer s.close(t, b) defer s.close(t, b)
@ -867,7 +867,7 @@ func (s *Suite) TestBackend(t *testing.T) {
} }
// TestZZZDelete tests the Delete function. The name ensures that this test is executed last. // TestZZZDelete tests the Delete function. The name ensures that this test is executed last.
func (s *Suite) TestZZZDelete(t *testing.T) { func (s *Suite[C]) TestZZZDelete(t *testing.T) {
if !test.TestCleanupTempDirs { if !test.TestCleanupTempDirs {
t.Skipf("not removing backend, TestCleanupTempDirs is false") t.Skipf("not removing backend, TestCleanupTempDirs is false")
} }

View File

@ -80,3 +80,8 @@ type FileInfo struct {
Size int64 Size int64
Name string Name string
} }
// ApplyEnvironmenter fills in a backend configuration from the environment
type ApplyEnvironmenter interface {
ApplyEnvironment(prefix string) error
}