2015-03-28 10:50:23 +00:00
|
|
|
package sftp
|
2014-10-04 17:20:15 +00:00
|
|
|
|
|
|
|
import (
|
2016-08-28 17:53:18 +00:00
|
|
|
"bufio"
|
2017-06-03 15:39:57 +00:00
|
|
|
"context"
|
2021-09-22 20:28:57 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"encoding/hex"
|
2014-10-04 17:20:15 +00:00
|
|
|
"fmt"
|
2020-12-19 11:39:48 +00:00
|
|
|
"hash"
|
2014-10-04 17:20:15 +00:00
|
|
|
"io"
|
|
|
|
"os"
|
|
|
|
"os/exec"
|
2016-08-11 17:10:51 +00:00
|
|
|
"path"
|
2016-08-28 17:17:17 +00:00
|
|
|
"time"
|
2014-10-04 17:20:15 +00:00
|
|
|
|
2017-07-23 12:21:03 +00:00
|
|
|
"github.com/restic/restic/internal/errors"
|
2017-07-24 15:42:25 +00:00
|
|
|
"github.com/restic/restic/internal/restic"
|
2017-07-23 12:21:03 +00:00
|
|
|
|
|
|
|
"github.com/restic/restic/internal/backend"
|
|
|
|
"github.com/restic/restic/internal/debug"
|
2016-08-11 17:10:51 +00:00
|
|
|
|
2020-12-16 15:00:21 +00:00
|
|
|
"github.com/cenkalti/backoff/v4"
|
2016-08-11 17:10:51 +00:00
|
|
|
"github.com/pkg/sftp"
|
2014-10-04 17:20:15 +00:00
|
|
|
)
|
|
|
|
|
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 {
|
2015-05-03 14:43:27 +00:00
|
|
|
c *sftp.Client
|
|
|
|
p string
|
2014-10-04 17:20:15 +00:00
|
|
|
|
2016-08-28 17:17:17 +00:00
|
|
|
cmd *exec.Cmd
|
|
|
|
result <-chan error
|
2017-04-10 20:40:24 +00:00
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
sem *backend.Semaphore
|
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"
|
|
|
|
|
2018-01-17 22:14:47 +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...)
|
|
|
|
|
2016-08-28 17:53:18 +00:00
|
|
|
// 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")
|
2016-08-28 17:53:18 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
// 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
|
|
|
}
|
|
|
|
|
2018-03-13 20:09:16 +00:00
|
|
|
bg, err := backend.StartForeground(cmd)
|
2018-01-17 22:02:47 +00:00
|
|
|
if 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
|
|
|
}
|
|
|
|
|
2016-08-28 17:17:17 +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)
|
sftp: persist "ssh command exited" error
If our ssh process has died, not only the next, but all subsequent
calls to clientError() should indicate the error.
restic output when the ssh process is killed with "kill -9":
Save(<data/afb68adbf9>) returned error, retrying after 253.661803ms: Write: failed to send packet header: write |1: file already closed
Save(<data/afb68adbf9>) returned error, retrying after 580.752212ms: ssh command exited: signal: killed
Save(<data/afb68adbf9>) returned error, retrying after 790.150468ms: ssh command exited: signal: killed
Save(<data/afb68adbf9>) returned error, retrying after 1.769595051s: ssh command exited: signal: killed
[...]
error in cleanup handler: ssh command exited: signal: killed
Before this patch:
Save(<data/de698d934f>) returned error, retrying after 252.84163ms: Write: failed to send packet header: write |1: file already closed
Save(<data/de698d934f>) returned error, retrying after 660.236963ms: OpenFile: failed to send packet header: write |1: file already closed
Save(<data/de698d934f>) returned error, retrying after 568.049909ms: OpenFile: failed to send packet header: write |1: file already closed
Save(<data/de698d934f>) returned error, retrying after 2.428813824s: OpenFile: failed to send packet header: write |1: file already closed
[...]
error in cleanup handler: failed to send packet header: write |1: file already closed
2018-05-30 17:28:14 +00:00
|
|
|
for {
|
|
|
|
ch <- errors.Wrap(err, "ssh command exited")
|
|
|
|
}
|
2016-08-28 17:17:17 +00:00
|
|
|
}()
|
|
|
|
|
2014-10-04 17:20:15 +00:00
|
|
|
// open the SFTP session
|
|
|
|
client, err := sftp.NewClientPipe(rd, wr)
|
|
|
|
if err != nil {
|
2016-08-21 15:48:36 +00:00
|
|
|
return nil, errors.Errorf("unable to start the sftp session, error: %v", err)
|
2014-10-04 17:20:15 +00:00
|
|
|
}
|
|
|
|
|
2018-01-17 22:02:47 +00:00
|
|
|
err = bg()
|
|
|
|
if err != nil {
|
|
|
|
return nil, errors.Wrap(err, "bg")
|
|
|
|
}
|
|
|
|
|
2016-08-28 17:17:17 +00:00
|
|
|
return &SFTP{c: client, cmd: cmd, result: ch}, nil
|
2014-10-04 17:20:15 +00:00
|
|
|
}
|
|
|
|
|
2016-08-28 17:17:17 +00:00
|
|
|
// 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)
|
2020-12-16 15:00:21 +00:00
|
|
|
return backoff.Permanent(err)
|
2016-08-28 17:17:17 +00:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-04-10 20:41:20 +00:00
|
|
|
// Open opens an sftp backend as described by the config by running
|
2017-09-23 09:21:27 +00:00
|
|
|
// "ssh" with the appropriate arguments (or cfg.Command, if set). The function
|
|
|
|
// preExec is run just before, postExec just after starting a program.
|
2020-09-19 20:01:32 +00:00
|
|
|
func Open(ctx context.Context, cfg Config) (*SFTP, error) {
|
2017-04-10 20:41:20 +00:00
|
|
|
debug.Log("open backend with config %#v", cfg)
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
sem, err := backend.NewSemaphore(cfg.Connections)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-04-10 20:41:20 +00:00
|
|
|
cmd, args, err := buildSSHCommand(cfg)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2018-01-17 22:14:47 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-09-19 20:01:32 +00:00
|
|
|
sftp.Layout, err = backend.ParseLayout(ctx, sftp, cfg.Layout, defaultLayout, cfg.Path)
|
2017-04-10 20:40:24 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2017-04-10 20:17:50 +00:00
|
|
|
debug.Log("layout: %v\n", sftp.Layout)
|
|
|
|
|
2017-04-10 20:41:20 +00:00
|
|
|
sftp.Config = cfg
|
|
|
|
sftp.p = cfg.Path
|
2021-08-07 17:56:59 +00:00
|
|
|
sftp.sem = sem
|
2014-10-04 17:20:15 +00:00
|
|
|
return sftp, nil
|
|
|
|
}
|
|
|
|
|
2017-07-16 13:10:06 +00:00
|
|
|
func (r *SFTP) mkdirAllDataSubdirs() error {
|
|
|
|
for _, d := range r.Paths() {
|
2020-01-01 16:26:38 +00:00
|
|
|
err := r.c.MkdirAll(d)
|
2017-07-16 13:10:06 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return 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.
|
2020-09-19 20:01:32 +00:00
|
|
|
func (r *SFTP) ReadDir(ctx context.Context, dir string) ([]os.FileInfo, error) {
|
2017-10-28 18:16:27 +00:00
|
|
|
fi, err := r.c.ReadDir(dir)
|
|
|
|
|
|
|
|
// sftp client does not specify dir name on error, so add it here
|
|
|
|
err = errors.Wrapf(err, "(%v)", dir)
|
|
|
|
|
|
|
|
return fi, err
|
2017-04-10 20:41:20 +00:00
|
|
|
}
|
|
|
|
|
2017-04-10 20:17:50 +00:00
|
|
|
// IsNotExist returns true if the error is caused by a not existing file.
|
|
|
|
func (r *SFTP) IsNotExist(err error) bool {
|
2018-01-05 16:51:09 +00:00
|
|
|
err = errors.Cause(err)
|
2021-03-10 19:38:22 +00:00
|
|
|
return errors.Is(err, os.ErrNotExist)
|
2017-04-10 20:17:50 +00:00
|
|
|
}
|
|
|
|
|
2017-04-10 20:41:20 +00:00
|
|
|
func buildSSHCommand(cfg Config) (cmd string, args []string, err error) {
|
|
|
|
if cfg.Command != "" {
|
2018-03-13 19:50:37 +00:00
|
|
|
args, err := backend.SplitShellStrings(cfg.Command)
|
|
|
|
if err != nil {
|
|
|
|
return "", nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return args[0], args[1:], nil
|
2017-04-10 20:41:20 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
cmd = "ssh"
|
|
|
|
|
2020-03-08 15:45:33 +00:00
|
|
|
host, port := cfg.Host, cfg.Port
|
|
|
|
|
|
|
|
args = []string{host}
|
|
|
|
if port != "" {
|
|
|
|
args = append(args, "-p", port)
|
2016-02-15 18:17:41 +00:00
|
|
|
}
|
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-09-23 09:21:27 +00:00
|
|
|
// Create creates an sftp backend as described by the config by running "ssh"
|
|
|
|
// with the appropriate arguments (or cfg.Command, if set). The function
|
|
|
|
// preExec is run just before, postExec just after starting a program.
|
2020-09-19 20:01:32 +00:00
|
|
|
func Create(ctx context.Context, 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
|
|
|
}
|
|
|
|
|
2018-01-17 22:14:47 +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
|
|
|
|
}
|
|
|
|
|
2020-09-19 20:01:32 +00:00
|
|
|
sftp.Layout, err = backend.ParseLayout(ctx, 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 {
|
2015-05-03 14:43:27 +00:00
|
|
|
return nil, errors.New("config file already exists")
|
2015-03-14 10:56:45 +00:00
|
|
|
}
|
|
|
|
|
2017-04-19 16:56:01 +00:00
|
|
|
// create paths for data and refs
|
2017-07-16 13:10:06 +00:00
|
|
|
if err = sftp.mkdirAllDataSubdirs(); err != nil {
|
|
|
|
return nil, err
|
2014-10-04 17:20:15 +00:00
|
|
|
}
|
|
|
|
|
2016-08-28 17:17:17 +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
|
2020-09-19 20:01:32 +00:00
|
|
|
return Open(ctx, cfg)
|
2015-12-28 17:22:19 +00:00
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
func (r *SFTP) Connections() uint {
|
|
|
|
return r.Config.Connections
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-12-19 11:39:48 +00:00
|
|
|
// Hasher may return a hash function for calculating a content hash for the backend
|
|
|
|
func (r *SFTP) Hasher() hash.Hash {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-08-11 17:10:51 +00:00
|
|
|
// Join joins the given paths and cleans them afterwards. This always uses
|
|
|
|
// forward slashes, which is required by sftp.
|
2015-11-02 13:53:34 +00:00
|
|
|
func Join(parts ...string) string {
|
2016-08-11 17:10:51 +00:00
|
|
|
return path.Clean(path.Join(parts...))
|
2015-11-02 13:53:34 +00:00
|
|
|
}
|
|
|
|
|
2021-09-22 20:28:57 +00:00
|
|
|
// tempSuffix generates a random string suffix that should be sufficiently long
|
|
|
|
// to avoid accidential conflicts
|
|
|
|
func tempSuffix() string {
|
|
|
|
var nonce [16]byte
|
|
|
|
_, err := rand.Read(nonce[:])
|
|
|
|
if err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
return hex.EncodeToString(nonce[:])
|
|
|
|
}
|
|
|
|
|
2016-01-24 00:15:35 +00:00
|
|
|
// Save stores data in the backend at the handle.
|
2018-03-03 13:20:54 +00:00
|
|
|
func (r *SFTP) Save(ctx context.Context, h restic.Handle, rd restic.RewindReader) error {
|
2017-04-10 20:40:24 +00:00
|
|
|
debug.Log("Save %v", h)
|
2016-08-28 17:17:17 +00:00
|
|
|
if err := r.clientError(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-01-24 00:15:35 +00:00
|
|
|
if err := h.Valid(); err != nil {
|
2020-12-17 11:47:53 +00:00
|
|
|
return backoff.Permanent(err)
|
2016-01-24 00:15:35 +00:00
|
|
|
}
|
|
|
|
|
2017-04-10 20:40:24 +00:00
|
|
|
filename := r.Filename(h)
|
2021-09-22 20:28:57 +00:00
|
|
|
tmpFilename := filename + "-restic-temp-" + tempSuffix()
|
2021-03-16 20:35:10 +00:00
|
|
|
dirname := r.Dirname(h)
|
2017-04-10 20:40:24 +00:00
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
r.sem.GetToken()
|
|
|
|
defer r.sem.ReleaseToken()
|
|
|
|
|
2017-05-03 19:18:49 +00:00
|
|
|
// create new file
|
2021-09-22 20:28:57 +00:00
|
|
|
f, err := r.c.OpenFile(tmpFilename, os.O_CREATE|os.O_EXCL|os.O_WRONLY)
|
2017-05-03 19:18:49 +00:00
|
|
|
|
2018-01-05 16:51:09 +00:00
|
|
|
if r.IsNotExist(err) {
|
|
|
|
// error is caused by a missing directory, try to create it
|
2020-01-01 16:26:38 +00:00
|
|
|
mkdirErr := r.c.MkdirAll(r.Dirname(h))
|
2018-01-05 16:51:09 +00:00
|
|
|
if mkdirErr != nil {
|
|
|
|
debug.Log("error creating dir %v: %v", r.Dirname(h), mkdirErr)
|
|
|
|
} else {
|
|
|
|
// try again
|
2021-09-22 20:28:57 +00:00
|
|
|
f, err = r.c.OpenFile(tmpFilename, os.O_CREATE|os.O_EXCL|os.O_WRONLY)
|
2018-01-05 16:51:09 +00:00
|
|
|
}
|
2017-04-10 20:40:24 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 20:35:10 +00:00
|
|
|
// pkg/sftp doesn't allow creating with a mode.
|
|
|
|
// Chmod while the file is still empty.
|
|
|
|
if err == nil {
|
|
|
|
err = f.Chmod(backend.Modes.File)
|
|
|
|
}
|
2017-04-10 20:17:50 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "OpenFile")
|
2017-04-10 20:40:24 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 20:35:10 +00:00
|
|
|
defer func() {
|
|
|
|
if err == nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Try not to leave a partial file behind.
|
|
|
|
rmErr := r.c.Remove(f.Name())
|
|
|
|
if rmErr != nil {
|
|
|
|
debug.Log("sftp: failed to remove broken file %v: %v",
|
2021-09-22 20:28:57 +00:00
|
|
|
f.Name(), rmErr)
|
2021-03-16 20:35:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
err = r.checkNoSpace(dirname, rd.Length(), err)
|
|
|
|
}()
|
|
|
|
|
2021-01-01 11:19:48 +00:00
|
|
|
// save data, make sure to use the optimized sftp upload method
|
2020-12-18 22:41:29 +00:00
|
|
|
wbytes, err := f.ReadFrom(rd)
|
2017-04-10 20:40:24 +00:00
|
|
|
if err != nil {
|
2017-05-11 19:53:57 +00:00
|
|
|
_ = f.Close()
|
2017-04-10 20:17:50 +00:00
|
|
|
return errors.Wrap(err, "Write")
|
|
|
|
}
|
|
|
|
|
2020-12-18 22:41:29 +00:00
|
|
|
// sanity check
|
|
|
|
if wbytes != rd.Length() {
|
|
|
|
_ = f.Close()
|
|
|
|
return errors.Errorf("wrote %d bytes instead of the expected %d bytes", wbytes, rd.Length())
|
|
|
|
}
|
|
|
|
|
2017-04-10 20:17:50 +00:00
|
|
|
err = f.Close()
|
2021-09-22 20:28:57 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "Close")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = r.c.Rename(tmpFilename, filename)
|
|
|
|
return errors.Wrap(err, "Rename")
|
2021-03-16 20:35:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// checkNoSpace checks if err was likely caused by lack of available space
|
|
|
|
// on the remote, and if so, makes it permanent.
|
|
|
|
func (r *SFTP) checkNoSpace(dir string, size int64, origErr error) error {
|
|
|
|
// The SFTP protocol has a message for ENOSPC,
|
|
|
|
// but pkg/sftp doesn't export it and OpenSSH's sftp-server
|
|
|
|
// sends FX_FAILURE instead.
|
|
|
|
|
|
|
|
e, ok := origErr.(*sftp.StatusError)
|
|
|
|
_, hasExt := r.c.HasExtension("statvfs@openssh.com")
|
|
|
|
if !ok || e.FxCode() != sftp.ErrSSHFxFailure || !hasExt {
|
|
|
|
return origErr
|
2017-04-10 20:40:24 +00:00
|
|
|
}
|
|
|
|
|
2021-03-16 20:35:10 +00:00
|
|
|
fsinfo, err := r.c.StatVFS(dir)
|
|
|
|
if err != nil {
|
|
|
|
debug.Log("sftp: StatVFS returned %v", err)
|
|
|
|
return origErr
|
|
|
|
}
|
|
|
|
if fsinfo.Favail == 0 || fsinfo.FreeSpace() < uint64(size) {
|
|
|
|
err := errors.New("sftp: no space left on device")
|
|
|
|
return backoff.Permanent(err)
|
|
|
|
}
|
|
|
|
return origErr
|
2016-01-24 00:15:35 +00:00
|
|
|
}
|
|
|
|
|
2018-01-17 04:59:16 +00:00
|
|
|
// Load runs fn with a reader that yields the contents of the file at h at the
|
|
|
|
// given offset.
|
|
|
|
func (r *SFTP) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
|
|
|
|
return backend.DefaultLoad(ctx, h, length, offset, r.openReader, fn)
|
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
// wrapReader wraps an io.ReadCloser to run an additional function on Close.
|
|
|
|
type wrapReader struct {
|
|
|
|
io.ReadCloser
|
|
|
|
io.WriterTo
|
|
|
|
f func()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (wr *wrapReader) Close() error {
|
|
|
|
err := wr.ReadCloser.Close()
|
|
|
|
wr.f()
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2018-01-17 04:59:16 +00:00
|
|
|
func (r *SFTP) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
|
2017-01-23 17:11:10 +00:00
|
|
|
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 {
|
2020-12-17 11:47:53 +00:00
|
|
|
return nil, backoff.Permanent(err)
|
2017-01-22 21:01:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if offset < 0 {
|
|
|
|
return nil, errors.New("offset is negative")
|
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
r.sem.GetToken()
|
2017-04-10 20:26:17 +00:00
|
|
|
f, err := r.c.Open(r.Filename(h))
|
2017-01-22 21:01:12 +00:00
|
|
|
if err != nil {
|
2021-08-07 17:56:59 +00:00
|
|
|
r.sem.ReleaseToken()
|
2017-01-22 21:01:12 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if offset > 0 {
|
|
|
|
_, err = f.Seek(offset, 0)
|
|
|
|
if err != nil {
|
2021-08-07 17:56:59 +00:00
|
|
|
r.sem.ReleaseToken()
|
2017-01-23 15:20:07 +00:00
|
|
|
_ = f.Close()
|
2017-01-22 21:01:12 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
// use custom close wrapper to also provide WriteTo() on the wrapper
|
|
|
|
rd := &wrapReader{
|
|
|
|
ReadCloser: f,
|
|
|
|
WriterTo: f,
|
|
|
|
f: func() {
|
|
|
|
r.sem.ReleaseToken()
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2017-01-22 21:01:12 +00:00
|
|
|
if length > 0 {
|
2021-01-01 11:19:48 +00:00
|
|
|
// unlimited reads usually use io.Copy which needs WriteTo support at the underlying reader
|
|
|
|
// limited reads are usually combined with io.ReadFull which reads all required bytes into a buffer in one go
|
2021-08-07 17:56:59 +00:00
|
|
|
return backend.LimitReadCloser(rd, int64(length)), nil
|
2017-01-22 21:01:12 +00:00
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
return rd, nil
|
2017-01-22 21:01:12 +00:00
|
|
|
}
|
|
|
|
|
2016-01-23 22:27:58 +00:00
|
|
|
// Stat returns information about a blob.
|
2017-06-03 15:39:57 +00:00
|
|
|
func (r *SFTP) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
|
2017-01-25 16:48:35 +00:00
|
|
|
debug.Log("Stat(%v)", h)
|
2016-08-28 17:17:17 +00:00
|
|
|
if err := r.clientError(); err != nil {
|
2016-08-31 20:51:35 +00:00
|
|
|
return restic.FileInfo{}, err
|
2016-08-28 17:17:17 +00:00
|
|
|
}
|
|
|
|
|
2016-01-23 22:27:58 +00:00
|
|
|
if err := h.Valid(); err != nil {
|
2020-12-17 11:47:53 +00:00
|
|
|
return restic.FileInfo{}, backoff.Permanent(err)
|
2016-01-23 22:27:58 +00:00
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
r.sem.GetToken()
|
|
|
|
defer r.sem.ReleaseToken()
|
|
|
|
|
2017-04-10 20:26:17 +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
|
|
|
}
|
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
return restic.FileInfo{Size: fi.Size(), Name: h.Name}, 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.
|
2017-06-03 15:39:57 +00:00
|
|
|
func (r *SFTP) Test(ctx context.Context, h restic.Handle) (bool, error) {
|
2017-01-25 16:48:35 +00:00
|
|
|
debug.Log("Test(%v)", h)
|
2016-08-28 17:17:17 +00:00
|
|
|
if err := r.clientError(); err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
r.sem.GetToken()
|
|
|
|
defer r.sem.ReleaseToken()
|
|
|
|
|
2017-04-10 20:26:17 +00:00
|
|
|
_, err := r.c.Lstat(r.Filename(h))
|
2016-08-29 17:18:57 +00:00
|
|
|
if os.IsNotExist(errors.Cause(err)) {
|
2016-02-13 18:11:41 +00:00
|
|
|
return false, nil
|
|
|
|
}
|
2014-11-30 14:50:30 +00:00
|
|
|
|
2016-02-13 18:11:41 +00:00
|
|
|
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
|
|
|
}
|
2014-11-30 14:50:30 +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.
|
2017-06-03 15:39:57 +00:00
|
|
|
func (r *SFTP) Remove(ctx context.Context, h restic.Handle) error {
|
2017-01-25 16:48:35 +00:00
|
|
|
debug.Log("Remove(%v)", h)
|
2016-08-28 17:17:17 +00:00
|
|
|
if err := r.clientError(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2021-08-07 17:56:59 +00:00
|
|
|
r.sem.GetToken()
|
|
|
|
defer r.sem.ReleaseToken()
|
|
|
|
|
2017-04-10 20:26:17 +00:00
|
|
|
return r.c.Remove(r.Filename(h))
|
2014-10-04 17:20:15 +00:00
|
|
|
}
|
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
// List runs fn for each file in the backend which has the type t. When an
|
|
|
|
// error occurs (or fn returns an error), List stops and returns it.
|
|
|
|
func (r *SFTP) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
|
2017-04-10 21:31:13 +00:00
|
|
|
debug.Log("List %v", t)
|
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
basedir, subdirs := r.Basedir(t)
|
|
|
|
walker := r.c.Walk(basedir)
|
2021-08-07 17:56:59 +00:00
|
|
|
for {
|
|
|
|
r.sem.GetToken()
|
|
|
|
ok := walker.Step()
|
|
|
|
r.sem.ReleaseToken()
|
|
|
|
if !ok {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
if walker.Err() != nil {
|
2018-04-10 19:35:30 +00:00
|
|
|
if r.IsNotExist(walker.Err()) {
|
|
|
|
debug.Log("ignoring non-existing directory")
|
|
|
|
return nil
|
|
|
|
}
|
2018-01-20 18:34:38 +00:00
|
|
|
return walker.Err()
|
|
|
|
}
|
2014-11-30 14:50:30 +00:00
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
if walker.Path() == basedir {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
if walker.Stat().IsDir() && !subdirs {
|
|
|
|
walker.SkipDir()
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
fi := walker.Stat()
|
|
|
|
if !fi.Mode().IsRegular() {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
debug.Log("send %v\n", path.Base(walker.Path()))
|
|
|
|
|
|
|
|
rfi := restic.FileInfo{
|
|
|
|
Name: path.Base(walker.Path()),
|
|
|
|
Size: fi.Size(),
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.Err() != nil {
|
|
|
|
return ctx.Err()
|
2014-10-04 17:20:15 +00:00
|
|
|
}
|
2015-03-28 10:50:23 +00:00
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
err := fn(rfi)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if ctx.Err() != nil {
|
|
|
|
return ctx.Err()
|
|
|
|
}
|
|
|
|
}
|
2014-10-04 17:20:15 +00:00
|
|
|
|
2018-01-20 18:34:38 +00:00
|
|
|
return ctx.Err()
|
2014-10-04 17:20:15 +00:00
|
|
|
}
|
|
|
|
|
2016-08-28 17:17:17 +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 {
|
2017-10-28 18:16:27 +00:00
|
|
|
debug.Log("Close")
|
2016-01-24 19:23:50 +00:00
|
|
|
if r == nil {
|
2015-05-01 15:13:03 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-01-26 21:16:17 +00:00
|
|
|
err := r.c.Close()
|
2016-09-27 20:35:08 +00:00
|
|
|
debug.Log("Close returned error %v", err)
|
2015-07-05 09:06:28 +00:00
|
|
|
|
2016-08-28 17:17:17 +00:00
|
|
|
// 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 {
|
2015-07-05 09:06:28 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2016-08-28 17:17:17 +00:00
|
|
|
// get the error, but ignore it
|
|
|
|
<-r.result
|
|
|
|
return nil
|
2014-10-04 17:20:15 +00:00
|
|
|
}
|
2017-10-14 11:38:17 +00:00
|
|
|
|
2020-09-19 20:01:32 +00:00
|
|
|
func (r *SFTP) deleteRecursive(ctx context.Context, name string) error {
|
|
|
|
entries, err := r.ReadDir(ctx, name)
|
2017-10-14 14:08:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "ReadDir")
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, fi := range entries {
|
|
|
|
itemName := r.Join(name, fi.Name())
|
|
|
|
if fi.IsDir() {
|
2020-09-19 20:01:32 +00:00
|
|
|
err := r.deleteRecursive(ctx, itemName)
|
2017-10-14 14:08:15 +00:00
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "ReadDir")
|
|
|
|
}
|
|
|
|
|
|
|
|
err = r.c.RemoveDirectory(itemName)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "RemoveDirectory")
|
|
|
|
}
|
|
|
|
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
err := r.c.Remove(itemName)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "ReadDir")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2017-10-14 11:38:17 +00:00
|
|
|
// Delete removes all data in the backend.
|
2020-09-19 20:01:32 +00:00
|
|
|
func (r *SFTP) Delete(ctx context.Context) error {
|
|
|
|
return r.deleteRecursive(ctx, r.p)
|
2017-10-14 11:38:17 +00:00
|
|
|
}
|