2
2
mirror of https://github.com/octoleo/restic.git synced 2024-11-29 08:14:03 +00:00

backend: Unify backend construction using factory and registry

This unified construction removes most backend-specific code from
global.go. The backend registry will also enable integration tests to
use custom backends if necessary.
This commit is contained in:
Michael Eischer 2023-06-08 13:04:34 +02:00
parent 56836364a4
commit 7d12c29286
16 changed files with 235 additions and 142 deletions

View File

@ -87,9 +87,9 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
return err return err
} }
be, err := create(ctx, repo, gopts.extended) be, err := create(ctx, repo, gopts, gopts.extended)
if err != nil { if err != nil {
return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.Repo), err) return errors.Fatalf("create repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
} }
s, err := repository.New(be, repository.Options{ s, err := repository.New(be, repository.Options{
@ -102,11 +102,11 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
err = s.Init(ctx, version, gopts.password, chunkerPolynomial) err = s.Init(ctx, version, gopts.password, chunkerPolynomial)
if err != nil { if err != nil {
return errors.Fatalf("create key in repository at %s failed: %v\n", location.StripPassword(gopts.Repo), err) return errors.Fatalf("create key in repository at %s failed: %v\n", location.StripPassword(gopts.backends, gopts.Repo), err)
} }
if !gopts.JSON { if !gopts.JSON {
Verbosef("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.Repo)) Verbosef("created restic repository %v at %s", s.Config().ID[:10], location.StripPassword(gopts.backends, gopts.Repo))
if opts.CopyChunkerParameters && chunkerPolynomial != nil { if opts.CopyChunkerParameters && chunkerPolynomial != nil {
Verbosef(" with chunker parameters copied from secondary repository\n") Verbosef(" with chunker parameters copied from secondary repository\n")
} else { } else {
@ -121,7 +121,7 @@ func runInit(ctx context.Context, opts InitOptions, gopts GlobalOptions, args []
status := initSuccess{ status := initSuccess{
MessageType: "initialized", MessageType: "initialized",
ID: s.Config().ID, ID: s.Config().ID,
Repository: location.StripPassword(gopts.Repo), Repository: location.StripPassword(gopts.backends, gopts.Repo),
} }
return json.NewEncoder(globalOptions.stdout).Encode(status) return json.NewEncoder(globalOptions.stdout).Encode(status)
} }

View File

@ -75,6 +75,7 @@ type GlobalOptions struct {
stdout io.Writer stdout io.Writer
stderr io.Writer stderr io.Writer
backends *location.Registry
backendTestHook, backendInnerTestHook backendWrapper backendTestHook, backendInnerTestHook backendWrapper
// verbosity is set as follows: // verbosity is set as follows:
@ -98,6 +99,18 @@ var isReadingPassword bool
var internalGlobalCtx context.Context var internalGlobalCtx context.Context
func init() { func init() {
backends := location.NewRegistry()
backends.Register("b2", b2.NewFactory())
backends.Register("local", local.NewFactory())
backends.Register("sftp", sftp.NewFactory())
backends.Register("s3", s3.NewFactory())
backends.Register("gs", gs.NewFactory())
backends.Register("azure", azure.NewFactory())
backends.Register("swift", swift.NewFactory())
backends.Register("rest", rest.NewFactory())
backends.Register("rclone", rclone.NewFactory())
globalOptions.backends = backends
var cancel context.CancelFunc var cancel context.CancelFunc
internalGlobalCtx, cancel = context.WithCancel(context.Background()) internalGlobalCtx, cancel = context.WithCancel(context.Background())
AddCleanupHandler(func(code int) (int, error) { AddCleanupHandler(func(code int) (int, error) {
@ -554,8 +567,8 @@ func parseConfig(loc location.Location, opts options.Options) (interface{}, erro
// Open the backend specified by a location config. // Open the backend specified by a location config.
func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Options) (restic.Backend, error) { func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Options) (restic.Backend, error) {
debug.Log("parsing location %v", location.StripPassword(s)) debug.Log("parsing location %v", location.StripPassword(gopts.backends, s))
loc, err := location.Parse(s) loc, err := location.Parse(gopts.backends, s)
if err != nil { if err != nil {
return nil, errors.Fatalf("parsing repository location failed: %v", err) return nil, errors.Fatalf("parsing repository location failed: %v", err)
} }
@ -576,32 +589,14 @@ func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Optio
lim := limiter.NewStaticLimiter(gopts.Limits) lim := limiter.NewStaticLimiter(gopts.Limits)
rt = lim.Transport(rt) rt = lim.Transport(rt)
switch loc.Scheme { factory := gopts.backends.Lookup(loc.Scheme)
case "local": if factory == nil {
be, err = local.Open(ctx, *cfg.(*local.Config))
case "sftp":
be, err = sftp.Open(ctx, *cfg.(*sftp.Config))
case "s3":
be, err = s3.Open(ctx, *cfg.(*s3.Config), rt)
case "gs":
be, err = gs.Open(ctx, *cfg.(*gs.Config), rt)
case "azure":
be, err = azure.Open(ctx, *cfg.(*azure.Config), rt)
case "swift":
be, err = swift.Open(ctx, *cfg.(*swift.Config), rt)
case "b2":
be, err = b2.Open(ctx, *cfg.(*b2.Config), rt)
case "rest":
be, err = rest.Open(ctx, *cfg.(*rest.Config), rt)
case "rclone":
be, err = rclone.Open(ctx, *cfg.(*rclone.Config), lim)
default:
return nil, errors.Fatalf("invalid backend: %q", loc.Scheme) return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
} }
be, err = factory.Open(ctx, cfg, rt, lim)
if err != nil { if err != nil {
return nil, errors.Fatalf("unable to open repository at %v: %v", location.StripPassword(s), err) return nil, errors.Fatalf("unable to open repository at %v: %v", location.StripPassword(gopts.backends, s), err)
} }
// wrap with debug logging and connection limiting // wrap with debug logging and connection limiting
@ -623,7 +618,7 @@ func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Optio
// check if config is there // check if config is there
fi, err := be.Stat(ctx, restic.Handle{Type: restic.ConfigFile}) fi, err := be.Stat(ctx, restic.Handle{Type: restic.ConfigFile})
if err != nil { if err != nil {
return nil, errors.Fatalf("unable to open config file: %v\nIs there a repository at the following location?\n%v", err, location.StripPassword(s)) return nil, errors.Fatalf("unable to open config file: %v\nIs there a repository at the following location?\n%v", err, location.StripPassword(gopts.backends, s))
} }
if fi.Size == 0 { if fi.Size == 0 {
@ -634,9 +629,9 @@ func open(ctx context.Context, s string, gopts GlobalOptions, opts options.Optio
} }
// Create the backend specified by URI. // Create the backend specified by URI.
func create(ctx context.Context, s string, opts options.Options) (restic.Backend, error) { func create(ctx context.Context, s string, gopts GlobalOptions, opts options.Options) (restic.Backend, error) {
debug.Log("parsing location %v", s) debug.Log("parsing location %v", s)
loc, err := location.Parse(s) loc, err := location.Parse(gopts.backends, s)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -651,31 +646,12 @@ func create(ctx context.Context, s string, opts options.Options) (restic.Backend
return nil, err return nil, err
} }
var be restic.Backend factory := gopts.backends.Lookup(loc.Scheme)
switch loc.Scheme { if factory == nil {
case "local": return nil, errors.Fatalf("invalid backend: %q", loc.Scheme)
be, err = local.Create(ctx, *cfg.(*local.Config))
case "sftp":
be, err = sftp.Create(ctx, *cfg.(*sftp.Config))
case "s3":
be, err = s3.Create(ctx, *cfg.(*s3.Config), rt)
case "gs":
be, err = gs.Create(ctx, *cfg.(*gs.Config), rt)
case "azure":
be, err = azure.Create(ctx, *cfg.(*azure.Config), rt)
case "swift":
be, err = swift.Open(ctx, *cfg.(*swift.Config), rt)
case "b2":
be, err = b2.Create(ctx, *cfg.(*b2.Config), rt)
case "rest":
be, err = rest.Create(ctx, *cfg.(*rest.Config), rt)
case "rclone":
be, err = rclone.Create(ctx, *cfg.(*rclone.Config))
default:
debug.Log("invalid repository scheme: %v", s)
return nil, errors.Fatalf("invalid scheme %q", loc.Scheme)
} }
be, err := factory.Create(ctx, cfg, rt, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -206,6 +206,8 @@ func withTestEnvironment(t testing.TB) (env *testEnvironment, cleanup func()) {
// replace this hook with "nil" if listing a filetype more than once is necessary // replace this hook with "nil" if listing a filetype more than once is necessary
backendTestHook: func(r restic.Backend) (restic.Backend, error) { return newOrderedListOnceBackend(r), nil }, backendTestHook: func(r restic.Backend) (restic.Backend, error) { return newOrderedListOnceBackend(r), nil },
// start with default set of backends
backends: globalOptions.backends,
} }
// always overwrite global options // always overwrite global options

View File

@ -14,6 +14,7 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -43,6 +44,10 @@ const defaultListMaxItems = 5000
// make sure that *Backend implements backend.Backend // make sure that *Backend implements backend.Backend
var _ restic.Backend = &Backend{} var _ restic.Backend = &Backend{}
func NewFactory() location.Factory {
return location.NewHTTPBackendFactory(ParseConfig, location.NoPassword, Create, Open)
}
func open(cfg Config, rt http.RoundTripper) (*Backend, error) { func open(cfg Config, rt http.RoundTripper) (*Backend, error) {
debug.Log("open, config %#v", cfg) debug.Log("open, config %#v", cfg)
var client *azContainer.Client var client *azContainer.Client

View File

@ -11,6 +11,7 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -36,6 +37,10 @@ const defaultListMaxItems = 10 * 1000
// ensure statically that *b2Backend implements restic.Backend. // ensure statically that *b2Backend implements restic.Backend.
var _ restic.Backend = &b2Backend{} var _ restic.Backend = &b2Backend{}
func NewFactory() location.Factory {
return location.NewHTTPBackendFactory(ParseConfig, location.NoPassword, Create, Open)
}
type sniffingRoundTripper struct { type sniffingRoundTripper struct {
sync.Mutex sync.Mutex
lastErr error lastErr error

View File

@ -15,6 +15,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -47,6 +48,10 @@ type Backend struct {
// Ensure that *Backend implements restic.Backend. // Ensure that *Backend implements restic.Backend.
var _ restic.Backend = &Backend{} var _ restic.Backend = &Backend{}
func NewFactory() location.Factory {
return location.NewHTTPBackendFactory(ParseConfig, location.NoPassword, Create, Open)
}
func getStorageClient(rt http.RoundTripper) (*storage.Client, error) { func getStorageClient(rt http.RoundTripper) (*storage.Client, error) {
// create a new HTTP client // create a new HTTP client
httpClient := &http.Client{ httpClient := &http.Client{

View File

@ -10,6 +10,8 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/limiter"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/fs" "github.com/restic/restic/internal/fs"
@ -28,6 +30,14 @@ type Local struct {
// ensure statically that *Local implements restic.Backend. // ensure statically that *Local implements restic.Backend.
var _ restic.Backend = &Local{} var _ restic.Backend = &Local{}
func NewFactory() location.Factory {
return location.NewLimitedBackendFactory(ParseConfig, location.NoPassword, func(ctx context.Context, cfg Config, _ limiter.Limiter) (*Local, error) {
return Create(ctx, cfg)
}, func(ctx context.Context, cfg Config, _ limiter.Limiter) (*Local, error) {
return Open(ctx, cfg)
})
}
const defaultLayout = "default" const defaultLayout = "default"
func open(ctx context.Context, cfg Config) (*Local, error) { func open(ctx context.Context, cfg Config) (*Local, error) {

View File

@ -4,15 +4,6 @@ package location
import ( import (
"strings" "strings"
"github.com/restic/restic/internal/backend/azure"
"github.com/restic/restic/internal/backend/b2"
"github.com/restic/restic/internal/backend/gs"
"github.com/restic/restic/internal/backend/local"
"github.com/restic/restic/internal/backend/rclone"
"github.com/restic/restic/internal/backend/rest"
"github.com/restic/restic/internal/backend/s3"
"github.com/restic/restic/internal/backend/sftp"
"github.com/restic/restic/internal/backend/swift"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
) )
@ -23,34 +14,8 @@ type Location struct {
Config interface{} Config interface{}
} }
type parser struct { // NoPassword returns the repository location unchanged (there's no sensitive information there)
scheme string func NoPassword(s string) string {
parse func(string) (interface{}, error)
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
// is the fallback and should always be set to the local backend.
var parsers = []parser{
{"b2", configToAny(b2.ParseConfig), noPassword},
{"local", configToAny(local.ParseConfig), noPassword},
{"sftp", configToAny(sftp.ParseConfig), noPassword},
{"s3", configToAny(s3.ParseConfig), noPassword},
{"gs", configToAny(gs.ParseConfig), noPassword},
{"azure", configToAny(azure.ParseConfig), noPassword},
{"swift", configToAny(swift.ParseConfig), noPassword},
{"rest", configToAny(rest.ParseConfig), rest.StripPassword},
{"rclone", configToAny(rclone.ParseConfig), noPassword},
}
// noPassword returns the repository location unchanged (there's no sensitive information there)
func noPassword(s string) string {
return s return s
} }
@ -88,16 +53,13 @@ func isPath(s string) bool {
// starts with a backend name followed by a colon, that backend's Parse() // starts with a backend name followed by a colon, that backend's Parse()
// function is called. Otherwise, the local backend is used which interprets s // function is called. Otherwise, the local backend is used which interprets s
// as the name of a directory. // as the name of a directory.
func Parse(s string) (u Location, err error) { func Parse(registry *Registry, s string) (u Location, err error) {
scheme := extractScheme(s) scheme := extractScheme(s)
u.Scheme = scheme u.Scheme = scheme
for _, parser := range parsers { factory := registry.Lookup(scheme)
if parser.scheme != scheme { if factory != nil {
continue u.Config, err = factory.ParseConfig(s)
}
u.Config, err = parser.parse(s)
if err != nil { if err != nil {
return Location{}, err return Location{}, err
} }
@ -111,7 +73,12 @@ func Parse(s string) (u Location, err error) {
} }
u.Scheme = "local" u.Scheme = "local"
u.Config, err = local.ParseConfig("local:" + s) factory = registry.Lookup(u.Scheme)
if factory == nil {
return Location{}, errors.New("local backend not available")
}
u.Config, err = factory.ParseConfig("local:" + s)
if err != nil { if err != nil {
return Location{}, err return Location{}, err
} }
@ -120,14 +87,12 @@ func Parse(s string) (u Location, err error) {
} }
// StripPassword returns a displayable version of a repository location (with any sensitive information removed) // StripPassword returns a displayable version of a repository location (with any sensitive information removed)
func StripPassword(s string) string { func StripPassword(registry *Registry, s string) string {
scheme := extractScheme(s) scheme := extractScheme(s)
for _, parser := range parsers { factory := registry.Lookup(scheme)
if parser.scheme != scheme { if factory != nil {
continue return factory.StripPassword(s)
}
return parser.stripPassword(s)
} }
return s return s
} }

View File

@ -1,4 +1,4 @@
package location package location_test
import ( import (
"net/url" "net/url"
@ -7,6 +7,7 @@ import (
"github.com/restic/restic/internal/backend/b2" "github.com/restic/restic/internal/backend/b2"
"github.com/restic/restic/internal/backend/local" "github.com/restic/restic/internal/backend/local"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/backend/rest" "github.com/restic/restic/internal/backend/rest"
"github.com/restic/restic/internal/backend/s3" "github.com/restic/restic/internal/backend/s3"
"github.com/restic/restic/internal/backend/sftp" "github.com/restic/restic/internal/backend/sftp"
@ -24,11 +25,11 @@ func parseURL(s string) *url.URL {
var parseTests = []struct { var parseTests = []struct {
s string s string
u Location u location.Location
}{ }{
{ {
"local:/srv/repo", "local:/srv/repo",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "/srv/repo", Path: "/srv/repo",
Connections: 2, Connections: 2,
@ -37,7 +38,7 @@ var parseTests = []struct {
}, },
{ {
"local:dir1/dir2", "local:dir1/dir2",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "dir1/dir2", Path: "dir1/dir2",
Connections: 2, Connections: 2,
@ -46,7 +47,7 @@ var parseTests = []struct {
}, },
{ {
"local:dir1/dir2", "local:dir1/dir2",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "dir1/dir2", Path: "dir1/dir2",
Connections: 2, Connections: 2,
@ -55,7 +56,7 @@ var parseTests = []struct {
}, },
{ {
"dir1/dir2", "dir1/dir2",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "dir1/dir2", Path: "dir1/dir2",
Connections: 2, Connections: 2,
@ -64,7 +65,7 @@ var parseTests = []struct {
}, },
{ {
"/dir1/dir2", "/dir1/dir2",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "/dir1/dir2", Path: "/dir1/dir2",
Connections: 2, Connections: 2,
@ -73,7 +74,7 @@ var parseTests = []struct {
}, },
{ {
"local:../dir1/dir2", "local:../dir1/dir2",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "../dir1/dir2", Path: "../dir1/dir2",
Connections: 2, Connections: 2,
@ -82,7 +83,7 @@ var parseTests = []struct {
}, },
{ {
"/dir1/dir2", "/dir1/dir2",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "/dir1/dir2", Path: "/dir1/dir2",
Connections: 2, Connections: 2,
@ -91,7 +92,7 @@ var parseTests = []struct {
}, },
{ {
"/dir1:foobar/dir2", "/dir1:foobar/dir2",
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: "/dir1:foobar/dir2", Path: "/dir1:foobar/dir2",
Connections: 2, Connections: 2,
@ -100,7 +101,7 @@ var parseTests = []struct {
}, },
{ {
`\dir1\foobar\dir2`, `\dir1\foobar\dir2`,
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: `\dir1\foobar\dir2`, Path: `\dir1\foobar\dir2`,
Connections: 2, Connections: 2,
@ -109,7 +110,7 @@ var parseTests = []struct {
}, },
{ {
`c:\dir1\foobar\dir2`, `c:\dir1\foobar\dir2`,
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: `c:\dir1\foobar\dir2`, Path: `c:\dir1\foobar\dir2`,
Connections: 2, Connections: 2,
@ -118,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.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,
@ -127,7 +128,7 @@ var parseTests = []struct {
}, },
{ {
`c:/dir1/foobar/dir2`, `c:/dir1/foobar/dir2`,
Location{Scheme: "local", location.Location{Scheme: "local",
Config: &local.Config{ Config: &local.Config{
Path: `c:/dir1/foobar/dir2`, Path: `c:/dir1/foobar/dir2`,
Connections: 2, Connections: 2,
@ -136,7 +137,7 @@ var parseTests = []struct {
}, },
{ {
"sftp:user@host:/srv/repo", "sftp:user@host:/srv/repo",
Location{Scheme: "sftp", location.Location{Scheme: "sftp",
Config: &sftp.Config{ Config: &sftp.Config{
User: "user", User: "user",
Host: "host", Host: "host",
@ -147,7 +148,7 @@ var parseTests = []struct {
}, },
{ {
"sftp:host:/srv/repo", "sftp:host:/srv/repo",
Location{Scheme: "sftp", location.Location{Scheme: "sftp",
Config: &sftp.Config{ Config: &sftp.Config{
User: "", User: "",
Host: "host", Host: "host",
@ -158,7 +159,7 @@ var parseTests = []struct {
}, },
{ {
"sftp://user@host/srv/repo", "sftp://user@host/srv/repo",
Location{Scheme: "sftp", location.Location{Scheme: "sftp",
Config: &sftp.Config{ Config: &sftp.Config{
User: "user", User: "user",
Host: "host", Host: "host",
@ -169,7 +170,7 @@ var parseTests = []struct {
}, },
{ {
"sftp://user@host//srv/repo", "sftp://user@host//srv/repo",
Location{Scheme: "sftp", location.Location{Scheme: "sftp",
Config: &sftp.Config{ Config: &sftp.Config{
User: "user", User: "user",
Host: "host", Host: "host",
@ -181,7 +182,7 @@ var parseTests = []struct {
{ {
"s3://eu-central-1/bucketname", "s3://eu-central-1/bucketname",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "bucketname", Bucket: "bucketname",
@ -192,7 +193,7 @@ var parseTests = []struct {
}, },
{ {
"s3://hostname.foo/bucketname", "s3://hostname.foo/bucketname",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "bucketname", Bucket: "bucketname",
@ -203,7 +204,7 @@ var parseTests = []struct {
}, },
{ {
"s3://hostname.foo/bucketname/prefix/directory", "s3://hostname.foo/bucketname/prefix/directory",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "bucketname", Bucket: "bucketname",
@ -214,7 +215,7 @@ var parseTests = []struct {
}, },
{ {
"s3:eu-central-1/repo", "s3:eu-central-1/repo",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "repo", Bucket: "repo",
@ -225,7 +226,7 @@ var parseTests = []struct {
}, },
{ {
"s3:eu-central-1/repo/prefix/directory", "s3:eu-central-1/repo/prefix/directory",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "eu-central-1", Endpoint: "eu-central-1",
Bucket: "repo", Bucket: "repo",
@ -236,7 +237,7 @@ var parseTests = []struct {
}, },
{ {
"s3:https://hostname.foo/repo", "s3:https://hostname.foo/repo",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "repo", Bucket: "repo",
@ -247,7 +248,7 @@ var parseTests = []struct {
}, },
{ {
"s3:https://hostname.foo/repo/prefix/directory", "s3:https://hostname.foo/repo/prefix/directory",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "repo", Bucket: "repo",
@ -258,7 +259,7 @@ var parseTests = []struct {
}, },
{ {
"s3:http://hostname.foo/repo", "s3:http://hostname.foo/repo",
Location{Scheme: "s3", location.Location{Scheme: "s3",
Config: &s3.Config{ Config: &s3.Config{
Endpoint: "hostname.foo", Endpoint: "hostname.foo",
Bucket: "repo", Bucket: "repo",
@ -270,7 +271,7 @@ var parseTests = []struct {
}, },
{ {
"swift:container17:/", "swift:container17:/",
Location{Scheme: "swift", location.Location{Scheme: "swift",
Config: &swift.Config{ Config: &swift.Config{
Container: "container17", Container: "container17",
Prefix: "", Prefix: "",
@ -280,7 +281,7 @@ var parseTests = []struct {
}, },
{ {
"swift:container17:/prefix97", "swift:container17:/prefix97",
Location{Scheme: "swift", location.Location{Scheme: "swift",
Config: &swift.Config{ Config: &swift.Config{
Container: "container17", Container: "container17",
Prefix: "prefix97", Prefix: "prefix97",
@ -290,7 +291,7 @@ var parseTests = []struct {
}, },
{ {
"rest:http://hostname.foo:1234/", "rest:http://hostname.foo:1234/",
Location{Scheme: "rest", location.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,
@ -298,7 +299,7 @@ var parseTests = []struct {
}, },
}, },
{ {
"b2:bucketname:/prefix", Location{Scheme: "b2", "b2:bucketname:/prefix", location.Location{Scheme: "b2",
Config: &b2.Config{ Config: &b2.Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "prefix", Prefix: "prefix",
@ -307,7 +308,7 @@ var parseTests = []struct {
}, },
}, },
{ {
"b2:bucketname", Location{Scheme: "b2", "b2:bucketname", location.Location{Scheme: "b2",
Config: &b2.Config{ Config: &b2.Config{
Bucket: "bucketname", Bucket: "bucketname",
Prefix: "", Prefix: "",
@ -320,7 +321,7 @@ var parseTests = []struct {
func TestParse(t *testing.T) { func TestParse(t *testing.T) {
for i, test := range parseTests { for i, test := range parseTests {
t.Run(test.s, func(t *testing.T) { t.Run(test.s, func(t *testing.T) {
u, err := Parse(test.s) u, err := location.Parse(test.s)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@ -346,7 +347,7 @@ func TestInvalidScheme(t *testing.T) {
for _, s := range invalidSchemes { for _, s := range invalidSchemes {
t.Run(s, func(t *testing.T) { t.Run(s, func(t *testing.T) {
_, err := Parse(s) _, err := location.Parse(s)
if err == nil { if err == nil {
t.Fatalf("error for invalid location %q not found", s) t.Fatalf("error for invalid location %q not found", s)
} }

View File

@ -0,0 +1,94 @@
package location
import (
"context"
"net/http"
"github.com/restic/restic/internal/backend/limiter"
"github.com/restic/restic/internal/restic"
)
type Registry struct {
factories map[string]Factory
}
func NewRegistry() *Registry {
return &Registry{
factories: make(map[string]Factory),
}
}
func (r *Registry) Register(scheme string, factory Factory) {
if r.factories[scheme] != nil {
panic("duplicate backend")
}
r.factories[scheme] = factory
}
func (r *Registry) Lookup(scheme string) Factory {
return r.factories[scheme]
}
type Factory interface {
ParseConfig(s string) (interface{}, error)
StripPassword(s string) string
Create(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (restic.Backend, error)
Open(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (restic.Backend, error)
}
type GenericBackendFactory[C any, T restic.Backend] struct {
parseConfigFn func(s string) (*C, error)
stripPasswordFn func(s string) string
createFn func(ctx context.Context, cfg C, rt http.RoundTripper, lim limiter.Limiter) (T, error)
openFn func(ctx context.Context, cfg C, rt http.RoundTripper, lim limiter.Limiter) (T, error)
}
func (f *GenericBackendFactory[C, T]) ParseConfig(s string) (interface{}, error) {
return f.parseConfigFn(s)
}
func (f *GenericBackendFactory[C, T]) StripPassword(s string) string {
if f.stripPasswordFn != nil {
return f.stripPasswordFn(s)
}
return s
}
func (f *GenericBackendFactory[C, T]) Create(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (restic.Backend, error) {
return f.createFn(ctx, *cfg.(*C), rt, lim)
}
func (f *GenericBackendFactory[C, T]) Open(ctx context.Context, cfg interface{}, rt http.RoundTripper, lim limiter.Limiter) (restic.Backend, error) {
return f.openFn(ctx, *cfg.(*C), rt, lim)
}
func NewHTTPBackendFactory[C any, T restic.Backend](parseConfigFn func(s string) (*C, error),
stripPasswordFn func(s string) string,
createFn func(ctx context.Context, cfg C, rt http.RoundTripper) (T, error),
openFn func(ctx context.Context, cfg C, rt http.RoundTripper) (T, error)) *GenericBackendFactory[C, T] {
return &GenericBackendFactory[C, T]{
parseConfigFn: parseConfigFn,
stripPasswordFn: stripPasswordFn,
createFn: func(ctx context.Context, cfg C, rt http.RoundTripper, _ limiter.Limiter) (T, error) {
return createFn(ctx, cfg, rt)
},
openFn: func(ctx context.Context, cfg C, rt http.RoundTripper, _ limiter.Limiter) (T, error) {
return openFn(ctx, cfg, rt)
},
}
}
func NewLimitedBackendFactory[C any, T restic.Backend](parseConfigFn func(s string) (*C, error),
stripPasswordFn func(s string) string,
createFn func(ctx context.Context, cfg C, lim limiter.Limiter) (T, error),
openFn func(ctx context.Context, cfg C, lim limiter.Limiter) (T, error)) *GenericBackendFactory[C, T] {
return &GenericBackendFactory[C, T]{
parseConfigFn: parseConfigFn,
stripPasswordFn: stripPasswordFn,
createFn: func(ctx context.Context, cfg C, _ http.RoundTripper, lim limiter.Limiter) (T, error) {
return createFn(ctx, cfg, lim)
},
openFn: func(ctx context.Context, cfg C, _ http.RoundTripper, lim limiter.Limiter) (T, error) {
return openFn(ctx, cfg, lim)
},
}
}

View File

@ -19,6 +19,7 @@ import (
"github.com/cenkalti/backoff/v4" "github.com/cenkalti/backoff/v4"
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/limiter" "github.com/restic/restic/internal/backend/limiter"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/backend/rest" "github.com/restic/restic/internal/backend/rest"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
@ -36,6 +37,10 @@ type Backend struct {
conn *StdioConn conn *StdioConn
} }
func NewFactory() location.Factory {
return location.NewLimitedBackendFactory(ParseConfig, location.NoPassword, Create, Open)
}
// run starts command with args and initializes the StdioConn. // run starts command with args and initializes the StdioConn.
func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, chan struct{}, func() error, error) { func run(command string, args ...string) (*StdioConn, *sync.WaitGroup, chan struct{}, func() error, error) {
cmd := exec.Command(command, args...) cmd := exec.Command(command, args...)
@ -283,8 +288,8 @@ func Open(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, error
} }
// Create initializes a new restic repo with rclone. // Create initializes a new restic repo with rclone.
func Create(ctx context.Context, cfg Config) (*Backend, error) { func Create(ctx context.Context, cfg Config, lim limiter.Limiter) (*Backend, error) {
be, err := newBackend(ctx, cfg, nil) be, err := newBackend(ctx, cfg, lim)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -27,7 +27,7 @@ func newTestSuite(t testing.TB) *test.Suite[rclone.Config] {
// 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 rclone.Config) (restic.Backend, error) { Create: func(cfg rclone.Config) (restic.Backend, error) {
t.Logf("Create()") t.Logf("Create()")
be, err := rclone.Create(context.TODO(), cfg) be, err := rclone.Create(context.TODO(), cfg, nil)
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 {
t.Skipf("program %q not found", e.Name) t.Skipf("program %q not found", e.Name)

View File

@ -13,6 +13,7 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -29,6 +30,10 @@ type Backend struct {
layout.Layout layout.Layout
} }
func NewFactory() location.Factory {
return location.NewHTTPBackendFactory(ParseConfig, StripPassword, Create, Open)
}
// the REST API protocol version is decided by HTTP request headers, these are the constants. // the REST API protocol version is decided by HTTP request headers, these are the constants.
const ( const (
ContentTypeV1 = "application/vnd.x.restic.rest.v1" ContentTypeV1 = "application/vnd.x.restic.rest.v1"

View File

@ -13,6 +13,7 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -31,6 +32,10 @@ type Backend struct {
// make sure that *Backend implements backend.Backend // make sure that *Backend implements backend.Backend
var _ restic.Backend = &Backend{} var _ restic.Backend = &Backend{}
func NewFactory() location.Factory {
return location.NewHTTPBackendFactory(ParseConfig, location.NoPassword, Create, Open)
}
const defaultLayout = "default" const defaultLayout = "default"
func open(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, error) { func open(ctx context.Context, cfg Config, rt http.RoundTripper) (*Backend, error) {

View File

@ -15,6 +15,8 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/limiter"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -41,6 +43,14 @@ type SFTP struct {
var _ restic.Backend = &SFTP{} var _ restic.Backend = &SFTP{}
func NewFactory() location.Factory {
return location.NewLimitedBackendFactory(ParseConfig, location.NoPassword, func(ctx context.Context, cfg Config, _ limiter.Limiter) (*SFTP, error) {
return Create(ctx, cfg)
}, func(ctx context.Context, cfg Config, _ limiter.Limiter) (*SFTP, error) {
return Open(ctx, cfg)
})
}
const defaultLayout = "default" const defaultLayout = "default"
func startClient(cfg Config) (*SFTP, error) { func startClient(cfg Config) (*SFTP, error) {

View File

@ -15,6 +15,7 @@ import (
"github.com/restic/restic/internal/backend" "github.com/restic/restic/internal/backend"
"github.com/restic/restic/internal/backend/layout" "github.com/restic/restic/internal/backend/layout"
"github.com/restic/restic/internal/backend/location"
"github.com/restic/restic/internal/debug" "github.com/restic/restic/internal/debug"
"github.com/restic/restic/internal/errors" "github.com/restic/restic/internal/errors"
"github.com/restic/restic/internal/restic" "github.com/restic/restic/internal/restic"
@ -34,6 +35,10 @@ type beSwift struct {
// ensure statically that *beSwift implements restic.Backend. // ensure statically that *beSwift implements restic.Backend.
var _ restic.Backend = &beSwift{} var _ restic.Backend = &beSwift{}
func NewFactory() location.Factory {
return location.NewHTTPBackendFactory(ParseConfig, location.NoPassword, Open, Open)
}
// Open opens the swift backend at a container in region. The container is // Open opens the swift backend at a container in region. The container is
// created if it does not exist yet. // created if it does not exist yet.
func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend, error) { func Open(ctx context.Context, cfg Config, rt http.RoundTripper) (restic.Backend, error) {