github.com/10XDev/rclone@v1.52.3-0.20200626220027-16af9ab76b2a/backend/sftp/sftp.go (about)

     1  // Package sftp provides a filesystem interface using github.com/pkg/sftp
     2  
     3  // +build !plan9
     4  
     5  package sftp
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"fmt"
    11  	"io"
    12  	"io/ioutil"
    13  	"os"
    14  	"os/user"
    15  	"path"
    16  	"regexp"
    17  	"strconv"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/pkg/errors"
    23  	"github.com/pkg/sftp"
    24  	"github.com/rclone/rclone/fs"
    25  	"github.com/rclone/rclone/fs/config"
    26  	"github.com/rclone/rclone/fs/config/configmap"
    27  	"github.com/rclone/rclone/fs/config/configstruct"
    28  	"github.com/rclone/rclone/fs/config/obscure"
    29  	"github.com/rclone/rclone/fs/fshttp"
    30  	"github.com/rclone/rclone/fs/hash"
    31  	"github.com/rclone/rclone/lib/env"
    32  	"github.com/rclone/rclone/lib/pacer"
    33  	"github.com/rclone/rclone/lib/readers"
    34  	sshagent "github.com/xanzy/ssh-agent"
    35  	"golang.org/x/crypto/ssh"
    36  )
    37  
    38  const (
    39  	hashCommandNotSupported = "none"
    40  	minSleep                = 100 * time.Millisecond
    41  	maxSleep                = 2 * time.Second
    42  	decayConstant           = 2 // bigger for slower decay, exponential
    43  )
    44  
    45  var (
    46  	currentUser = readCurrentUser()
    47  )
    48  
    49  func init() {
    50  	fsi := &fs.RegInfo{
    51  		Name:        "sftp",
    52  		Description: "SSH/SFTP Connection",
    53  		NewFs:       NewFs,
    54  		Options: []fs.Option{{
    55  			Name:     "host",
    56  			Help:     "SSH host to connect to",
    57  			Required: true,
    58  			Examples: []fs.OptionExample{{
    59  				Value: "example.com",
    60  				Help:  "Connect to example.com",
    61  			}},
    62  		}, {
    63  			Name: "user",
    64  			Help: "SSH username, leave blank for current username, " + currentUser,
    65  		}, {
    66  			Name: "port",
    67  			Help: "SSH port, leave blank to use default (22)",
    68  		}, {
    69  			Name:       "pass",
    70  			Help:       "SSH password, leave blank to use ssh-agent.",
    71  			IsPassword: true,
    72  		}, {
    73  			Name: "key_pem",
    74  			Help: "Raw PEM-encoded private key, If specified, will override key_file parameter.",
    75  		}, {
    76  			Name: "key_file",
    77  			Help: "Path to PEM-encoded private key file, leave blank or set key-use-agent to use ssh-agent.",
    78  		}, {
    79  			Name: "key_file_pass",
    80  			Help: `The passphrase to decrypt the PEM-encoded private key file.
    81  
    82  Only PEM encrypted key files (old OpenSSH format) are supported. Encrypted keys
    83  in the new OpenSSH format can't be used.`,
    84  			IsPassword: true,
    85  		}, {
    86  			Name: "key_use_agent",
    87  			Help: `When set forces the usage of the ssh-agent.
    88  
    89  When key-file is also set, the ".pub" file of the specified key-file is read and only the associated key is
    90  requested from the ssh-agent. This allows to avoid ` + "`Too many authentication failures for *username*`" + ` errors
    91  when the ssh-agent contains many keys.`,
    92  			Default: false,
    93  		}, {
    94  			Name: "use_insecure_cipher",
    95  			Help: `Enable the use of insecure ciphers and key exchange methods. 
    96  
    97  This enables the use of the following insecure ciphers and key exchange methods:
    98  
    99  - aes128-cbc
   100  - aes192-cbc
   101  - aes256-cbc
   102  - 3des-cbc
   103  - diffie-hellman-group-exchange-sha256
   104  - diffie-hellman-group-exchange-sha1
   105  
   106  Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.`,
   107  			Default: false,
   108  			Examples: []fs.OptionExample{
   109  				{
   110  					Value: "false",
   111  					Help:  "Use default Cipher list.",
   112  				}, {
   113  					Value: "true",
   114  					Help:  "Enables the use of the aes128-cbc cipher and diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1 key exchange.",
   115  				},
   116  			},
   117  		}, {
   118  			Name:    "disable_hashcheck",
   119  			Default: false,
   120  			Help:    "Disable the execution of SSH commands to determine if remote file hashing is available.\nLeave blank or set to false to enable hashing (recommended), set to true to disable hashing.",
   121  		}, {
   122  			Name:    "ask_password",
   123  			Default: false,
   124  			Help: `Allow asking for SFTP password when needed.
   125  
   126  If this is set and no password is supplied then rclone will:
   127  - ask for a password
   128  - not contact the ssh agent
   129  `,
   130  			Advanced: true,
   131  		}, {
   132  			Name:    "path_override",
   133  			Default: "",
   134  			Help: `Override path used by SSH connection.
   135  
   136  This allows checksum calculation when SFTP and SSH paths are
   137  different. This issue affects among others Synology NAS boxes.
   138  
   139  Shared folders can be found in directories representing volumes
   140  
   141      rclone sync /home/local/directory remote:/directory --ssh-path-override /volume2/directory
   142  
   143  Home directory can be found in a shared folder called "home"
   144  
   145      rclone sync /home/local/directory remote:/home/directory --ssh-path-override /volume1/homes/USER/directory`,
   146  			Advanced: true,
   147  		}, {
   148  			Name:     "set_modtime",
   149  			Default:  true,
   150  			Help:     "Set the modified time on the remote if set.",
   151  			Advanced: true,
   152  		}, {
   153  			Name:     "md5sum_command",
   154  			Default:  "",
   155  			Help:     "The command used to read md5 hashes. Leave blank for autodetect.",
   156  			Advanced: true,
   157  		}, {
   158  			Name:     "sha1sum_command",
   159  			Default:  "",
   160  			Help:     "The command used to read sha1 hashes. Leave blank for autodetect.",
   161  			Advanced: true,
   162  		}, {
   163  			Name:     "skip_links",
   164  			Default:  false,
   165  			Help:     "Set to skip any symlinks and any other non regular files.",
   166  			Advanced: true,
   167  		}},
   168  	}
   169  	fs.Register(fsi)
   170  }
   171  
   172  // Options defines the configuration for this backend
   173  type Options struct {
   174  	Host              string `config:"host"`
   175  	User              string `config:"user"`
   176  	Port              string `config:"port"`
   177  	Pass              string `config:"pass"`
   178  	KeyPem            string `config:"key_pem"`
   179  	KeyFile           string `config:"key_file"`
   180  	KeyFilePass       string `config:"key_file_pass"`
   181  	KeyUseAgent       bool   `config:"key_use_agent"`
   182  	UseInsecureCipher bool   `config:"use_insecure_cipher"`
   183  	DisableHashCheck  bool   `config:"disable_hashcheck"`
   184  	AskPassword       bool   `config:"ask_password"`
   185  	PathOverride      string `config:"path_override"`
   186  	SetModTime        bool   `config:"set_modtime"`
   187  	Md5sumCommand     string `config:"md5sum_command"`
   188  	Sha1sumCommand    string `config:"sha1sum_command"`
   189  	SkipLinks         bool   `config:"skip_links"`
   190  }
   191  
   192  // Fs stores the interface to the remote SFTP files
   193  type Fs struct {
   194  	name         string
   195  	root         string
   196  	opt          Options          // parsed options
   197  	m            configmap.Mapper // config
   198  	features     *fs.Features     // optional features
   199  	config       *ssh.ClientConfig
   200  	url          string
   201  	mkdirLock    *stringLock
   202  	cachedHashes *hash.Set
   203  	poolMu       sync.Mutex
   204  	pool         []*conn
   205  	pacer        *fs.Pacer // pacer for operations
   206  }
   207  
   208  // Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading)
   209  type Object struct {
   210  	fs      *Fs
   211  	remote  string
   212  	size    int64       // size of the object
   213  	modTime time.Time   // modification time of the object
   214  	mode    os.FileMode // mode bits from the file
   215  	md5sum  *string     // Cached MD5 checksum
   216  	sha1sum *string     // Cached SHA1 checksum
   217  }
   218  
   219  // readCurrentUser finds the current user name or "" if not found
   220  func readCurrentUser() (userName string) {
   221  	usr, err := user.Current()
   222  	if err == nil {
   223  		return usr.Username
   224  	}
   225  	// Fall back to reading $USER then $LOGNAME
   226  	userName = os.Getenv("USER")
   227  	if userName != "" {
   228  		return userName
   229  	}
   230  	return os.Getenv("LOGNAME")
   231  }
   232  
   233  // dial starts a client connection to the given SSH server. It is a
   234  // convenience function that connects to the given network address,
   235  // initiates the SSH handshake, and then sets up a Client.
   236  func (f *Fs) dial(network, addr string, sshConfig *ssh.ClientConfig) (*ssh.Client, error) {
   237  	dialer := fshttp.NewDialer(fs.Config)
   238  	conn, err := dialer.Dial(network, addr)
   239  	if err != nil {
   240  		return nil, err
   241  	}
   242  	c, chans, reqs, err := ssh.NewClientConn(conn, addr, sshConfig)
   243  	if err != nil {
   244  		return nil, err
   245  	}
   246  	fs.Debugf(f, "New connection %s->%s to %q", c.LocalAddr(), c.RemoteAddr(), c.ServerVersion())
   247  	return ssh.NewClient(c, chans, reqs), nil
   248  }
   249  
   250  // conn encapsulates an ssh client and corresponding sftp client
   251  type conn struct {
   252  	sshClient  *ssh.Client
   253  	sftpClient *sftp.Client
   254  	err        chan error
   255  }
   256  
   257  // Wait for connection to close
   258  func (c *conn) wait() {
   259  	c.err <- c.sshClient.Conn.Wait()
   260  }
   261  
   262  // Closes the connection
   263  func (c *conn) close() error {
   264  	sftpErr := c.sftpClient.Close()
   265  	sshErr := c.sshClient.Close()
   266  	if sftpErr != nil {
   267  		return sftpErr
   268  	}
   269  	return sshErr
   270  }
   271  
   272  // Returns an error if closed
   273  func (c *conn) closed() error {
   274  	select {
   275  	case err := <-c.err:
   276  		return err
   277  	default:
   278  	}
   279  	return nil
   280  }
   281  
   282  // Open a new connection to the SFTP server.
   283  func (f *Fs) sftpConnection() (c *conn, err error) {
   284  	// Rate limit rate of new connections
   285  	c = &conn{
   286  		err: make(chan error, 1),
   287  	}
   288  	c.sshClient, err = f.dial("tcp", f.opt.Host+":"+f.opt.Port, f.config)
   289  	if err != nil {
   290  		return nil, errors.Wrap(err, "couldn't connect SSH")
   291  	}
   292  	c.sftpClient, err = sftp.NewClient(c.sshClient)
   293  	if err != nil {
   294  		_ = c.sshClient.Close()
   295  		return nil, errors.Wrap(err, "couldn't initialise SFTP")
   296  	}
   297  	go c.wait()
   298  	return c, nil
   299  }
   300  
   301  // Get an SFTP connection from the pool, or open a new one
   302  func (f *Fs) getSftpConnection() (c *conn, err error) {
   303  	f.poolMu.Lock()
   304  	for len(f.pool) > 0 {
   305  		c = f.pool[0]
   306  		f.pool = f.pool[1:]
   307  		err := c.closed()
   308  		if err == nil {
   309  			break
   310  		}
   311  		fs.Errorf(f, "Discarding closed SSH connection: %v", err)
   312  		c = nil
   313  	}
   314  	f.poolMu.Unlock()
   315  	if c != nil {
   316  		return c, nil
   317  	}
   318  	err = f.pacer.Call(func() (bool, error) {
   319  		c, err = f.sftpConnection()
   320  		if err != nil {
   321  			return true, err
   322  		}
   323  		return false, nil
   324  	})
   325  	return c, err
   326  }
   327  
   328  // Return an SFTP connection to the pool
   329  //
   330  // It nils the pointed to connection out so it can't be reused
   331  //
   332  // if err is not nil then it checks the connection is alive using a
   333  // Getwd request
   334  func (f *Fs) putSftpConnection(pc **conn, err error) {
   335  	c := *pc
   336  	*pc = nil
   337  	if err != nil {
   338  		// work out if this is an expected error
   339  		underlyingErr := errors.Cause(err)
   340  		isRegularError := false
   341  		switch underlyingErr {
   342  		case os.ErrNotExist:
   343  			isRegularError = true
   344  		default:
   345  			switch underlyingErr.(type) {
   346  			case *sftp.StatusError, *os.PathError:
   347  				isRegularError = true
   348  			}
   349  		}
   350  		// If not a regular SFTP error code then check the connection
   351  		if !isRegularError {
   352  			_, nopErr := c.sftpClient.Getwd()
   353  			if nopErr != nil {
   354  				fs.Debugf(f, "Connection failed, closing: %v", nopErr)
   355  				_ = c.close()
   356  				return
   357  			}
   358  			fs.Debugf(f, "Connection OK after error: %v", err)
   359  		}
   360  	}
   361  	f.poolMu.Lock()
   362  	f.pool = append(f.pool, c)
   363  	f.poolMu.Unlock()
   364  }
   365  
   366  // NewFs creates a new Fs object from the name and root. It connects to
   367  // the host specified in the config file.
   368  func NewFs(name, root string, m configmap.Mapper) (fs.Fs, error) {
   369  	ctx := context.Background()
   370  	// Parse config into Options struct
   371  	opt := new(Options)
   372  	err := configstruct.Set(m, opt)
   373  	if err != nil {
   374  		return nil, err
   375  	}
   376  	if opt.User == "" {
   377  		opt.User = currentUser
   378  	}
   379  	if opt.Port == "" {
   380  		opt.Port = "22"
   381  	}
   382  	sshConfig := &ssh.ClientConfig{
   383  		User:            opt.User,
   384  		Auth:            []ssh.AuthMethod{},
   385  		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
   386  		Timeout:         fs.Config.ConnectTimeout,
   387  		ClientVersion:   "SSH-2.0-" + fs.Config.UserAgent,
   388  	}
   389  
   390  	if opt.UseInsecureCipher {
   391  		sshConfig.Config.SetDefaults()
   392  		sshConfig.Config.Ciphers = append(sshConfig.Config.Ciphers, "aes128-cbc", "aes192-cbc", "aes256-cbc", "3des-cbc")
   393  		sshConfig.Config.KeyExchanges = append(sshConfig.Config.KeyExchanges, "diffie-hellman-group-exchange-sha1", "diffie-hellman-group-exchange-sha256")
   394  	}
   395  
   396  	keyFile := env.ShellExpand(opt.KeyFile)
   397  	//keyPem := env.ShellExpand(opt.KeyPem)
   398  	// Add ssh agent-auth if no password or file or key PEM specified
   399  	if (opt.Pass == "" && keyFile == "" && !opt.AskPassword && opt.KeyPem == "") || opt.KeyUseAgent {
   400  		sshAgentClient, _, err := sshagent.New()
   401  		if err != nil {
   402  			return nil, errors.Wrap(err, "couldn't connect to ssh-agent")
   403  		}
   404  		signers, err := sshAgentClient.Signers()
   405  		if err != nil {
   406  			return nil, errors.Wrap(err, "couldn't read ssh agent signers")
   407  		}
   408  		if keyFile != "" {
   409  			pubBytes, err := ioutil.ReadFile(keyFile + ".pub")
   410  			if err != nil {
   411  				return nil, errors.Wrap(err, "failed to read public key file")
   412  			}
   413  			pub, _, _, _, err := ssh.ParseAuthorizedKey(pubBytes)
   414  			if err != nil {
   415  				return nil, errors.Wrap(err, "failed to parse public key file")
   416  			}
   417  			pubM := pub.Marshal()
   418  			found := false
   419  			for _, s := range signers {
   420  				if bytes.Equal(pubM, s.PublicKey().Marshal()) {
   421  					sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(s))
   422  					found = true
   423  					break
   424  				}
   425  			}
   426  			if !found {
   427  				return nil, errors.New("private key not found in the ssh-agent")
   428  			}
   429  		} else {
   430  			sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signers...))
   431  		}
   432  	}
   433  
   434  	// Load key file if specified
   435  	if keyFile != "" || opt.KeyPem != "" {
   436  		var key []byte
   437  		if opt.KeyPem == "" {
   438  			key, err = ioutil.ReadFile(keyFile)
   439  			if err != nil {
   440  				return nil, errors.Wrap(err, "failed to read private key file")
   441  			}
   442  		} else {
   443  			// wrap in quotes because the config is a coming as a literal without them.
   444  			opt.KeyPem, err = strconv.Unquote("\"" + opt.KeyPem + "\"")
   445  			if err != nil {
   446  				return nil, errors.Wrap(err, "pem key not formatted properly")
   447  			}
   448  			key = []byte(opt.KeyPem)
   449  		}
   450  		clearpass := ""
   451  		if opt.KeyFilePass != "" {
   452  			clearpass, err = obscure.Reveal(opt.KeyFilePass)
   453  			if err != nil {
   454  				return nil, err
   455  			}
   456  		}
   457  		var signer ssh.Signer
   458  		if clearpass == "" {
   459  			signer, err = ssh.ParsePrivateKey(key)
   460  		} else {
   461  			signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(clearpass))
   462  		}
   463  		if err != nil {
   464  			return nil, errors.Wrap(err, "failed to parse private key file")
   465  		}
   466  		sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signer))
   467  	}
   468  
   469  	// Auth from password if specified
   470  	if opt.Pass != "" {
   471  		clearpass, err := obscure.Reveal(opt.Pass)
   472  		if err != nil {
   473  			return nil, err
   474  		}
   475  		sshConfig.Auth = append(sshConfig.Auth, ssh.Password(clearpass))
   476  	}
   477  
   478  	// Ask for password if none was defined and we're allowed to
   479  	if opt.Pass == "" && opt.AskPassword {
   480  		_, _ = fmt.Fprint(os.Stderr, "Enter SFTP password: ")
   481  		clearpass := config.ReadPassword()
   482  		sshConfig.Auth = append(sshConfig.Auth, ssh.Password(clearpass))
   483  	}
   484  
   485  	return NewFsWithConnection(ctx, name, root, m, opt, sshConfig)
   486  }
   487  
   488  // NewFsWithConnection creates a new Fs object from the name and root and an ssh.ClientConfig. It connects to
   489  // the host specified in the ssh.ClientConfig
   490  func NewFsWithConnection(ctx context.Context, name string, root string, m configmap.Mapper, opt *Options, sshConfig *ssh.ClientConfig) (fs.Fs, error) {
   491  	f := &Fs{
   492  		name:      name,
   493  		root:      root,
   494  		opt:       *opt,
   495  		m:         m,
   496  		config:    sshConfig,
   497  		url:       "sftp://" + opt.User + "@" + opt.Host + ":" + opt.Port + "/" + root,
   498  		mkdirLock: newStringLock(),
   499  		pacer:     fs.NewPacer(pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))),
   500  	}
   501  	f.features = (&fs.Features{
   502  		CanHaveEmptyDirectories: true,
   503  	}).Fill(f)
   504  	// Make a connection and pool it to return errors early
   505  	c, err := f.getSftpConnection()
   506  	if err != nil {
   507  		return nil, errors.Wrap(err, "NewFs")
   508  	}
   509  	f.putSftpConnection(&c, nil)
   510  	if root != "" {
   511  		// Check to see if the root actually an existing file
   512  		remote := path.Base(root)
   513  		f.root = path.Dir(root)
   514  		if f.root == "." {
   515  			f.root = ""
   516  		}
   517  		_, err := f.NewObject(ctx, remote)
   518  		if err != nil {
   519  			if err == fs.ErrorObjectNotFound || errors.Cause(err) == fs.ErrorNotAFile {
   520  				// File doesn't exist so return old f
   521  				f.root = root
   522  				return f, nil
   523  			}
   524  			return nil, err
   525  		}
   526  		// return an error with an fs which points to the parent
   527  		return f, fs.ErrorIsFile
   528  	}
   529  	return f, nil
   530  }
   531  
   532  // Name returns the configured name of the file system
   533  func (f *Fs) Name() string {
   534  	return f.name
   535  }
   536  
   537  // Root returns the root for the filesystem
   538  func (f *Fs) Root() string {
   539  	return f.root
   540  }
   541  
   542  // String returns the URL for the filesystem
   543  func (f *Fs) String() string {
   544  	return f.url
   545  }
   546  
   547  // Features returns the optional features of this Fs
   548  func (f *Fs) Features() *fs.Features {
   549  	return f.features
   550  }
   551  
   552  // Precision is the remote sftp file system's modtime precision, which we have no way of knowing. We estimate at 1s
   553  func (f *Fs) Precision() time.Duration {
   554  	return time.Second
   555  }
   556  
   557  // NewObject creates a new remote sftp file object
   558  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
   559  	o := &Object{
   560  		fs:     f,
   561  		remote: remote,
   562  	}
   563  	err := o.stat()
   564  	if err != nil {
   565  		return nil, err
   566  	}
   567  	return o, nil
   568  }
   569  
   570  // dirExists returns true,nil if the directory exists, false, nil if
   571  // it doesn't or false, err
   572  func (f *Fs) dirExists(dir string) (bool, error) {
   573  	if dir == "" {
   574  		dir = "."
   575  	}
   576  	c, err := f.getSftpConnection()
   577  	if err != nil {
   578  		return false, errors.Wrap(err, "dirExists")
   579  	}
   580  	info, err := c.sftpClient.Stat(dir)
   581  	f.putSftpConnection(&c, err)
   582  	if err != nil {
   583  		if os.IsNotExist(err) {
   584  			return false, nil
   585  		}
   586  		return false, errors.Wrap(err, "dirExists stat failed")
   587  	}
   588  	if !info.IsDir() {
   589  		return false, fs.ErrorIsFile
   590  	}
   591  	return true, nil
   592  }
   593  
   594  // List the objects and directories in dir into entries.  The
   595  // entries can be returned in any order but should be for a
   596  // complete directory.
   597  //
   598  // dir should be "" to list the root, and should not have
   599  // trailing slashes.
   600  //
   601  // This should return ErrDirNotFound if the directory isn't
   602  // found.
   603  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
   604  	root := path.Join(f.root, dir)
   605  	ok, err := f.dirExists(root)
   606  	if err != nil {
   607  		return nil, errors.Wrap(err, "List failed")
   608  	}
   609  	if !ok {
   610  		return nil, fs.ErrorDirNotFound
   611  	}
   612  	sftpDir := root
   613  	if sftpDir == "" {
   614  		sftpDir = "."
   615  	}
   616  	c, err := f.getSftpConnection()
   617  	if err != nil {
   618  		return nil, errors.Wrap(err, "List")
   619  	}
   620  	infos, err := c.sftpClient.ReadDir(sftpDir)
   621  	f.putSftpConnection(&c, err)
   622  	if err != nil {
   623  		return nil, errors.Wrapf(err, "error listing %q", dir)
   624  	}
   625  	for _, info := range infos {
   626  		remote := path.Join(dir, info.Name())
   627  		// If file is a symlink (not a regular file is the best cross platform test we can do), do a stat to
   628  		// pick up the size and type of the destination, instead of the size and type of the symlink.
   629  		if !info.Mode().IsRegular() && !info.IsDir() {
   630  			if f.opt.SkipLinks {
   631  				// skip non regular file if SkipLinks is set
   632  				continue
   633  			}
   634  			oldInfo := info
   635  			info, err = f.stat(remote)
   636  			if err != nil {
   637  				if !os.IsNotExist(err) {
   638  					fs.Errorf(remote, "stat of non-regular file failed: %v", err)
   639  				}
   640  				info = oldInfo
   641  			}
   642  		}
   643  		if info.IsDir() {
   644  			d := fs.NewDir(remote, info.ModTime())
   645  			entries = append(entries, d)
   646  		} else {
   647  			o := &Object{
   648  				fs:     f,
   649  				remote: remote,
   650  			}
   651  			o.setMetadata(info)
   652  			entries = append(entries, o)
   653  		}
   654  	}
   655  	return entries, nil
   656  }
   657  
   658  // Put data from <in> into a new remote sftp file object described by <src.Remote()> and <src.ModTime(ctx)>
   659  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   660  	err := f.mkParentDir(src.Remote())
   661  	if err != nil {
   662  		return nil, errors.Wrap(err, "Put mkParentDir failed")
   663  	}
   664  	// Temporary object under construction
   665  	o := &Object{
   666  		fs:     f,
   667  		remote: src.Remote(),
   668  	}
   669  	err = o.Update(ctx, in, src, options...)
   670  	if err != nil {
   671  		return nil, err
   672  	}
   673  	return o, nil
   674  }
   675  
   676  // PutStream uploads to the remote path with the modTime given of indeterminate size
   677  func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
   678  	return f.Put(ctx, in, src, options...)
   679  }
   680  
   681  // mkParentDir makes the parent of remote if necessary and any
   682  // directories above that
   683  func (f *Fs) mkParentDir(remote string) error {
   684  	parent := path.Dir(remote)
   685  	return f.mkdir(path.Join(f.root, parent))
   686  }
   687  
   688  // mkdir makes the directory and parents using native paths
   689  func (f *Fs) mkdir(dirPath string) error {
   690  	f.mkdirLock.Lock(dirPath)
   691  	defer f.mkdirLock.Unlock(dirPath)
   692  	if dirPath == "." || dirPath == "/" {
   693  		return nil
   694  	}
   695  	ok, err := f.dirExists(dirPath)
   696  	if err != nil {
   697  		return errors.Wrap(err, "mkdir dirExists failed")
   698  	}
   699  	if ok {
   700  		return nil
   701  	}
   702  	parent := path.Dir(dirPath)
   703  	err = f.mkdir(parent)
   704  	if err != nil {
   705  		return err
   706  	}
   707  	c, err := f.getSftpConnection()
   708  	if err != nil {
   709  		return errors.Wrap(err, "mkdir")
   710  	}
   711  	err = c.sftpClient.Mkdir(dirPath)
   712  	f.putSftpConnection(&c, err)
   713  	if err != nil {
   714  		return errors.Wrapf(err, "mkdir %q failed", dirPath)
   715  	}
   716  	return nil
   717  }
   718  
   719  // Mkdir makes the root directory of the Fs object
   720  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
   721  	root := path.Join(f.root, dir)
   722  	return f.mkdir(root)
   723  }
   724  
   725  // Rmdir removes the root directory of the Fs object
   726  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
   727  	// Check to see if directory is empty as some servers will
   728  	// delete recursively with RemoveDirectory
   729  	entries, err := f.List(ctx, dir)
   730  	if err != nil {
   731  		return errors.Wrap(err, "Rmdir")
   732  	}
   733  	if len(entries) != 0 {
   734  		return fs.ErrorDirectoryNotEmpty
   735  	}
   736  	// Remove the directory
   737  	root := path.Join(f.root, dir)
   738  	c, err := f.getSftpConnection()
   739  	if err != nil {
   740  		return errors.Wrap(err, "Rmdir")
   741  	}
   742  	err = c.sftpClient.RemoveDirectory(root)
   743  	f.putSftpConnection(&c, err)
   744  	return err
   745  }
   746  
   747  // Move renames a remote sftp file object
   748  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
   749  	srcObj, ok := src.(*Object)
   750  	if !ok {
   751  		fs.Debugf(src, "Can't move - not same remote type")
   752  		return nil, fs.ErrorCantMove
   753  	}
   754  	err := f.mkParentDir(remote)
   755  	if err != nil {
   756  		return nil, errors.Wrap(err, "Move mkParentDir failed")
   757  	}
   758  	c, err := f.getSftpConnection()
   759  	if err != nil {
   760  		return nil, errors.Wrap(err, "Move")
   761  	}
   762  	err = c.sftpClient.Rename(
   763  		srcObj.path(),
   764  		path.Join(f.root, remote),
   765  	)
   766  	f.putSftpConnection(&c, err)
   767  	if err != nil {
   768  		return nil, errors.Wrap(err, "Move Rename failed")
   769  	}
   770  	dstObj, err := f.NewObject(ctx, remote)
   771  	if err != nil {
   772  		return nil, errors.Wrap(err, "Move NewObject failed")
   773  	}
   774  	return dstObj, nil
   775  }
   776  
   777  // DirMove moves src, srcRemote to this remote at dstRemote
   778  // using server side move operations.
   779  //
   780  // Will only be called if src.Fs().Name() == f.Name()
   781  //
   782  // If it isn't possible then return fs.ErrorCantDirMove
   783  //
   784  // If destination exists then return fs.ErrorDirExists
   785  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
   786  	srcFs, ok := src.(*Fs)
   787  	if !ok {
   788  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
   789  		return fs.ErrorCantDirMove
   790  	}
   791  	srcPath := path.Join(srcFs.root, srcRemote)
   792  	dstPath := path.Join(f.root, dstRemote)
   793  
   794  	// Check if destination exists
   795  	ok, err := f.dirExists(dstPath)
   796  	if err != nil {
   797  		return errors.Wrap(err, "DirMove dirExists dst failed")
   798  	}
   799  	if ok {
   800  		return fs.ErrorDirExists
   801  	}
   802  
   803  	// Make sure the parent directory exists
   804  	err = f.mkdir(path.Dir(dstPath))
   805  	if err != nil {
   806  		return errors.Wrap(err, "DirMove mkParentDir dst failed")
   807  	}
   808  
   809  	// Do the move
   810  	c, err := f.getSftpConnection()
   811  	if err != nil {
   812  		return errors.Wrap(err, "DirMove")
   813  	}
   814  	err = c.sftpClient.Rename(
   815  		srcPath,
   816  		dstPath,
   817  	)
   818  	f.putSftpConnection(&c, err)
   819  	if err != nil {
   820  		return errors.Wrapf(err, "DirMove Rename(%q,%q) failed", srcPath, dstPath)
   821  	}
   822  	return nil
   823  }
   824  
   825  // run runds cmd on the remote end returning standard output
   826  func (f *Fs) run(cmd string) ([]byte, error) {
   827  	c, err := f.getSftpConnection()
   828  	if err != nil {
   829  		return nil, errors.Wrap(err, "run: get SFTP connection")
   830  	}
   831  	defer f.putSftpConnection(&c, err)
   832  
   833  	session, err := c.sshClient.NewSession()
   834  	if err != nil {
   835  		return nil, errors.Wrap(err, "run: get SFTP sessiion")
   836  	}
   837  	defer func() {
   838  		_ = session.Close()
   839  	}()
   840  
   841  	var stdout, stderr bytes.Buffer
   842  	session.Stdout = &stdout
   843  	session.Stderr = &stderr
   844  
   845  	err = session.Run(cmd)
   846  	if err != nil {
   847  		return nil, errors.Wrapf(err, "failed to run %q: %s", cmd, stderr.Bytes())
   848  	}
   849  
   850  	return stdout.Bytes(), nil
   851  }
   852  
   853  // Hashes returns the supported hash types of the filesystem
   854  func (f *Fs) Hashes() hash.Set {
   855  	if f.opt.DisableHashCheck {
   856  		return hash.Set(hash.None)
   857  	}
   858  
   859  	if f.cachedHashes != nil {
   860  		return *f.cachedHashes
   861  	}
   862  
   863  	// look for a hash command which works
   864  	checkHash := func(commands []string, expected string, hashCommand *string, changed *bool) bool {
   865  		if *hashCommand == hashCommandNotSupported {
   866  			return false
   867  		}
   868  		if *hashCommand != "" {
   869  			return true
   870  		}
   871  		*changed = true
   872  		for _, command := range commands {
   873  			output, err := f.run(command)
   874  			if err != nil {
   875  				continue
   876  			}
   877  			output = bytes.TrimSpace(output)
   878  			fs.Debugf(f, "checking %q command: %q", command, output)
   879  			if parseHash(output) == expected {
   880  				*hashCommand = command
   881  				return true
   882  			}
   883  		}
   884  		*hashCommand = hashCommandNotSupported
   885  		return false
   886  	}
   887  
   888  	changed := false
   889  	md5Works := checkHash([]string{"md5sum", "md5 -r"}, "d41d8cd98f00b204e9800998ecf8427e", &f.opt.Md5sumCommand, &changed)
   890  	sha1Works := checkHash([]string{"sha1sum", "sha1 -r"}, "da39a3ee5e6b4b0d3255bfef95601890afd80709", &f.opt.Sha1sumCommand, &changed)
   891  
   892  	if changed {
   893  		f.m.Set("md5sum_command", f.opt.Md5sumCommand)
   894  		f.m.Set("sha1sum_command", f.opt.Sha1sumCommand)
   895  	}
   896  
   897  	set := hash.NewHashSet()
   898  	if sha1Works {
   899  		set.Add(hash.SHA1)
   900  	}
   901  	if md5Works {
   902  		set.Add(hash.MD5)
   903  	}
   904  
   905  	f.cachedHashes = &set
   906  	return set
   907  }
   908  
   909  // About gets usage stats
   910  func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
   911  	escapedPath := shellEscape(f.root)
   912  	if f.opt.PathOverride != "" {
   913  		escapedPath = shellEscape(path.Join(f.opt.PathOverride, f.root))
   914  	}
   915  	if len(escapedPath) == 0 {
   916  		escapedPath = "/"
   917  	}
   918  	stdout, err := f.run("df -k " + escapedPath)
   919  	if err != nil {
   920  		return nil, errors.Wrap(err, "your remote may not support About")
   921  	}
   922  
   923  	usageTotal, usageUsed, usageAvail := parseUsage(stdout)
   924  	usage := &fs.Usage{}
   925  	if usageTotal >= 0 {
   926  		usage.Total = fs.NewUsageValue(usageTotal)
   927  	}
   928  	if usageUsed >= 0 {
   929  		usage.Used = fs.NewUsageValue(usageUsed)
   930  	}
   931  	if usageAvail >= 0 {
   932  		usage.Free = fs.NewUsageValue(usageAvail)
   933  	}
   934  	return usage, nil
   935  }
   936  
   937  // Fs is the filesystem this remote sftp file object is located within
   938  func (o *Object) Fs() fs.Info {
   939  	return o.fs
   940  }
   941  
   942  // String returns the URL to the remote SFTP file
   943  func (o *Object) String() string {
   944  	if o == nil {
   945  		return "<nil>"
   946  	}
   947  	return o.remote
   948  }
   949  
   950  // Remote the name of the remote SFTP file, relative to the fs root
   951  func (o *Object) Remote() string {
   952  	return o.remote
   953  }
   954  
   955  // Hash returns the selected checksum of the file
   956  // If no checksum is available it returns ""
   957  func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
   958  	if o.fs.opt.DisableHashCheck {
   959  		return "", nil
   960  	}
   961  	_ = o.fs.Hashes()
   962  
   963  	var hashCmd string
   964  	if r == hash.MD5 {
   965  		if o.md5sum != nil {
   966  			return *o.md5sum, nil
   967  		}
   968  		hashCmd = o.fs.opt.Md5sumCommand
   969  	} else if r == hash.SHA1 {
   970  		if o.sha1sum != nil {
   971  			return *o.sha1sum, nil
   972  		}
   973  		hashCmd = o.fs.opt.Sha1sumCommand
   974  	} else {
   975  		return "", hash.ErrUnsupported
   976  	}
   977  	if hashCmd == "" || hashCmd == hashCommandNotSupported {
   978  		return "", hash.ErrUnsupported
   979  	}
   980  
   981  	c, err := o.fs.getSftpConnection()
   982  	if err != nil {
   983  		return "", errors.Wrap(err, "Hash get SFTP connection")
   984  	}
   985  	session, err := c.sshClient.NewSession()
   986  	o.fs.putSftpConnection(&c, err)
   987  	if err != nil {
   988  		return "", errors.Wrap(err, "Hash put SFTP connection")
   989  	}
   990  
   991  	var stdout, stderr bytes.Buffer
   992  	session.Stdout = &stdout
   993  	session.Stderr = &stderr
   994  	escapedPath := shellEscape(o.path())
   995  	if o.fs.opt.PathOverride != "" {
   996  		escapedPath = shellEscape(path.Join(o.fs.opt.PathOverride, o.remote))
   997  	}
   998  	err = session.Run(hashCmd + " " + escapedPath)
   999  	fs.Debugf(nil, "sftp cmd = %s", escapedPath)
  1000  	if err != nil {
  1001  		_ = session.Close()
  1002  		fs.Debugf(o, "Failed to calculate %v hash: %v (%s)", r, err, bytes.TrimSpace(stderr.Bytes()))
  1003  		return "", nil
  1004  	}
  1005  
  1006  	_ = session.Close()
  1007  	b := stdout.Bytes()
  1008  	fs.Debugf(nil, "sftp output = %q", b)
  1009  	str := parseHash(b)
  1010  	fs.Debugf(nil, "sftp hash = %q", str)
  1011  	if r == hash.MD5 {
  1012  		o.md5sum = &str
  1013  	} else if r == hash.SHA1 {
  1014  		o.sha1sum = &str
  1015  	}
  1016  	return str, nil
  1017  }
  1018  
  1019  var shellEscapeRegex = regexp.MustCompile("[^A-Za-z0-9_.,:/\\@\u0080-\uFFFFFFFF\n-]")
  1020  
  1021  // Escape a string s.t. it cannot cause unintended behavior
  1022  // when sending it to a shell.
  1023  func shellEscape(str string) string {
  1024  	safe := shellEscapeRegex.ReplaceAllString(str, `\$0`)
  1025  	return strings.Replace(safe, "\n", "'\n'", -1)
  1026  }
  1027  
  1028  // Converts a byte array from the SSH session returned by
  1029  // an invocation of md5sum/sha1sum to a hash string
  1030  // as expected by the rest of this application
  1031  func parseHash(bytes []byte) string {
  1032  	// For strings with backslash *sum writes a leading \
  1033  	// https://unix.stackexchange.com/q/313733/94054
  1034  	return strings.Split(strings.TrimLeft(string(bytes), "\\"), " ")[0] // Split at hash / filename separator
  1035  }
  1036  
  1037  // Parses the byte array output from the SSH session
  1038  // returned by an invocation of df into
  1039  // the disk size, used space, and available space on the disk, in that order.
  1040  // Only works when `df` has output info on only one disk
  1041  func parseUsage(bytes []byte) (spaceTotal int64, spaceUsed int64, spaceAvail int64) {
  1042  	spaceTotal, spaceUsed, spaceAvail = -1, -1, -1
  1043  	lines := strings.Split(string(bytes), "\n")
  1044  	if len(lines) < 2 {
  1045  		return
  1046  	}
  1047  	split := strings.Fields(lines[1])
  1048  	if len(split) < 6 {
  1049  		return
  1050  	}
  1051  	spaceTotal, err := strconv.ParseInt(split[1], 10, 64)
  1052  	if err != nil {
  1053  		spaceTotal = -1
  1054  	}
  1055  	spaceUsed, err = strconv.ParseInt(split[2], 10, 64)
  1056  	if err != nil {
  1057  		spaceUsed = -1
  1058  	}
  1059  	spaceAvail, err = strconv.ParseInt(split[3], 10, 64)
  1060  	if err != nil {
  1061  		spaceAvail = -1
  1062  	}
  1063  	return spaceTotal * 1024, spaceUsed * 1024, spaceAvail * 1024
  1064  }
  1065  
  1066  // Size returns the size in bytes of the remote sftp file
  1067  func (o *Object) Size() int64 {
  1068  	return o.size
  1069  }
  1070  
  1071  // ModTime returns the modification time of the remote sftp file
  1072  func (o *Object) ModTime(ctx context.Context) time.Time {
  1073  	return o.modTime
  1074  }
  1075  
  1076  // path returns the native path of the object
  1077  func (o *Object) path() string {
  1078  	return path.Join(o.fs.root, o.remote)
  1079  }
  1080  
  1081  // setMetadata updates the info in the object from the stat result passed in
  1082  func (o *Object) setMetadata(info os.FileInfo) {
  1083  	o.modTime = info.ModTime()
  1084  	o.size = info.Size()
  1085  	o.mode = info.Mode()
  1086  }
  1087  
  1088  // statRemote stats the file or directory at the remote given
  1089  func (f *Fs) stat(remote string) (info os.FileInfo, err error) {
  1090  	c, err := f.getSftpConnection()
  1091  	if err != nil {
  1092  		return nil, errors.Wrap(err, "stat")
  1093  	}
  1094  	absPath := path.Join(f.root, remote)
  1095  	info, err = c.sftpClient.Stat(absPath)
  1096  	f.putSftpConnection(&c, err)
  1097  	return info, err
  1098  }
  1099  
  1100  // stat updates the info in the Object
  1101  func (o *Object) stat() error {
  1102  	info, err := o.fs.stat(o.remote)
  1103  	if err != nil {
  1104  		if os.IsNotExist(err) {
  1105  			return fs.ErrorObjectNotFound
  1106  		}
  1107  		return errors.Wrap(err, "stat failed")
  1108  	}
  1109  	if info.IsDir() {
  1110  		return errors.Wrapf(fs.ErrorNotAFile, "%q", o.remote)
  1111  	}
  1112  	o.setMetadata(info)
  1113  	return nil
  1114  }
  1115  
  1116  // SetModTime sets the modification and access time to the specified time
  1117  //
  1118  // it also updates the info field
  1119  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
  1120  	if o.fs.opt.SetModTime {
  1121  		c, err := o.fs.getSftpConnection()
  1122  		if err != nil {
  1123  			return errors.Wrap(err, "SetModTime")
  1124  		}
  1125  		err = c.sftpClient.Chtimes(o.path(), modTime, modTime)
  1126  		o.fs.putSftpConnection(&c, err)
  1127  		if err != nil {
  1128  			return errors.Wrap(err, "SetModTime failed")
  1129  		}
  1130  	}
  1131  	err := o.stat()
  1132  	if err != nil {
  1133  		return errors.Wrap(err, "SetModTime stat failed")
  1134  	}
  1135  	return nil
  1136  }
  1137  
  1138  // Storable returns whether the remote sftp file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc)
  1139  func (o *Object) Storable() bool {
  1140  	return o.mode.IsRegular()
  1141  }
  1142  
  1143  // objectReader represents a file open for reading on the SFTP server
  1144  type objectReader struct {
  1145  	sftpFile   *sftp.File
  1146  	pipeReader *io.PipeReader
  1147  	done       chan struct{}
  1148  }
  1149  
  1150  func newObjectReader(sftpFile *sftp.File) *objectReader {
  1151  	pipeReader, pipeWriter := io.Pipe()
  1152  	file := &objectReader{
  1153  		sftpFile:   sftpFile,
  1154  		pipeReader: pipeReader,
  1155  		done:       make(chan struct{}),
  1156  	}
  1157  
  1158  	go func() {
  1159  		// Use sftpFile.WriteTo to pump data so that it gets a
  1160  		// chance to build the window up.
  1161  		_, err := sftpFile.WriteTo(pipeWriter)
  1162  		// Close the pipeWriter so the pipeReader fails with
  1163  		// the same error or EOF if err == nil
  1164  		_ = pipeWriter.CloseWithError(err)
  1165  		// signal that we've finished
  1166  		close(file.done)
  1167  	}()
  1168  
  1169  	return file
  1170  }
  1171  
  1172  // Read from a remote sftp file object reader
  1173  func (file *objectReader) Read(p []byte) (n int, err error) {
  1174  	n, err = file.pipeReader.Read(p)
  1175  	return n, err
  1176  }
  1177  
  1178  // Close a reader of a remote sftp file
  1179  func (file *objectReader) Close() (err error) {
  1180  	// Close the sftpFile - this will likely cause the WriteTo to error
  1181  	err = file.sftpFile.Close()
  1182  	// Close the pipeReader so writes to the pipeWriter fail
  1183  	_ = file.pipeReader.Close()
  1184  	// Wait for the background process to finish
  1185  	<-file.done
  1186  	return err
  1187  }
  1188  
  1189  // Open a remote sftp file object for reading. Seek is supported
  1190  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  1191  	var offset, limit int64 = 0, -1
  1192  	for _, option := range options {
  1193  		switch x := option.(type) {
  1194  		case *fs.SeekOption:
  1195  			offset = x.Offset
  1196  		case *fs.RangeOption:
  1197  			offset, limit = x.Decode(o.Size())
  1198  		default:
  1199  			if option.Mandatory() {
  1200  				fs.Logf(o, "Unsupported mandatory option: %v", option)
  1201  			}
  1202  		}
  1203  	}
  1204  	c, err := o.fs.getSftpConnection()
  1205  	if err != nil {
  1206  		return nil, errors.Wrap(err, "Open")
  1207  	}
  1208  	sftpFile, err := c.sftpClient.Open(o.path())
  1209  	o.fs.putSftpConnection(&c, err)
  1210  	if err != nil {
  1211  		return nil, errors.Wrap(err, "Open failed")
  1212  	}
  1213  	if offset > 0 {
  1214  		off, err := sftpFile.Seek(offset, io.SeekStart)
  1215  		if err != nil || off != offset {
  1216  			return nil, errors.Wrap(err, "Open Seek failed")
  1217  		}
  1218  	}
  1219  	in = readers.NewLimitedReadCloser(newObjectReader(sftpFile), limit)
  1220  	return in, nil
  1221  }
  1222  
  1223  // Update a remote sftp file using the data <in> and ModTime from <src>
  1224  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
  1225  	// Clear the hash cache since we are about to update the object
  1226  	o.md5sum = nil
  1227  	o.sha1sum = nil
  1228  	c, err := o.fs.getSftpConnection()
  1229  	if err != nil {
  1230  		return errors.Wrap(err, "Update")
  1231  	}
  1232  	file, err := c.sftpClient.OpenFile(o.path(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
  1233  	o.fs.putSftpConnection(&c, err)
  1234  	if err != nil {
  1235  		return errors.Wrap(err, "Update Create failed")
  1236  	}
  1237  	// remove the file if upload failed
  1238  	remove := func() {
  1239  		c, removeErr := o.fs.getSftpConnection()
  1240  		if removeErr != nil {
  1241  			fs.Debugf(src, "Failed to open new SSH connection for delete: %v", removeErr)
  1242  			return
  1243  		}
  1244  		removeErr = c.sftpClient.Remove(o.path())
  1245  		o.fs.putSftpConnection(&c, removeErr)
  1246  		if removeErr != nil {
  1247  			fs.Debugf(src, "Failed to remove: %v", removeErr)
  1248  		} else {
  1249  			fs.Debugf(src, "Removed after failed upload: %v", err)
  1250  		}
  1251  	}
  1252  	_, err = file.ReadFrom(in)
  1253  	if err != nil {
  1254  		remove()
  1255  		return errors.Wrap(err, "Update ReadFrom failed")
  1256  	}
  1257  	err = file.Close()
  1258  	if err != nil {
  1259  		remove()
  1260  		return errors.Wrap(err, "Update Close failed")
  1261  	}
  1262  	err = o.SetModTime(ctx, src.ModTime(ctx))
  1263  	if err != nil {
  1264  		return errors.Wrap(err, "Update SetModTime failed")
  1265  	}
  1266  	return nil
  1267  }
  1268  
  1269  // Remove a remote sftp file object
  1270  func (o *Object) Remove(ctx context.Context) error {
  1271  	c, err := o.fs.getSftpConnection()
  1272  	if err != nil {
  1273  		return errors.Wrap(err, "Remove")
  1274  	}
  1275  	err = c.sftpClient.Remove(o.path())
  1276  	o.fs.putSftpConnection(&c, err)
  1277  	return err
  1278  }
  1279  
  1280  // Check the interfaces are satisfied
  1281  var (
  1282  	_ fs.Fs          = &Fs{}
  1283  	_ fs.PutStreamer = &Fs{}
  1284  	_ fs.Mover       = &Fs{}
  1285  	_ fs.DirMover    = &Fs{}
  1286  	_ fs.Abouter     = &Fs{}
  1287  	_ fs.Object      = &Object{}
  1288  )