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