github.com/mckael/restic@v0.8.3/internal/backend/sftp/sftp.go (about)

     1  package sftp
     2  
     3  import (
     4  	"bufio"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"os/exec"
    10  	"path"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/restic/restic/internal/errors"
    15  	"github.com/restic/restic/internal/restic"
    16  
    17  	"github.com/restic/restic/internal/backend"
    18  	"github.com/restic/restic/internal/debug"
    19  
    20  	"github.com/pkg/sftp"
    21  )
    22  
    23  // SFTP is a backend in a directory accessed via SFTP.
    24  type SFTP struct {
    25  	c *sftp.Client
    26  	p string
    27  
    28  	cmd    *exec.Cmd
    29  	result <-chan error
    30  
    31  	backend.Layout
    32  	Config
    33  }
    34  
    35  var _ restic.Backend = &SFTP{}
    36  
    37  const defaultLayout = "default"
    38  
    39  func startClient(program string, args ...string) (*SFTP, error) {
    40  	debug.Log("start client %v %v", program, args)
    41  	// Connect to a remote host and request the sftp subsystem via the 'ssh'
    42  	// command.  This assumes that passwordless login is correctly configured.
    43  	cmd := exec.Command(program, args...)
    44  
    45  	// prefix the errors with the program name
    46  	stderr, err := cmd.StderrPipe()
    47  	if err != nil {
    48  		return nil, errors.Wrap(err, "cmd.StderrPipe")
    49  	}
    50  
    51  	go func() {
    52  		sc := bufio.NewScanner(stderr)
    53  		for sc.Scan() {
    54  			fmt.Fprintf(os.Stderr, "subprocess %v: %v\n", program, sc.Text())
    55  		}
    56  	}()
    57  
    58  	// get stdin and stdout
    59  	wr, err := cmd.StdinPipe()
    60  	if err != nil {
    61  		return nil, errors.Wrap(err, "cmd.StdinPipe")
    62  	}
    63  	rd, err := cmd.StdoutPipe()
    64  	if err != nil {
    65  		return nil, errors.Wrap(err, "cmd.StdoutPipe")
    66  	}
    67  
    68  	bg, err := startForeground(cmd)
    69  	if err != nil {
    70  		return nil, errors.Wrap(err, "cmd.Start")
    71  	}
    72  
    73  	// wait in a different goroutine
    74  	ch := make(chan error, 1)
    75  	go func() {
    76  		err := cmd.Wait()
    77  		debug.Log("ssh command exited, err %v", err)
    78  		ch <- errors.Wrap(err, "cmd.Wait")
    79  	}()
    80  
    81  	// open the SFTP session
    82  	client, err := sftp.NewClientPipe(rd, wr)
    83  	if err != nil {
    84  		return nil, errors.Errorf("unable to start the sftp session, error: %v", err)
    85  	}
    86  
    87  	err = bg()
    88  	if err != nil {
    89  		return nil, errors.Wrap(err, "bg")
    90  	}
    91  
    92  	return &SFTP{c: client, cmd: cmd, result: ch}, nil
    93  }
    94  
    95  // clientError returns an error if the client has exited. Otherwise, nil is
    96  // returned immediately.
    97  func (r *SFTP) clientError() error {
    98  	select {
    99  	case err := <-r.result:
   100  		debug.Log("client has exited with err %v", err)
   101  		return err
   102  	default:
   103  	}
   104  
   105  	return nil
   106  }
   107  
   108  // Open opens an sftp backend as described by the config by running
   109  // "ssh" with the appropriate arguments (or cfg.Command, if set). The function
   110  // preExec is run just before, postExec just after starting a program.
   111  func Open(cfg Config) (*SFTP, error) {
   112  	debug.Log("open backend with config %#v", cfg)
   113  
   114  	cmd, args, err := buildSSHCommand(cfg)
   115  	if err != nil {
   116  		return nil, err
   117  	}
   118  
   119  	sftp, err := startClient(cmd, args...)
   120  	if err != nil {
   121  		debug.Log("unable to start program: %v", err)
   122  		return nil, err
   123  	}
   124  
   125  	sftp.Layout, err = backend.ParseLayout(sftp, cfg.Layout, defaultLayout, cfg.Path)
   126  	if err != nil {
   127  		return nil, err
   128  	}
   129  
   130  	debug.Log("layout: %v\n", sftp.Layout)
   131  
   132  	sftp.Config = cfg
   133  	sftp.p = cfg.Path
   134  	return sftp, nil
   135  }
   136  
   137  func (r *SFTP) mkdirAllDataSubdirs() error {
   138  	for _, d := range r.Paths() {
   139  		err := r.mkdirAll(d, backend.Modes.Dir)
   140  		debug.Log("mkdirAll %v -> %v", d, err)
   141  		if err != nil {
   142  			return err
   143  		}
   144  	}
   145  
   146  	return nil
   147  }
   148  
   149  // Join combines path components with slashes (according to the sftp spec).
   150  func (r *SFTP) Join(p ...string) string {
   151  	return path.Join(p...)
   152  }
   153  
   154  // ReadDir returns the entries for a directory.
   155  func (r *SFTP) ReadDir(dir string) ([]os.FileInfo, error) {
   156  	fi, err := r.c.ReadDir(dir)
   157  
   158  	// sftp client does not specify dir name on error, so add it here
   159  	err = errors.Wrapf(err, "(%v)", dir)
   160  
   161  	return fi, err
   162  }
   163  
   164  // IsNotExist returns true if the error is caused by a not existing file.
   165  func (r *SFTP) IsNotExist(err error) bool {
   166  	err = errors.Cause(err)
   167  
   168  	if os.IsNotExist(err) {
   169  		return true
   170  	}
   171  
   172  	statusError, ok := err.(*sftp.StatusError)
   173  	if !ok {
   174  		return false
   175  	}
   176  
   177  	return statusError.Error() == `sftp: "No such file" (SSH_FX_NO_SUCH_FILE)`
   178  }
   179  
   180  func buildSSHCommand(cfg Config) (cmd string, args []string, err error) {
   181  	if cfg.Command != "" {
   182  		return SplitShellArgs(cfg.Command)
   183  	}
   184  
   185  	cmd = "ssh"
   186  
   187  	hostport := strings.Split(cfg.Host, ":")
   188  	args = []string{hostport[0]}
   189  	if len(hostport) > 1 {
   190  		args = append(args, "-p", hostport[1])
   191  	}
   192  	if cfg.User != "" {
   193  		args = append(args, "-l")
   194  		args = append(args, cfg.User)
   195  	}
   196  	args = append(args, "-s")
   197  	args = append(args, "sftp")
   198  	return cmd, args, nil
   199  }
   200  
   201  // Create creates an sftp backend as described by the config by running "ssh"
   202  // with the appropriate arguments (or cfg.Command, if set). The function
   203  // preExec is run just before, postExec just after starting a program.
   204  func Create(cfg Config) (*SFTP, error) {
   205  	cmd, args, err := buildSSHCommand(cfg)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	sftp, err := startClient(cmd, args...)
   211  	if err != nil {
   212  		debug.Log("unable to start program: %v", err)
   213  		return nil, err
   214  	}
   215  
   216  	sftp.Layout, err = backend.ParseLayout(sftp, cfg.Layout, defaultLayout, cfg.Path)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  
   221  	// test if config file already exists
   222  	_, err = sftp.c.Lstat(Join(cfg.Path, backend.Paths.Config))
   223  	if err == nil {
   224  		return nil, errors.New("config file already exists")
   225  	}
   226  
   227  	// create paths for data and refs
   228  	if err = sftp.mkdirAllDataSubdirs(); err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	err = sftp.Close()
   233  	if err != nil {
   234  		return nil, errors.Wrap(err, "Close")
   235  	}
   236  
   237  	// open backend
   238  	return Open(cfg)
   239  }
   240  
   241  // Location returns this backend's location (the directory name).
   242  func (r *SFTP) Location() string {
   243  	return r.p
   244  }
   245  
   246  func (r *SFTP) mkdirAll(dir string, mode os.FileMode) error {
   247  	// check if directory already exists
   248  	fi, err := r.c.Lstat(dir)
   249  	if err == nil {
   250  		if fi.IsDir() {
   251  			return nil
   252  		}
   253  
   254  		return errors.Errorf("mkdirAll(%s): entry exists but is not a directory", dir)
   255  	}
   256  
   257  	// create parent directories
   258  	errMkdirAll := r.mkdirAll(path.Dir(dir), backend.Modes.Dir)
   259  
   260  	// create directory
   261  	errMkdir := r.c.Mkdir(dir)
   262  
   263  	// test if directory was created successfully
   264  	fi, err = r.c.Lstat(dir)
   265  	if err != nil {
   266  		// return previous errors
   267  		return errors.Errorf("mkdirAll(%s): unable to create directories: %v, %v", dir, errMkdirAll, errMkdir)
   268  	}
   269  
   270  	if !fi.IsDir() {
   271  		return errors.Errorf("mkdirAll(%s): entry exists but is not a directory", dir)
   272  	}
   273  
   274  	// set mode
   275  	return r.c.Chmod(dir, mode)
   276  }
   277  
   278  // Join joins the given paths and cleans them afterwards. This always uses
   279  // forward slashes, which is required by sftp.
   280  func Join(parts ...string) string {
   281  	return path.Clean(path.Join(parts...))
   282  }
   283  
   284  // Save stores data in the backend at the handle.
   285  func (r *SFTP) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) {
   286  	debug.Log("Save %v", h)
   287  	if err := r.clientError(); err != nil {
   288  		return err
   289  	}
   290  
   291  	if err := h.Valid(); err != nil {
   292  		return err
   293  	}
   294  
   295  	filename := r.Filename(h)
   296  
   297  	// create new file
   298  	f, err := r.c.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY)
   299  
   300  	if r.IsNotExist(err) {
   301  		// error is caused by a missing directory, try to create it
   302  		mkdirErr := r.mkdirAll(r.Dirname(h), backend.Modes.Dir)
   303  		if mkdirErr != nil {
   304  			debug.Log("error creating dir %v: %v", r.Dirname(h), mkdirErr)
   305  		} else {
   306  			// try again
   307  			f, err = r.c.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY)
   308  		}
   309  	}
   310  
   311  	if err != nil {
   312  		return errors.Wrap(err, "OpenFile")
   313  	}
   314  
   315  	// save data
   316  	_, err = io.Copy(f, rd)
   317  	if err != nil {
   318  		_ = f.Close()
   319  		return errors.Wrap(err, "Write")
   320  	}
   321  
   322  	err = f.Close()
   323  	if err != nil {
   324  		return errors.Wrap(err, "Close")
   325  	}
   326  
   327  	return errors.Wrap(r.c.Chmod(filename, backend.Modes.File), "Chmod")
   328  }
   329  
   330  // Load runs fn with a reader that yields the contents of the file at h at the
   331  // given offset.
   332  func (r *SFTP) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error {
   333  	return backend.DefaultLoad(ctx, h, length, offset, r.openReader, fn)
   334  }
   335  
   336  func (r *SFTP) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) {
   337  	debug.Log("Load %v, length %v, offset %v", h, length, offset)
   338  	if err := h.Valid(); err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	if offset < 0 {
   343  		return nil, errors.New("offset is negative")
   344  	}
   345  
   346  	f, err := r.c.Open(r.Filename(h))
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  
   351  	if offset > 0 {
   352  		_, err = f.Seek(offset, 0)
   353  		if err != nil {
   354  			_ = f.Close()
   355  			return nil, err
   356  		}
   357  	}
   358  
   359  	if length > 0 {
   360  		return backend.LimitReadCloser(f, int64(length)), nil
   361  	}
   362  
   363  	return f, nil
   364  }
   365  
   366  // Stat returns information about a blob.
   367  func (r *SFTP) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) {
   368  	debug.Log("Stat(%v)", h)
   369  	if err := r.clientError(); err != nil {
   370  		return restic.FileInfo{}, err
   371  	}
   372  
   373  	if err := h.Valid(); err != nil {
   374  		return restic.FileInfo{}, err
   375  	}
   376  
   377  	fi, err := r.c.Lstat(r.Filename(h))
   378  	if err != nil {
   379  		return restic.FileInfo{}, errors.Wrap(err, "Lstat")
   380  	}
   381  
   382  	return restic.FileInfo{Size: fi.Size(), Name: h.Name}, nil
   383  }
   384  
   385  // Test returns true if a blob of the given type and name exists in the backend.
   386  func (r *SFTP) Test(ctx context.Context, h restic.Handle) (bool, error) {
   387  	debug.Log("Test(%v)", h)
   388  	if err := r.clientError(); err != nil {
   389  		return false, err
   390  	}
   391  
   392  	_, err := r.c.Lstat(r.Filename(h))
   393  	if os.IsNotExist(errors.Cause(err)) {
   394  		return false, nil
   395  	}
   396  
   397  	if err != nil {
   398  		return false, errors.Wrap(err, "Lstat")
   399  	}
   400  
   401  	return true, nil
   402  }
   403  
   404  // Remove removes the content stored at name.
   405  func (r *SFTP) Remove(ctx context.Context, h restic.Handle) error {
   406  	debug.Log("Remove(%v)", h)
   407  	if err := r.clientError(); err != nil {
   408  		return err
   409  	}
   410  
   411  	return r.c.Remove(r.Filename(h))
   412  }
   413  
   414  // List runs fn for each file in the backend which has the type t. When an
   415  // error occurs (or fn returns an error), List stops and returns it.
   416  func (r *SFTP) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error {
   417  	debug.Log("List %v", t)
   418  
   419  	basedir, subdirs := r.Basedir(t)
   420  	walker := r.c.Walk(basedir)
   421  	for walker.Step() {
   422  		if walker.Err() != nil {
   423  			return walker.Err()
   424  		}
   425  
   426  		if walker.Path() == basedir {
   427  			continue
   428  		}
   429  
   430  		if walker.Stat().IsDir() && !subdirs {
   431  			walker.SkipDir()
   432  			continue
   433  		}
   434  
   435  		fi := walker.Stat()
   436  		if !fi.Mode().IsRegular() {
   437  			continue
   438  		}
   439  
   440  		debug.Log("send %v\n", path.Base(walker.Path()))
   441  
   442  		rfi := restic.FileInfo{
   443  			Name: path.Base(walker.Path()),
   444  			Size: fi.Size(),
   445  		}
   446  
   447  		if ctx.Err() != nil {
   448  			return ctx.Err()
   449  		}
   450  
   451  		err := fn(rfi)
   452  		if err != nil {
   453  			return err
   454  		}
   455  
   456  		if ctx.Err() != nil {
   457  			return ctx.Err()
   458  		}
   459  	}
   460  
   461  	return ctx.Err()
   462  }
   463  
   464  var closeTimeout = 2 * time.Second
   465  
   466  // Close closes the sftp connection and terminates the underlying command.
   467  func (r *SFTP) Close() error {
   468  	debug.Log("Close")
   469  	if r == nil {
   470  		return nil
   471  	}
   472  
   473  	err := r.c.Close()
   474  	debug.Log("Close returned error %v", err)
   475  
   476  	// wait for closeTimeout before killing the process
   477  	select {
   478  	case err := <-r.result:
   479  		return err
   480  	case <-time.After(closeTimeout):
   481  	}
   482  
   483  	if err := r.cmd.Process.Kill(); err != nil {
   484  		return err
   485  	}
   486  
   487  	// get the error, but ignore it
   488  	<-r.result
   489  	return nil
   490  }
   491  
   492  func (r *SFTP) deleteRecursive(name string) error {
   493  	entries, err := r.ReadDir(name)
   494  	if err != nil {
   495  		return errors.Wrap(err, "ReadDir")
   496  	}
   497  
   498  	for _, fi := range entries {
   499  		itemName := r.Join(name, fi.Name())
   500  		if fi.IsDir() {
   501  			err := r.deleteRecursive(itemName)
   502  			if err != nil {
   503  				return errors.Wrap(err, "ReadDir")
   504  			}
   505  
   506  			err = r.c.RemoveDirectory(itemName)
   507  			if err != nil {
   508  				return errors.Wrap(err, "RemoveDirectory")
   509  			}
   510  
   511  			continue
   512  		}
   513  
   514  		err := r.c.Remove(itemName)
   515  		if err != nil {
   516  			return errors.Wrap(err, "ReadDir")
   517  		}
   518  	}
   519  
   520  	return nil
   521  }
   522  
   523  // Delete removes all data in the backend.
   524  func (r *SFTP) Delete(context.Context) error {
   525  	return r.deleteRecursive(r.p)
   526  }