restic/src/restic/backend/sftp/sftp.go

571 lines
12 KiB
Go
Raw Normal View History

2015-03-28 10:50:23 +00:00
package sftp
2014-10-04 17:20:15 +00:00
import (
"bufio"
2014-10-04 17:20:15 +00:00
"fmt"
"io"
"os"
"os/exec"
"path"
2016-08-31 20:39:36 +00:00
"restic"
"strings"
"time"
2014-10-04 17:20:15 +00:00
2016-09-01 20:17:37 +00:00
"restic/errors"
"restic/backend"
"restic/debug"
"github.com/pkg/sftp"
2014-10-04 17:20:15 +00:00
)
const (
tempfileRandomSuffixLength = 10
)
2016-01-24 19:23:50 +00:00
// SFTP is a backend in a directory accessed via SFTP.
2014-10-04 17:20:15 +00:00
type SFTP struct {
c *sftp.Client
p string
2014-10-04 17:20:15 +00:00
cmd *exec.Cmd
result <-chan error
2017-04-10 20:40:24 +00:00
backend.Layout
2017-04-10 20:41:06 +00:00
Config
2014-10-04 17:20:15 +00:00
}
2016-08-31 20:51:35 +00:00
var _ restic.Backend = &SFTP{}
2017-04-10 20:40:24 +00:00
const defaultLayout = "default"
2015-04-24 23:39:32 +00:00
func startClient(program string, args ...string) (*SFTP, error) {
2017-04-03 19:05:42 +00:00
debug.Log("start client %v %v", program, args)
2014-10-04 17:20:15 +00:00
// 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...)
// prefix the errors with the program name
stderr, err := cmd.StderrPipe()
if err != nil {
2016-08-29 19:54:50 +00:00
return nil, errors.Wrap(err, "cmd.StderrPipe")
}
go func() {
sc := bufio.NewScanner(stderr)
for sc.Scan() {
fmt.Fprintf(os.Stderr, "subprocess %v: %v\n", program, sc.Text())
}
}()
2014-10-04 17:20:15 +00:00
// ignore signals sent to the parent (e.g. SIGINT)
cmd.SysProcAttr = ignoreSigIntProcAttr()
2014-10-04 17:20:15 +00:00
// get stdin and stdout
wr, err := cmd.StdinPipe()
if err != nil {
2016-08-29 19:54:50 +00:00
return nil, errors.Wrap(err, "cmd.StdinPipe")
2014-10-04 17:20:15 +00:00
}
rd, err := cmd.StdoutPipe()
if err != nil {
2016-08-29 19:54:50 +00:00
return nil, errors.Wrap(err, "cmd.StdoutPipe")
2014-10-04 17:20:15 +00:00
}
// start the process
if err := cmd.Start(); err != nil {
2016-08-29 19:54:50 +00:00
return nil, errors.Wrap(err, "cmd.Start")
2014-10-04 17:20:15 +00:00
}
// wait in a different goroutine
ch := make(chan error, 1)
go func() {
err := cmd.Wait()
2016-09-27 20:35:08 +00:00
debug.Log("ssh command exited, err %v", err)
2016-08-29 19:54:50 +00:00
ch <- errors.Wrap(err, "cmd.Wait")
}()
2014-10-04 17:20:15 +00:00
// open the SFTP session
client, err := sftp.NewClientPipe(rd, wr)
if err != nil {
return nil, errors.Errorf("unable to start the sftp session, error: %v", err)
2014-10-04 17:20:15 +00:00
}
return &SFTP{c: client, cmd: cmd, result: ch}, nil
2014-10-04 17:20:15 +00:00
}
func paths(dir string) []string {
return []string{
dir,
Join(dir, backend.Paths.Data),
Join(dir, backend.Paths.Snapshots),
Join(dir, backend.Paths.Index),
Join(dir, backend.Paths.Locks),
Join(dir, backend.Paths.Keys),
Join(dir, backend.Paths.Temp),
}
}
// clientError returns an error if the client has exited. Otherwise, nil is
// returned immediately.
func (r *SFTP) clientError() error {
select {
case err := <-r.result:
2016-09-27 20:35:08 +00:00
debug.Log("client has exited with err %v", err)
return err
default:
}
return nil
}
2017-04-10 20:41:20 +00:00
// Open opens an sftp backend as described by the config by running
// "ssh" with the appropriate arguments (or cfg.Command, if set).
func Open(cfg Config) (*SFTP, error) {
debug.Log("open backend with config %#v", cfg)
cmd, args, err := buildSSHCommand(cfg)
if err != nil {
return nil, err
}
sftp, err := startClient(cmd, args...)
2014-10-04 17:20:15 +00:00
if err != nil {
2016-09-27 20:35:08 +00:00
debug.Log("unable to start program: %v", err)
2014-10-04 17:20:15 +00:00
return nil, err
}
sftp.Layout, err = backend.ParseLayout(sftp, cfg.Layout, defaultLayout, cfg.Path)
2017-04-10 20:40:24 +00:00
if err != nil {
return nil, err
}
2014-10-04 17:20:15 +00:00
// test if all necessary dirs and files are there
2017-04-10 20:41:20 +00:00
for _, d := range paths(cfg.Path) {
2014-10-04 17:20:15 +00:00
if _, err := sftp.c.Lstat(d); err != nil {
return nil, errors.Errorf("%s does not exist", d)
2014-10-04 17:20:15 +00:00
}
}
debug.Log("layout: %v\n", sftp.Layout)
2017-04-10 20:41:20 +00:00
sftp.Config = cfg
sftp.p = cfg.Path
2014-10-04 17:20:15 +00:00
return sftp, nil
}
2017-04-10 20:41:20 +00:00
// Join combines path components with slashes (according to the sftp spec).
func (r *SFTP) Join(p ...string) string {
return path.Join(p...)
}
// ReadDir returns the entries for a directory.
func (r *SFTP) ReadDir(dir string) ([]os.FileInfo, error) {
return r.c.ReadDir(dir)
}
// IsNotExist returns true if the error is caused by a not existing file.
func (r *SFTP) IsNotExist(err error) bool {
statusError, ok := err.(*sftp.StatusError)
if !ok {
return false
}
return statusError.Error() == `sftp: "No such file" (SSH_FX_NO_SUCH_FILE)`
}
2017-04-10 20:41:20 +00:00
func buildSSHCommand(cfg Config) (cmd string, args []string, err error) {
if cfg.Command != "" {
return SplitShellArgs(cfg.Command)
}
cmd = "ssh"
hostport := strings.Split(cfg.Host, ":")
2017-04-10 20:41:20 +00:00
args = []string{hostport[0]}
if len(hostport) > 1 {
args = append(args, "-p", hostport[1])
}
2015-12-28 17:22:19 +00:00
if cfg.User != "" {
args = append(args, "-l")
args = append(args, cfg.User)
}
args = append(args, "-s")
args = append(args, "sftp")
2017-04-10 20:41:20 +00:00
return cmd, args, nil
2015-12-28 17:22:19 +00:00
}
2017-04-10 20:41:20 +00:00
// Create creates an sftp backend as described by the config by running
// "ssh" with the appropriate arguments (or cfg.Command, if set).
func Create(cfg Config) (*SFTP, error) {
2017-04-10 20:41:20 +00:00
cmd, args, err := buildSSHCommand(cfg)
if err != nil {
return nil, err
2017-04-03 19:05:42 +00:00
}
2017-04-10 20:41:20 +00:00
sftp, err := startClient(cmd, args...)
2017-04-03 19:05:42 +00:00
if err != nil {
2017-04-10 20:41:20 +00:00
debug.Log("unable to start program: %v", err)
2017-04-03 19:05:42 +00:00
return nil, err
}
sftp.Layout, err = backend.ParseLayout(sftp, cfg.Layout, defaultLayout, cfg.Path)
2014-10-04 17:20:15 +00:00
if err != nil {
return nil, err
}
2015-05-04 18:39:45 +00:00
// test if config file already exists
2017-04-10 20:41:20 +00:00
_, err = sftp.c.Lstat(Join(cfg.Path, backend.Paths.Config))
2014-10-04 17:20:15 +00:00
if err == nil {
return nil, errors.New("config file already exists")
}
// create paths for data, refs and temp blobs
2017-04-10 20:41:20 +00:00
for _, d := range paths(cfg.Path) {
2015-03-28 10:50:23 +00:00
err = sftp.mkdirAll(d, backend.Modes.Dir)
2017-04-03 19:05:42 +00:00
debug.Log("mkdirAll %v -> %v", d, err)
2014-10-04 17:20:15 +00:00
if err != nil {
return nil, err
2014-10-04 17:20:15 +00:00
}
}
err = sftp.Close()
2014-10-04 17:20:15 +00:00
if err != nil {
2016-08-29 19:54:50 +00:00
return nil, errors.Wrap(err, "Close")
2014-10-04 17:20:15 +00:00
}
2014-10-07 21:19:26 +00:00
// open backend
2017-04-10 20:41:20 +00:00
return Open(cfg)
2015-12-28 17:22:19 +00:00
}
2014-10-04 17:20:15 +00:00
// Location returns this backend's location (the directory name).
func (r *SFTP) Location() string {
return r.p
}
func (r *SFTP) mkdirAll(dir string, mode os.FileMode) error {
// check if directory already exists
fi, err := r.c.Lstat(dir)
if err == nil {
if fi.IsDir() {
return nil
}
return errors.Errorf("mkdirAll(%s): entry exists but is not a directory", dir)
}
// create parent directories
errMkdirAll := r.mkdirAll(path.Dir(dir), backend.Modes.Dir)
// create directory
errMkdir := r.c.Mkdir(dir)
// test if directory was created successfully
fi, err = r.c.Lstat(dir)
if err != nil {
// return previous errors
return errors.Errorf("mkdirAll(%s): unable to create directories: %v, %v", dir, errMkdirAll, errMkdir)
}
if !fi.IsDir() {
return errors.Errorf("mkdirAll(%s): entry exists but is not a directory", dir)
}
// set mode
return r.c.Chmod(dir, mode)
}
2015-03-28 10:50:23 +00:00
// Rename temp file to final name according to type and name.
func (r *SFTP) renameFile(oldname string, h restic.Handle) error {
filename := r.filename(h)
// create directories if necessary
if h.Type == restic.DataFile {
err := r.mkdirAll(path.Dir(filename), backend.Modes.Dir)
if err != nil {
return err
}
}
2015-03-28 10:50:23 +00:00
// test if new file exists
if _, err := r.c.Lstat(filename); err == nil {
return errors.Errorf("Close(): file %v already exists", filename)
2015-03-28 10:50:23 +00:00
}
err := r.c.Rename(oldname, filename)
if err != nil {
2016-08-29 19:54:50 +00:00
return errors.Wrap(err, "Rename")
}
// set mode to read-only
fi, err := r.c.Lstat(filename)
if err != nil {
2016-08-29 19:54:50 +00:00
return errors.Wrap(err, "Lstat")
}
2016-08-29 19:54:50 +00:00
err = r.c.Chmod(filename, fi.Mode()&os.FileMode(^uint32(0222)))
return errors.Wrap(err, "Chmod")
2014-10-04 17:20:15 +00:00
}
// Join joins the given paths and cleans them afterwards. This always uses
// forward slashes, which is required by sftp.
func Join(parts ...string) string {
return path.Clean(path.Join(parts...))
}
2016-08-31 20:39:36 +00:00
// Construct path for given restic.Type and name.
func (r *SFTP) filename(h restic.Handle) string {
if h.Type == restic.ConfigFile {
2015-11-03 17:47:01 +00:00
return Join(r.p, "config")
}
return Join(r.dirname(h), h.Name)
2014-10-04 17:20:15 +00:00
}
2015-03-28 10:50:23 +00:00
// Construct directory for given backend.Type.
func (r *SFTP) dirname(h restic.Handle) string {
2015-03-28 10:50:23 +00:00
var n string
switch h.Type {
2016-08-31 20:39:36 +00:00
case restic.DataFile:
2015-03-28 10:50:23 +00:00
n = backend.Paths.Data
if len(h.Name) > 2 {
n = Join(n, h.Name[:2])
}
2016-08-31 20:39:36 +00:00
case restic.SnapshotFile:
2015-03-28 10:50:23 +00:00
n = backend.Paths.Snapshots
2016-08-31 20:39:36 +00:00
case restic.IndexFile:
2015-04-26 13:48:35 +00:00
n = backend.Paths.Index
2016-08-31 20:39:36 +00:00
case restic.LockFile:
2015-03-28 10:50:23 +00:00
n = backend.Paths.Locks
2016-08-31 20:39:36 +00:00
case restic.KeyFile:
2015-03-28 10:50:23 +00:00
n = backend.Paths.Keys
2014-11-24 21:11:09 +00:00
}
return Join(r.p, n)
2014-10-04 17:20:15 +00:00
}
2016-01-24 00:15:35 +00:00
// Save stores data in the backend at the handle.
func (r *SFTP) Save(h restic.Handle, rd io.Reader) (err error) {
2017-04-10 20:40:24 +00:00
debug.Log("Save %v", h)
if err := r.clientError(); err != nil {
return err
}
2016-01-24 00:15:35 +00:00
if err := h.Valid(); err != nil {
return err
}
2017-04-10 20:40:24 +00:00
filename := r.Filename(h)
// create directories if necessary
if h.Type == restic.DataFile {
err := r.mkdirAll(path.Dir(filename), backend.Modes.Dir)
if err != nil {
return err
}
}
// create new file
f, err := r.c.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY)
if err != nil {
return errors.Wrap(err, "OpenFile")
2017-04-10 20:40:24 +00:00
}
// save data
_, err = io.Copy(f, rd)
2017-04-10 20:40:24 +00:00
if err != nil {
f.Close()
return errors.Wrap(err, "Write")
}
err = f.Close()
if err != nil {
return errors.Wrap(err, "Close")
2017-04-10 20:40:24 +00:00
}
// set mode to read-only
fi, err := r.c.Lstat(filename)
if err != nil {
return errors.Wrap(err, "Lstat")
}
err = r.c.Chmod(filename, fi.Mode()&os.FileMode(^uint32(0222)))
return errors.Wrap(err, "Chmod")
2016-01-24 00:15:35 +00:00
}
2017-01-23 17:11:10 +00:00
// Load returns a reader that yields the contents of the file at h at the
2017-01-22 21:01:12 +00:00
// given offset. If length is nonzero, only a portion of the file is
// returned. rd must be closed after use.
2017-01-23 17:11:10 +00:00
func (r *SFTP) Load(h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
debug.Log("Load %v, length %v, offset %v", h, length, offset)
2017-01-22 21:01:12 +00:00
if err := h.Valid(); err != nil {
return nil, err
}
if offset < 0 {
return nil, errors.New("offset is negative")
}
f, err := r.c.Open(r.filename(h))
2017-01-22 21:01:12 +00:00
if err != nil {
return nil, err
}
if offset > 0 {
_, err = f.Seek(offset, 0)
if err != nil {
_ = f.Close()
2017-01-22 21:01:12 +00:00
return nil, err
}
}
if length > 0 {
return backend.LimitReadCloser(f, int64(length)), nil
}
return f, nil
}
2016-01-23 22:27:58 +00:00
// Stat returns information about a blob.
2016-08-31 20:51:35 +00:00
func (r *SFTP) Stat(h restic.Handle) (restic.FileInfo, error) {
debug.Log("Stat(%v)", h)
if err := r.clientError(); err != nil {
2016-08-31 20:51:35 +00:00
return restic.FileInfo{}, err
}
2016-01-23 22:27:58 +00:00
if err := h.Valid(); err != nil {
2016-08-31 20:51:35 +00:00
return restic.FileInfo{}, err
2016-01-23 22:27:58 +00:00
}
fi, err := r.c.Lstat(r.filename(h))
2016-01-23 22:27:58 +00:00
if err != nil {
2016-08-31 20:51:35 +00:00
return restic.FileInfo{}, errors.Wrap(err, "Lstat")
2016-01-23 22:27:58 +00:00
}
2016-08-31 20:51:35 +00:00
return restic.FileInfo{Size: fi.Size()}, nil
2016-01-23 22:27:58 +00:00
}
2015-03-28 10:50:23 +00:00
// Test returns true if a blob of the given type and name exists in the backend.
func (r *SFTP) Test(h restic.Handle) (bool, error) {
debug.Log("Test(%v)", h)
if err := r.clientError(); err != nil {
return false, err
}
_, err := r.c.Lstat(r.filename(h))
if os.IsNotExist(errors.Cause(err)) {
return false, nil
}
if err != nil {
2016-08-29 19:54:50 +00:00
return false, errors.Wrap(err, "Lstat")
2014-10-04 17:20:15 +00:00
}
return true, nil
2014-10-04 17:20:15 +00:00
}
2015-03-28 10:50:23 +00:00
// Remove removes the content stored at name.
func (r *SFTP) Remove(h restic.Handle) error {
debug.Log("Remove(%v)", h)
if err := r.clientError(); err != nil {
return err
}
return r.c.Remove(r.filename(h))
2014-10-04 17:20:15 +00:00
}
2015-03-28 10:50:23 +00:00
// List returns a channel that yields all names of blobs of type t. A
2015-06-28 07:44:06 +00:00
// goroutine is started for this. If the channel done is closed, sending
2015-03-28 10:50:23 +00:00
// stops.
2016-08-31 20:39:36 +00:00
func (r *SFTP) List(t restic.FileType, done <-chan struct{}) <-chan string {
2016-09-27 20:35:08 +00:00
debug.Log("list all %v", t)
2015-03-28 10:50:23 +00:00
ch := make(chan string)
2015-03-28 10:50:23 +00:00
go func() {
defer close(ch)
2016-08-31 20:39:36 +00:00
if t == restic.DataFile {
2015-03-28 10:50:23 +00:00
// read first level
basedir := r.dirname(restic.Handle{Type: t})
2015-03-28 10:50:23 +00:00
list1, err := r.c.ReadDir(basedir)
if err != nil {
return
}
dirs := make([]string, 0, len(list1))
for _, d := range list1 {
dirs = append(dirs, d.Name())
}
2015-03-28 10:50:23 +00:00
// read files
for _, dir := range dirs {
entries, err := r.c.ReadDir(Join(basedir, dir))
2015-03-28 10:50:23 +00:00
if err != nil {
continue
}
items := make([]string, 0, len(entries))
for _, entry := range entries {
items = append(items, entry.Name())
}
for _, file := range items {
select {
case ch <- file:
case <-done:
return
}
}
}
} else {
entries, err := r.c.ReadDir(r.dirname(restic.Handle{Type: t}))
if err != nil {
2015-03-28 10:50:23 +00:00
return
}
2015-03-28 10:50:23 +00:00
items := make([]string, 0, len(entries))
for _, entry := range entries {
2015-03-28 10:50:23 +00:00
items = append(items, entry.Name())
}
2014-10-04 17:20:15 +00:00
2015-03-28 10:50:23 +00:00
for _, file := range items {
select {
case ch <- file:
case <-done:
return
}
}
2014-10-04 17:20:15 +00:00
}
2015-03-28 10:50:23 +00:00
}()
return ch
2014-10-04 17:20:15 +00:00
}
var closeTimeout = 2 * time.Second
2014-10-04 17:20:15 +00:00
// Close closes the sftp connection and terminates the underlying command.
2016-01-24 19:23:50 +00:00
func (r *SFTP) Close() error {
2016-09-27 20:35:08 +00:00
debug.Log("")
2016-01-24 19:23:50 +00:00
if r == nil {
return nil
}
err := r.c.Close()
2016-09-27 20:35:08 +00:00
debug.Log("Close returned error %v", err)
// wait for closeTimeout before killing the process
select {
case err := <-r.result:
return err
case <-time.After(closeTimeout):
}
2016-01-24 19:23:50 +00:00
if err := r.cmd.Process.Kill(); err != nil {
return err
}
// get the error, but ignore it
<-r.result
return nil
2014-10-04 17:20:15 +00:00
}