diff --git a/src/restic/backend/sftp/config.go b/src/restic/backend/sftp/config.go index c5b65e639..3d029a0dd 100644 --- a/src/restic/backend/sftp/config.go +++ b/src/restic/backend/sftp/config.go @@ -11,6 +11,7 @@ import ( // Config collects all information required to connect to an sftp server. type Config struct { User, Host, Dir string + Command string `option:"command"` } // ParseConfig parses the string s and extracts the sftp config. The diff --git a/src/restic/backend/sftp/sftp.go b/src/restic/backend/sftp/sftp.go index 8894cdc85..dbeaf537f 100644 --- a/src/restic/backend/sftp/sftp.go +++ b/src/restic/backend/sftp/sftp.go @@ -37,6 +37,7 @@ type SFTP struct { var _ restic.Backend = &SFTP{} func startClient(program string, args ...string) (*SFTP, error) { + debug.Log("start client %v %v", program, args) // Connect to a remote host and request the sftp subsystem via the 'ssh' // command. This assumes that passwordless login is correctly configured. cmd := exec.Command(program, args...) @@ -114,11 +115,11 @@ func (r *SFTP) clientError() error { return nil } -// Open opens an sftp backend. When the command is started via +// open opens an sftp backend. When the command is started via // exec.Command, it is expected to speak sftp on stdin/stdout. The backend // is expected at the given path. `dir` must be delimited by forward slashes // ("/"), which is required by sftp. -func Open(dir string, program string, args ...string) (*SFTP, error) { +func open(dir string, program string, args ...string) (*SFTP, error) { debug.Log("open backend with program %v, %v at %v", program, args, dir) sftp, err := startClient(program, args...) if err != nil { @@ -155,15 +156,25 @@ func buildSSHCommand(cfg Config) []string { // OpenWithConfig opens an sftp backend as described by the config by running // "ssh" with the appropriate arguments. func OpenWithConfig(cfg Config) (*SFTP, error) { - debug.Log("open with config %v", cfg) - return Open(cfg.Dir, "ssh", buildSSHCommand(cfg)...) + debug.Log("config %#v", cfg) + + if cfg.Command == "" { + return open(cfg.Dir, "ssh", buildSSHCommand(cfg)...) + } + + cmd, args, err := SplitShellArgs(cfg.Command) + if err != nil { + return nil, err + } + + return open(cfg.Dir, cmd, args...) } -// Create creates all the necessary files and directories for a new sftp +// create creates all the necessary files and directories for a new sftp // backend at dir. Afterwards a new config blob should be created. `dir` must // be delimited by forward slashes ("/"), which is required by sftp. -func Create(dir string, program string, args ...string) (*SFTP, error) { - debug.Log("%v %v", program, args) +func create(dir string, program string, args ...string) (*SFTP, error) { + debug.Log("create() %v %v", program, args) sftp, err := startClient(program, args...) if err != nil { return nil, err @@ -178,6 +189,7 @@ func Create(dir string, program string, args ...string) (*SFTP, error) { // create paths for data, refs and temp blobs for _, d := range paths(dir) { err = sftp.mkdirAll(d, backend.Modes.Dir) + debug.Log("mkdirAll %v -> %v", d, err) if err != nil { return nil, err } @@ -189,14 +201,23 @@ func Create(dir string, program string, args ...string) (*SFTP, error) { } // open backend - return Open(dir, program, args...) + return open(dir, program, args...) } // CreateWithConfig creates an sftp backend as described by the config by running // "ssh" with the appropriate arguments. func CreateWithConfig(cfg Config) (*SFTP, error) { - debug.Log("config %v", cfg) - return Create(cfg.Dir, "ssh", buildSSHCommand(cfg)...) + debug.Log("config %#v", cfg) + if cfg.Command == "" { + return create(cfg.Dir, "ssh", buildSSHCommand(cfg)...) + } + + cmd, args, err := SplitShellArgs(cfg.Command) + if err != nil { + return nil, err + } + + return create(cfg.Dir, cmd, args...) } // Location returns this backend's location (the directory name). diff --git a/src/restic/backend/sftp/sftp_backend_test.go b/src/restic/backend/sftp/sftp_backend_test.go index 567b2cf94..4a12438fe 100644 --- a/src/restic/backend/sftp/sftp_backend_test.go +++ b/src/restic/backend/sftp/sftp_backend_test.go @@ -1,6 +1,7 @@ package sftp_test import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -50,7 +51,9 @@ func init() { return } - args := []string{"-e"} + cfg := sftp.Config{ + Command: fmt.Sprintf("%q -e", sftpserver), + } test.CreateFn = func() (restic.Backend, error) { err := createTempdir() @@ -58,7 +61,9 @@ func init() { return nil, err } - return sftp.Create(tempBackendDir, sftpserver, args...) + cfg.Dir = tempBackendDir + + return sftp.CreateWithConfig(cfg) } test.OpenFn = func() (restic.Backend, error) { @@ -66,7 +71,10 @@ func init() { if err != nil { return nil, err } - return sftp.Open(tempBackendDir, sftpserver, args...) + + cfg.Dir = tempBackendDir + + return sftp.OpenWithConfig(cfg) } test.CleanupFn = func() error { diff --git a/src/restic/backend/sftp/split.go b/src/restic/backend/sftp/split.go index 9be4a5464..b01fb3a93 100644 --- a/src/restic/backend/sftp/split.go +++ b/src/restic/backend/sftp/split.go @@ -43,7 +43,7 @@ func (s *shellSplitter) isSplitChar(c rune) bool { } // SplitShellArgs returns the list of arguments from a shell command string. -func SplitShellArgs(data string) (list []string, err error) { +func SplitShellArgs(data string) (cmd string, args []string, err error) { s := &shellSplitter{} // derived from strings.SplitFunc @@ -51,7 +51,7 @@ func SplitShellArgs(data string) (list []string, err error) { for i, rune := range data { if s.isSplitChar(rune) { if fieldStart >= 0 { - list = append(list, data[fieldStart:i]) + args = append(args, data[fieldStart:i]) fieldStart = -1 } } else if fieldStart == -1 { @@ -59,15 +59,21 @@ func SplitShellArgs(data string) (list []string, err error) { } } if fieldStart >= 0 { // Last field might end at EOF. - list = append(list, data[fieldStart:]) + args = append(args, data[fieldStart:]) } switch s.quote { case '\'': - return nil, errors.New("single-quoted string not terminated") + return "", nil, errors.New("single-quoted string not terminated") case '"': - return nil, errors.New("double-quoted string not terminated") + return "", nil, errors.New("double-quoted string not terminated") } - return list, nil + if len(args) == 0 { + return "", nil, errors.New("command string is empty") + } + + cmd, args = args[0], args[1:] + + return cmd, args, nil } diff --git a/src/restic/backend/sftp/split_test.go b/src/restic/backend/sftp/split_test.go index f2f3cd5f5..06241b29a 100644 --- a/src/restic/backend/sftp/split_test.go +++ b/src/restic/backend/sftp/split_test.go @@ -8,56 +8,62 @@ import ( func TestShellSplitter(t *testing.T) { var tests = []struct { data string - want []string + cmd string + args []string }{ { `foo`, - []string{"foo"}, + "foo", []string{}, }, { `'foo'`, - []string{"foo"}, + "foo", []string{}, }, { `foo bar baz`, - []string{"foo", "bar", "baz"}, + "foo", []string{"bar", "baz"}, }, { `foo 'bar' baz`, - []string{"foo", "bar", "baz"}, + "foo", []string{"bar", "baz"}, }, { - `foo 'bar box' baz`, - []string{"foo", "bar box", "baz"}, + `'bar box' baz`, + "bar box", []string{"baz"}, }, { `"bar 'box'" baz`, - []string{"bar 'box'", "baz"}, + "bar 'box'", []string{"baz"}, }, { `'bar "box"' baz`, - []string{`bar "box"`, "baz"}, + `bar "box"`, []string{"baz"}, }, { `\"bar box baz`, - []string{`"bar`, "box", "baz"}, + `"bar`, []string{"box", "baz"}, }, { `"bar/foo/x" "box baz"`, - []string{"bar/foo/x", "box baz"}, + "bar/foo/x", []string{"box baz"}, }, } for _, test := range tests { t.Run("", func(t *testing.T) { - res, err := SplitShellArgs(test.data) + cmd, args, err := SplitShellArgs(test.data) if err != nil { t.Fatal(err) } - if !reflect.DeepEqual(res, test.want) { - t.Fatalf("wrong data returned, want:\n %#v\ngot:\n %#v", - test.want, res) + if cmd != test.cmd { + t.Fatalf("wrong cmd returned, want:\n %#v\ngot:\n %#v", + test.cmd, cmd) + } + + if !reflect.DeepEqual(args, test.args) { + t.Fatalf("wrong args returned, want:\n %#v\ngot:\n %#v", + test.args, args) } }) } @@ -88,7 +94,7 @@ func TestShellSplitterInvalid(t *testing.T) { for _, test := range tests { t.Run("", func(t *testing.T) { - res, err := SplitShellArgs(test.data) + cmd, args, err := SplitShellArgs(test.data) if err == nil { t.Fatalf("expected error not found: %v", test.err) } @@ -97,8 +103,12 @@ func TestShellSplitterInvalid(t *testing.T) { t.Fatalf("expected error not found, want:\n %q\ngot:\n %q", test.err, err.Error()) } - if len(res) > 0 { - t.Fatalf("splitter returned fields from invalid data: %v", res) + if cmd != "" { + t.Fatalf("splitter returned cmd from invalid data: %v", cmd) + } + + if len(args) > 0 { + t.Fatalf("splitter returned fields from invalid data: %v", args) } }) }