sftp: Integrate command

This commit is contained in:
Alexander Neumann 2017-04-03 21:05:42 +02:00
parent d3b6f75848
commit c26dd6b76f
5 changed files with 83 additions and 37 deletions

View File

@ -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

View File

@ -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).

View File

@ -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 {

View File

@ -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
}

View File

@ -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)
}
})
}