github.com/rclone/rclone@v1.66.1-0.20240517100346-7b89735ae726/backend/sftp/sftp.go (about)

     1  //go:build !plan9
     2  
     3  // Package sftp provides a filesystem interface using github.com/pkg/sftp
     4  package sftp
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	iofs "io/fs"
    13  	"os"
    14  	"path"
    15  	"regexp"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  	"sync/atomic"
    20  	"time"
    21  
    22  	"github.com/pkg/sftp"
    23  	"github.com/rclone/rclone/fs"
    24  	"github.com/rclone/rclone/fs/accounting"
    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/hash"
    30  	"github.com/rclone/rclone/lib/env"
    31  	"github.com/rclone/rclone/lib/pacer"
    32  	"github.com/rclone/rclone/lib/readers"
    33  	sshagent "github.com/xanzy/ssh-agent"
    34  	"golang.org/x/crypto/ssh"
    35  	"golang.org/x/crypto/ssh/knownhosts"
    36  )
    37  
    38  const (
    39  	defaultShellType        = "unix"
    40  	shellTypeNotSupported   = "none"
    41  	hashCommandNotSupported = "none"
    42  	minSleep                = 100 * time.Millisecond
    43  	maxSleep                = 2 * time.Second
    44  	decayConstant           = 2           // bigger for slower decay, exponential
    45  	keepAliveInterval       = time.Minute // send keepalives every this long while running commands
    46  )
    47  
    48  var (
    49  	currentUser          = env.CurrentUser()
    50  	posixWinAbsPathRegex = regexp.MustCompile(`^/[a-zA-Z]\:($|/)`) // E.g. "/C:" or anything starting with "/C:/"
    51  	unixShellEscapeRegex = regexp.MustCompile("[^A-Za-z0-9_.,:/\\@\u0080-\uFFFFFFFF\n-]")
    52  )
    53  
    54  func init() {
    55  	fsi := &fs.RegInfo{
    56  		Name:        "sftp",
    57  		Description: "SSH/SFTP",
    58  		NewFs:       NewFs,
    59  		Options: []fs.Option{{
    60  			Name:      "host",
    61  			Help:      "SSH host to connect to.\n\nE.g. \"example.com\".",
    62  			Required:  true,
    63  			Sensitive: true,
    64  		}, {
    65  			Name:      "user",
    66  			Help:      "SSH username.",
    67  			Default:   currentUser,
    68  			Sensitive: true,
    69  		}, {
    70  			Name:    "port",
    71  			Help:    "SSH port number.",
    72  			Default: 22,
    73  		}, {
    74  			Name:       "pass",
    75  			Help:       "SSH password, leave blank to use ssh-agent.",
    76  			IsPassword: true,
    77  		}, {
    78  			Name:      "key_pem",
    79  			Help:      "Raw PEM-encoded private key.\n\nIf specified, will override key_file parameter.",
    80  			Sensitive: true,
    81  		}, {
    82  			Name: "key_file",
    83  			Help: "Path to PEM-encoded private key file.\n\nLeave blank or set key-use-agent to use ssh-agent." + env.ShellExpandHelp,
    84  		}, {
    85  			Name: "key_file_pass",
    86  			Help: `The passphrase to decrypt the PEM-encoded private key file.
    87  
    88  Only PEM encrypted key files (old OpenSSH format) are supported. Encrypted keys
    89  in the new OpenSSH format can't be used.`,
    90  			IsPassword: true,
    91  			Sensitive:  true,
    92  		}, {
    93  			Name: "pubkey_file",
    94  			Help: `Optional path to public key file.
    95  
    96  Set this if you have a signed certificate you want to use for authentication.` + env.ShellExpandHelp,
    97  		}, {
    98  			Name: "known_hosts_file",
    99  			Help: `Optional path to known_hosts file.
   100  
   101  Set this value to enable server host key validation.` + env.ShellExpandHelp,
   102  			Advanced: true,
   103  			Examples: []fs.OptionExample{{
   104  				Value: "~/.ssh/known_hosts",
   105  				Help:  "Use OpenSSH's known_hosts file.",
   106  			}},
   107  		}, {
   108  			Name: "key_use_agent",
   109  			Help: `When set forces the usage of the ssh-agent.
   110  
   111  When key-file is also set, the ".pub" file of the specified key-file is read and only the associated key is
   112  requested from the ssh-agent. This allows to avoid ` + "`Too many authentication failures for *username*`" + ` errors
   113  when the ssh-agent contains many keys.`,
   114  			Default: false,
   115  		}, {
   116  			Name: "use_insecure_cipher",
   117  			Help: `Enable the use of insecure ciphers and key exchange methods.
   118  
   119  This enables the use of the following insecure ciphers and key exchange methods:
   120  
   121  - aes128-cbc
   122  - aes192-cbc
   123  - aes256-cbc
   124  - 3des-cbc
   125  - diffie-hellman-group-exchange-sha256
   126  - diffie-hellman-group-exchange-sha1
   127  
   128  Those algorithms are insecure and may allow plaintext data to be recovered by an attacker.
   129  
   130  This must be false if you use either ciphers or key_exchange advanced options.
   131  `,
   132  			Default: false,
   133  			Examples: []fs.OptionExample{
   134  				{
   135  					Value: "false",
   136  					Help:  "Use default Cipher list.",
   137  				}, {
   138  					Value: "true",
   139  					Help:  "Enables the use of the aes128-cbc cipher and diffie-hellman-group-exchange-sha256, diffie-hellman-group-exchange-sha1 key exchange.",
   140  				},
   141  			},
   142  		}, {
   143  			Name:    "disable_hashcheck",
   144  			Default: false,
   145  			Help:    "Disable the execution of SSH commands to determine if remote file hashing is available.\n\nLeave blank or set to false to enable hashing (recommended), set to true to disable hashing.",
   146  		}, {
   147  			Name:    "ask_password",
   148  			Default: false,
   149  			Help: `Allow asking for SFTP password when needed.
   150  
   151  If this is set and no password is supplied then rclone will:
   152  - ask for a password
   153  - not contact the ssh agent
   154  `,
   155  			Advanced: true,
   156  		}, {
   157  			Name:    "path_override",
   158  			Default: "",
   159  			Help: `Override path used by SSH shell commands.
   160  
   161  This allows checksum calculation when SFTP and SSH paths are
   162  different. This issue affects among others Synology NAS boxes.
   163  
   164  E.g. if shared folders can be found in directories representing volumes:
   165  
   166      rclone sync /home/local/directory remote:/directory --sftp-path-override /volume2/directory
   167  
   168  E.g. if home directory can be found in a shared folder called "home":
   169  
   170      rclone sync /home/local/directory remote:/home/directory --sftp-path-override /volume1/homes/USER/directory
   171  	
   172  To specify only the path to the SFTP remote's root, and allow rclone to add any relative subpaths automatically (including unwrapping/decrypting remotes as necessary), add the '@' character to the beginning of the path.
   173  
   174  E.g. the first example above could be rewritten as:
   175  
   176  	rclone sync /home/local/directory remote:/directory --sftp-path-override @/volume2
   177  	
   178  Note that when using this method with Synology "home" folders, the full "/homes/USER" path should be specified instead of "/home".
   179  
   180  E.g. the second example above should be rewritten as:
   181  
   182  	rclone sync /home/local/directory remote:/homes/USER/directory --sftp-path-override @/volume1`,
   183  			Advanced: true,
   184  		}, {
   185  			Name:     "set_modtime",
   186  			Default:  true,
   187  			Help:     "Set the modified time on the remote if set.",
   188  			Advanced: true,
   189  		}, {
   190  			Name:     "shell_type",
   191  			Default:  "",
   192  			Help:     "The type of SSH shell on remote server, if any.\n\nLeave blank for autodetect.",
   193  			Advanced: true,
   194  			Examples: []fs.OptionExample{
   195  				{
   196  					Value: shellTypeNotSupported,
   197  					Help:  "No shell access",
   198  				}, {
   199  					Value: "unix",
   200  					Help:  "Unix shell",
   201  				}, {
   202  					Value: "powershell",
   203  					Help:  "PowerShell",
   204  				}, {
   205  					Value: "cmd",
   206  					Help:  "Windows Command Prompt",
   207  				},
   208  			},
   209  		}, {
   210  			Name:     "md5sum_command",
   211  			Default:  "",
   212  			Help:     "The command used to read md5 hashes.\n\nLeave blank for autodetect.",
   213  			Advanced: true,
   214  		}, {
   215  			Name:     "sha1sum_command",
   216  			Default:  "",
   217  			Help:     "The command used to read sha1 hashes.\n\nLeave blank for autodetect.",
   218  			Advanced: true,
   219  		}, {
   220  			Name:     "skip_links",
   221  			Default:  false,
   222  			Help:     "Set to skip any symlinks and any other non regular files.",
   223  			Advanced: true,
   224  		}, {
   225  			Name:     "subsystem",
   226  			Default:  "sftp",
   227  			Help:     "Specifies the SSH2 subsystem on the remote host.",
   228  			Advanced: true,
   229  		}, {
   230  			Name:    "server_command",
   231  			Default: "",
   232  			Help: `Specifies the path or command to run a sftp server on the remote host.
   233  
   234  The subsystem option is ignored when server_command is defined.
   235  
   236  If adding server_command to the configuration file please note that 
   237  it should not be enclosed in quotes, since that will make rclone fail.
   238  
   239  A working example is:
   240  
   241      [remote_name]
   242      type = sftp
   243      server_command = sudo /usr/libexec/openssh/sftp-server`,
   244  			Advanced: true,
   245  		}, {
   246  			Name:    "use_fstat",
   247  			Default: false,
   248  			Help: `If set use fstat instead of stat.
   249  
   250  Some servers limit the amount of open files and calling Stat after opening
   251  the file will throw an error from the server. Setting this flag will call
   252  Fstat instead of Stat which is called on an already open file handle.
   253  
   254  It has been found that this helps with IBM Sterling SFTP servers which have
   255  "extractability" level set to 1 which means only 1 file can be opened at
   256  any given time.
   257  `,
   258  			Advanced: true,
   259  		}, {
   260  			Name:    "disable_concurrent_reads",
   261  			Default: false,
   262  			Help: `If set don't use concurrent reads.
   263  
   264  Normally concurrent reads are safe to use and not using them will
   265  degrade performance, so this option is disabled by default.
   266  
   267  Some servers limit the amount number of times a file can be
   268  downloaded. Using concurrent reads can trigger this limit, so if you
   269  have a server which returns
   270  
   271      Failed to copy: file does not exist
   272  
   273  Then you may need to enable this flag.
   274  
   275  If concurrent reads are disabled, the use_fstat option is ignored.
   276  `,
   277  			Advanced: true,
   278  		}, {
   279  			Name:    "disable_concurrent_writes",
   280  			Default: false,
   281  			Help: `If set don't use concurrent writes.
   282  
   283  Normally rclone uses concurrent writes to upload files. This improves
   284  the performance greatly, especially for distant servers.
   285  
   286  This option disables concurrent writes should that be necessary.
   287  `,
   288  			Advanced: true,
   289  		}, {
   290  			Name:    "idle_timeout",
   291  			Default: fs.Duration(60 * time.Second),
   292  			Help: `Max time before closing idle connections.
   293  
   294  If no connections have been returned to the connection pool in the time
   295  given, rclone will empty the connection pool.
   296  
   297  Set to 0 to keep connections indefinitely.
   298  `,
   299  			Advanced: true,
   300  		}, {
   301  			Name: "chunk_size",
   302  			Help: `Upload and download chunk size.
   303  
   304  This controls the maximum size of payload in SFTP protocol packets.
   305  The RFC limits this to 32768 bytes (32k), which is the default. However,
   306  a lot of servers support larger sizes, typically limited to a maximum
   307  total package size of 256k, and setting it larger will increase transfer
   308  speed dramatically on high latency links. This includes OpenSSH, and,
   309  for example, using the value of 255k works well, leaving plenty of room
   310  for overhead while still being within a total packet size of 256k.
   311  
   312  Make sure to test thoroughly before using a value higher than 32k,
   313  and only use it if you always connect to the same server or after
   314  sufficiently broad testing. If you get errors such as
   315  "failed to send packet payload: EOF", lots of "connection lost",
   316  or "corrupted on transfer", when copying a larger file, try lowering
   317  the value. The server run by [rclone serve sftp](/commands/rclone_serve_sftp)
   318  sends packets with standard 32k maximum payload so you must not
   319  set a different chunk_size when downloading files, but it accepts
   320  packets up to the 256k total size, so for uploads the chunk_size
   321  can be set as for the OpenSSH example above.
   322  `,
   323  			Default:  32 * fs.Kibi,
   324  			Advanced: true,
   325  		}, {
   326  			Name: "concurrency",
   327  			Help: `The maximum number of outstanding requests for one file
   328  
   329  This controls the maximum number of outstanding requests for one file.
   330  Increasing it will increase throughput on high latency links at the
   331  cost of using more memory.
   332  `,
   333  			Default:  64,
   334  			Advanced: true,
   335  		}, {
   336  			Name:    "set_env",
   337  			Default: fs.SpaceSepList{},
   338  			Help: `Environment variables to pass to sftp and commands
   339  
   340  Set environment variables in the form:
   341  
   342      VAR=value
   343  
   344  to be passed to the sftp client and to any commands run (eg md5sum).
   345  
   346  Pass multiple variables space separated, eg
   347  
   348      VAR1=value VAR2=value
   349  
   350  and pass variables with spaces in quotes, eg
   351  
   352      "VAR3=value with space" "VAR4=value with space" VAR5=nospacehere
   353  
   354  `,
   355  			Advanced: true,
   356  		}, {
   357  			Name:    "ciphers",
   358  			Default: fs.SpaceSepList{},
   359  			Help: `Space separated list of ciphers to be used for session encryption, ordered by preference.
   360  
   361  At least one must match with server configuration. This can be checked for example using ssh -Q cipher.
   362  
   363  This must not be set if use_insecure_cipher is true.
   364  
   365  Example:
   366  
   367      aes128-ctr aes192-ctr aes256-ctr aes128-gcm@openssh.com aes256-gcm@openssh.com
   368  `,
   369  			Advanced: true,
   370  		}, {
   371  			Name:    "key_exchange",
   372  			Default: fs.SpaceSepList{},
   373  			Help: `Space separated list of key exchange algorithms, ordered by preference.
   374  
   375  At least one must match with server configuration. This can be checked for example using ssh -Q kex.
   376  
   377  This must not be set if use_insecure_cipher is true.
   378  
   379  Example:
   380  
   381      sntrup761x25519-sha512@openssh.com curve25519-sha256 curve25519-sha256@libssh.org ecdh-sha2-nistp256
   382  `,
   383  			Advanced: true,
   384  		}, {
   385  			Name:    "macs",
   386  			Default: fs.SpaceSepList{},
   387  			Help: `Space separated list of MACs (message authentication code) algorithms, ordered by preference.
   388  
   389  At least one must match with server configuration. This can be checked for example using ssh -Q mac.
   390  
   391  Example:
   392  
   393      umac-64-etm@openssh.com umac-128-etm@openssh.com hmac-sha2-256-etm@openssh.com
   394  `,
   395  			Advanced: true,
   396  		}, {
   397  			Name:    "host_key_algorithms",
   398  			Default: fs.SpaceSepList{},
   399  			Help: `Space separated list of host key algorithms, ordered by preference.
   400  
   401  At least one must match with server configuration. This can be checked for example using ssh -Q HostKeyAlgorithms.
   402  
   403  Note: This can affect the outcome of key negotiation with the server even if server host key validation is not enabled.
   404  
   405  Example:
   406  
   407      ssh-ed25519 ssh-rsa ssh-dss
   408  `,
   409  			Advanced: true,
   410  		}, {
   411  			Name:    "ssh",
   412  			Default: fs.SpaceSepList{},
   413  			Help: `Path and arguments to external ssh binary.
   414  
   415  Normally rclone will use its internal ssh library to connect to the
   416  SFTP server. However it does not implement all possible ssh options so
   417  it may be desirable to use an external ssh binary.
   418  
   419  Rclone ignores all the internal config if you use this option and
   420  expects you to configure the ssh binary with the user/host/port and
   421  any other options you need.
   422  
   423  **Important** The ssh command must log in without asking for a
   424  password so needs to be configured with keys or certificates.
   425  
   426  Rclone will run the command supplied either with the additional
   427  arguments "-s sftp" to access the SFTP subsystem or with commands such
   428  as "md5sum /path/to/file" appended to read checksums.
   429  
   430  Any arguments with spaces in should be surrounded by "double quotes".
   431  
   432  An example setting might be:
   433  
   434      ssh -o ServerAliveInterval=20 user@example.com
   435  
   436  Note that when using an external ssh binary rclone makes a new ssh
   437  connection for every hash it calculates.
   438  `,
   439  		}, {
   440  			Name:    "socks_proxy",
   441  			Default: "",
   442  			Help: `Socks 5 proxy host.
   443  	
   444  Supports the format user:pass@host:port, user@host:port, host:port.
   445  
   446  Example:
   447  
   448  	myUser:myPass@localhost:9005
   449  	`,
   450  			Advanced: true,
   451  		}, {
   452  			Name:    "copy_is_hardlink",
   453  			Default: false,
   454  			Help: `Set to enable server side copies using hardlinks.
   455  
   456  The SFTP protocol does not define a copy command so normally server
   457  side copies are not allowed with the sftp backend.
   458  
   459  However the SFTP protocol does support hardlinking, and if you enable
   460  this flag then the sftp backend will support server side copies. These
   461  will be implemented by doing a hardlink from the source to the
   462  destination.
   463  
   464  Not all sftp servers support this.
   465  
   466  Note that hardlinking two files together will use no additional space
   467  as the source and the destination will be the same file.
   468  
   469  This feature may be useful backups made with --copy-dest.`,
   470  			Advanced: true,
   471  		}},
   472  	}
   473  	fs.Register(fsi)
   474  }
   475  
   476  // Options defines the configuration for this backend
   477  type Options struct {
   478  	Host                    string          `config:"host"`
   479  	User                    string          `config:"user"`
   480  	Port                    string          `config:"port"`
   481  	Pass                    string          `config:"pass"`
   482  	KeyPem                  string          `config:"key_pem"`
   483  	KeyFile                 string          `config:"key_file"`
   484  	KeyFilePass             string          `config:"key_file_pass"`
   485  	PubKeyFile              string          `config:"pubkey_file"`
   486  	KnownHostsFile          string          `config:"known_hosts_file"`
   487  	KeyUseAgent             bool            `config:"key_use_agent"`
   488  	UseInsecureCipher       bool            `config:"use_insecure_cipher"`
   489  	DisableHashCheck        bool            `config:"disable_hashcheck"`
   490  	AskPassword             bool            `config:"ask_password"`
   491  	PathOverride            string          `config:"path_override"`
   492  	SetModTime              bool            `config:"set_modtime"`
   493  	ShellType               string          `config:"shell_type"`
   494  	Md5sumCommand           string          `config:"md5sum_command"`
   495  	Sha1sumCommand          string          `config:"sha1sum_command"`
   496  	SkipLinks               bool            `config:"skip_links"`
   497  	Subsystem               string          `config:"subsystem"`
   498  	ServerCommand           string          `config:"server_command"`
   499  	UseFstat                bool            `config:"use_fstat"`
   500  	DisableConcurrentReads  bool            `config:"disable_concurrent_reads"`
   501  	DisableConcurrentWrites bool            `config:"disable_concurrent_writes"`
   502  	IdleTimeout             fs.Duration     `config:"idle_timeout"`
   503  	ChunkSize               fs.SizeSuffix   `config:"chunk_size"`
   504  	Concurrency             int             `config:"concurrency"`
   505  	SetEnv                  fs.SpaceSepList `config:"set_env"`
   506  	Ciphers                 fs.SpaceSepList `config:"ciphers"`
   507  	KeyExchange             fs.SpaceSepList `config:"key_exchange"`
   508  	MACs                    fs.SpaceSepList `config:"macs"`
   509  	HostKeyAlgorithms       fs.SpaceSepList `config:"host_key_algorithms"`
   510  	SSH                     fs.SpaceSepList `config:"ssh"`
   511  	SocksProxy              string          `config:"socks_proxy"`
   512  	CopyIsHardlink          bool            `config:"copy_is_hardlink"`
   513  }
   514  
   515  // Fs stores the interface to the remote SFTP files
   516  type Fs struct {
   517  	name         string
   518  	root         string
   519  	absRoot      string
   520  	shellRoot    string
   521  	shellType    string
   522  	opt          Options          // parsed options
   523  	ci           *fs.ConfigInfo   // global config
   524  	m            configmap.Mapper // config
   525  	features     *fs.Features     // optional features
   526  	config       *ssh.ClientConfig
   527  	url          string
   528  	mkdirLock    *stringLock
   529  	cachedHashes *hash.Set
   530  	poolMu       sync.Mutex
   531  	pool         []*conn
   532  	drain        *time.Timer // used to drain the pool when we stop using the connections
   533  	pacer        *fs.Pacer   // pacer for operations
   534  	savedpswd    string
   535  	sessions     atomic.Int32 // count in use sessions
   536  }
   537  
   538  // Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading)
   539  type Object struct {
   540  	fs      *Fs
   541  	remote  string
   542  	size    int64       // size of the object
   543  	modTime time.Time   // modification time of the object
   544  	mode    os.FileMode // mode bits from the file
   545  	md5sum  *string     // Cached MD5 checksum
   546  	sha1sum *string     // Cached SHA1 checksum
   547  }
   548  
   549  // conn encapsulates an ssh client and corresponding sftp client
   550  type conn struct {
   551  	sshClient  sshClient
   552  	sftpClient *sftp.Client
   553  	err        chan error
   554  }
   555  
   556  // Wait for connection to close
   557  func (c *conn) wait() {
   558  	c.err <- c.sshClient.Wait()
   559  }
   560  
   561  // Send keepalives every interval over the ssh connection until done is closed
   562  func (c *conn) sendKeepAlives(interval time.Duration) (done chan struct{}) {
   563  	done = make(chan struct{})
   564  	go func() {
   565  		t := time.NewTicker(interval)
   566  		defer t.Stop()
   567  		for {
   568  			select {
   569  			case <-t.C:
   570  				c.sshClient.SendKeepAlive()
   571  			case <-done:
   572  				return
   573  			}
   574  		}
   575  	}()
   576  	return done
   577  }
   578  
   579  // Closes the connection
   580  func (c *conn) close() error {
   581  	sftpErr := c.sftpClient.Close()
   582  	sshErr := c.sshClient.Close()
   583  	if sftpErr != nil {
   584  		return sftpErr
   585  	}
   586  	return sshErr
   587  }
   588  
   589  // Returns an error if closed
   590  func (c *conn) closed() error {
   591  	select {
   592  	case err := <-c.err:
   593  		return err
   594  	default:
   595  	}
   596  	return nil
   597  }
   598  
   599  // Show that we are using an ssh session
   600  //
   601  // Call removeSession() when done
   602  func (f *Fs) addSession() {
   603  	f.sessions.Add(1)
   604  }
   605  
   606  // Show the ssh session is no longer in use
   607  func (f *Fs) removeSession() {
   608  	f.sessions.Add(-1)
   609  }
   610  
   611  // getSessions shows whether there are any sessions in use
   612  func (f *Fs) getSessions() int32 {
   613  	return f.sessions.Load()
   614  }
   615  
   616  // Open a new connection to the SFTP server.
   617  func (f *Fs) sftpConnection(ctx context.Context) (c *conn, err error) {
   618  	// Rate limit rate of new connections
   619  	c = &conn{
   620  		err: make(chan error, 1),
   621  	}
   622  	if len(f.opt.SSH) == 0 {
   623  		c.sshClient, err = f.newSSHClientInternal(ctx, "tcp", f.opt.Host+":"+f.opt.Port, f.config)
   624  	} else {
   625  		c.sshClient, err = f.newSSHClientExternal()
   626  	}
   627  	if err != nil {
   628  		return nil, fmt.Errorf("couldn't connect SSH: %w", err)
   629  	}
   630  	c.sftpClient, err = f.newSftpClient(c.sshClient)
   631  	if err != nil {
   632  		_ = c.sshClient.Close()
   633  		return nil, fmt.Errorf("couldn't initialise SFTP: %w", err)
   634  	}
   635  	go c.wait()
   636  	return c, nil
   637  }
   638  
   639  // Set any environment variables on the ssh.Session
   640  func (f *Fs) setEnv(s sshSession) error {
   641  	for _, env := range f.opt.SetEnv {
   642  		equal := strings.IndexRune(env, '=')
   643  		if equal < 0 {
   644  			return fmt.Errorf("no = found in env var %q", env)
   645  		}
   646  		// fs.Debugf(f, "Setting env %q = %q", env[:equal], env[equal+1:])
   647  		err := s.Setenv(env[:equal], env[equal+1:])
   648  		if err != nil {
   649  			return fmt.Errorf("failed to set env var %q: %w", env[:equal], err)
   650  		}
   651  	}
   652  	return nil
   653  }
   654  
   655  // Creates a new SFTP client on conn, using the specified subsystem
   656  // or sftp server, and zero or more option functions
   657  func (f *Fs) newSftpClient(client sshClient, opts ...sftp.ClientOption) (*sftp.Client, error) {
   658  	s, err := client.NewSession()
   659  	if err != nil {
   660  		return nil, err
   661  	}
   662  	err = f.setEnv(s)
   663  	if err != nil {
   664  		return nil, err
   665  	}
   666  	pw, err := s.StdinPipe()
   667  	if err != nil {
   668  		return nil, err
   669  	}
   670  	pr, err := s.StdoutPipe()
   671  	if err != nil {
   672  		return nil, err
   673  	}
   674  
   675  	if f.opt.ServerCommand != "" {
   676  		if err := s.Start(f.opt.ServerCommand); err != nil {
   677  			return nil, err
   678  		}
   679  	} else {
   680  		if err := s.RequestSubsystem(f.opt.Subsystem); err != nil {
   681  			return nil, err
   682  		}
   683  	}
   684  	opts = opts[:len(opts):len(opts)] // make sure we don't overwrite the callers opts
   685  	opts = append(opts,
   686  		sftp.UseFstat(f.opt.UseFstat),
   687  		sftp.UseConcurrentReads(!f.opt.DisableConcurrentReads),
   688  		sftp.UseConcurrentWrites(!f.opt.DisableConcurrentWrites),
   689  		sftp.MaxPacketUnchecked(int(f.opt.ChunkSize)),
   690  		sftp.MaxConcurrentRequestsPerFile(f.opt.Concurrency),
   691  	)
   692  	return sftp.NewClientPipe(pr, pw, opts...)
   693  }
   694  
   695  // Get an SFTP connection from the pool, or open a new one
   696  func (f *Fs) getSftpConnection(ctx context.Context) (c *conn, err error) {
   697  	accounting.LimitTPS(ctx)
   698  	f.poolMu.Lock()
   699  	for len(f.pool) > 0 {
   700  		c = f.pool[0]
   701  		f.pool = f.pool[1:]
   702  		err := c.closed()
   703  		if err == nil {
   704  			break
   705  		}
   706  		fs.Errorf(f, "Discarding closed SSH connection: %v", err)
   707  		c = nil
   708  	}
   709  	f.poolMu.Unlock()
   710  	if c != nil {
   711  		return c, nil
   712  	}
   713  	err = f.pacer.Call(func() (bool, error) {
   714  		c, err = f.sftpConnection(ctx)
   715  		if err != nil {
   716  			return true, err
   717  		}
   718  		return false, nil
   719  	})
   720  	return c, err
   721  }
   722  
   723  // Return an SFTP connection to the pool
   724  //
   725  // It nils the pointed to connection out so it can't be reused
   726  //
   727  // if err is not nil then it checks the connection is alive using a
   728  // Getwd request
   729  func (f *Fs) putSftpConnection(pc **conn, err error) {
   730  	c := *pc
   731  	if !c.sshClient.CanReuse() {
   732  		return
   733  	}
   734  	*pc = nil
   735  	if err != nil {
   736  		// work out if this is an expected error
   737  		isRegularError := false
   738  		var statusErr *sftp.StatusError
   739  		var pathErr *os.PathError
   740  		switch {
   741  		case errors.Is(err, os.ErrNotExist):
   742  			isRegularError = true
   743  		case errors.As(err, &statusErr):
   744  			isRegularError = true
   745  		case errors.As(err, &pathErr):
   746  			isRegularError = true
   747  		}
   748  		// If not a regular SFTP error code then check the connection
   749  		if !isRegularError {
   750  			_, nopErr := c.sftpClient.Getwd()
   751  			if nopErr != nil {
   752  				fs.Debugf(f, "Connection failed, closing: %v", nopErr)
   753  				_ = c.close()
   754  				return
   755  			}
   756  			fs.Debugf(f, "Connection OK after error: %v", err)
   757  		}
   758  	}
   759  	f.poolMu.Lock()
   760  	f.pool = append(f.pool, c)
   761  	if f.opt.IdleTimeout > 0 {
   762  		f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
   763  	}
   764  	f.poolMu.Unlock()
   765  }
   766  
   767  // Drain the pool of any connections
   768  func (f *Fs) drainPool(ctx context.Context) (err error) {
   769  	f.poolMu.Lock()
   770  	defer f.poolMu.Unlock()
   771  	if sessions := f.getSessions(); sessions != 0 {
   772  		fs.Debugf(f, "Not closing %d unused connections as %d sessions active", len(f.pool), sessions)
   773  		if f.opt.IdleTimeout > 0 {
   774  			f.drain.Reset(time.Duration(f.opt.IdleTimeout)) // nudge on the pool emptying timer
   775  		}
   776  		return nil
   777  	}
   778  	if f.opt.IdleTimeout > 0 {
   779  		f.drain.Stop()
   780  	}
   781  	if len(f.pool) != 0 {
   782  		fs.Debugf(f, "Closing %d unused connections", len(f.pool))
   783  	}
   784  	for i, c := range f.pool {
   785  		if cErr := c.closed(); cErr == nil {
   786  			cErr = c.close()
   787  			if cErr != nil {
   788  				err = cErr
   789  			}
   790  		}
   791  		f.pool[i] = nil
   792  	}
   793  	f.pool = nil
   794  	return err
   795  }
   796  
   797  // NewFs creates a new Fs object from the name and root. It connects to
   798  // the host specified in the config file.
   799  func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, error) {
   800  	// This will hold the Fs object.  We need to create it here
   801  	// so we can refer to it in the SSH callback, but it's populated
   802  	// in NewFsWithConnection
   803  	f := &Fs{
   804  		ci: fs.GetConfig(ctx),
   805  	}
   806  	// Parse config into Options struct
   807  	opt := new(Options)
   808  	err := configstruct.Set(m, opt)
   809  	if err != nil {
   810  		return nil, err
   811  	}
   812  	if len(opt.SSH) != 0 && ((opt.User != currentUser && opt.User != "") || opt.Host != "" || (opt.Port != "22" && opt.Port != "")) {
   813  		fs.Logf(name, "--sftp-ssh is in use - ignoring user/host/port from config - set in the parameters to --sftp-ssh (remove them from the config to silence this warning)")
   814  	}
   815  
   816  	if opt.User == "" {
   817  		opt.User = currentUser
   818  	}
   819  	if opt.Port == "" {
   820  		opt.Port = "22"
   821  	}
   822  
   823  	sshConfig := &ssh.ClientConfig{
   824  		User:            opt.User,
   825  		Auth:            []ssh.AuthMethod{},
   826  		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
   827  		Timeout:         f.ci.ConnectTimeout,
   828  		ClientVersion:   "SSH-2.0-" + f.ci.UserAgent,
   829  	}
   830  
   831  	if len(opt.HostKeyAlgorithms) != 0 {
   832  		sshConfig.HostKeyAlgorithms = []string(opt.HostKeyAlgorithms)
   833  	}
   834  
   835  	if opt.KnownHostsFile != "" {
   836  		hostcallback, err := knownhosts.New(env.ShellExpand(opt.KnownHostsFile))
   837  		if err != nil {
   838  			return nil, fmt.Errorf("couldn't parse known_hosts_file: %w", err)
   839  		}
   840  		sshConfig.HostKeyCallback = hostcallback
   841  	}
   842  
   843  	if opt.UseInsecureCipher && (opt.Ciphers != nil || opt.KeyExchange != nil) {
   844  		return nil, fmt.Errorf("use_insecure_cipher must be false if ciphers or key_exchange are set in advanced configuration")
   845  	}
   846  
   847  	sshConfig.Config.SetDefaults()
   848  	if opt.UseInsecureCipher {
   849  		sshConfig.Config.Ciphers = append(sshConfig.Config.Ciphers, "aes128-cbc", "aes192-cbc", "aes256-cbc", "3des-cbc")
   850  		sshConfig.Config.KeyExchanges = append(sshConfig.Config.KeyExchanges, "diffie-hellman-group-exchange-sha1", "diffie-hellman-group-exchange-sha256")
   851  	} else {
   852  		if opt.Ciphers != nil {
   853  			sshConfig.Config.Ciphers = opt.Ciphers
   854  		}
   855  		if opt.KeyExchange != nil {
   856  			sshConfig.Config.KeyExchanges = opt.KeyExchange
   857  		}
   858  	}
   859  
   860  	if opt.MACs != nil {
   861  		sshConfig.Config.MACs = opt.MACs
   862  	}
   863  
   864  	keyFile := env.ShellExpand(opt.KeyFile)
   865  	pubkeyFile := env.ShellExpand(opt.PubKeyFile)
   866  	//keyPem := env.ShellExpand(opt.KeyPem)
   867  	// Add ssh agent-auth if no password or file or key PEM specified
   868  	if (len(opt.SSH) == 0 && opt.Pass == "" && keyFile == "" && !opt.AskPassword && opt.KeyPem == "") || opt.KeyUseAgent {
   869  		sshAgentClient, _, err := sshagent.New()
   870  		if err != nil {
   871  			return nil, fmt.Errorf("couldn't connect to ssh-agent: %w", err)
   872  		}
   873  		signers, err := sshAgentClient.Signers()
   874  		if err != nil {
   875  			return nil, fmt.Errorf("couldn't read ssh agent signers: %w", err)
   876  		}
   877  		if keyFile != "" {
   878  			// If `opt.KeyUseAgent` is false, then it's expected that `opt.KeyFile` contains the private key
   879  			// and `${opt.KeyFile}.pub` contains the public key.
   880  			//
   881  			// If `opt.KeyUseAgent` is true, then it's expected that `opt.KeyFile` contains the public key.
   882  			// This is how it works with openssh; the `IdentityFile` in openssh config points to the public key.
   883  			// It's not necessary to specify the public key explicitly when using ssh-agent, since openssh and rclone
   884  			// will try all the keys they find in the ssh-agent until they find one that works. But just like
   885  			// `IdentityFile` is used in openssh config to limit the search to one specific key, so does
   886  			// `opt.KeyFile` in rclone config limit the search to that specific key.
   887  			//
   888  			// However, previous versions of rclone would always expect to find the public key in
   889  			// `${opt.KeyFile}.pub` even if `opt.KeyUseAgent` was true. So for the sake of backward compatibility
   890  			// we still first attempt to read the public key from `${opt.KeyFile}.pub`. But if it fails with
   891  			// an `fs.ErrNotExist` then we also try to read the public key from `opt.KeyFile`.
   892  			pubBytes, err := os.ReadFile(keyFile + ".pub")
   893  			if err != nil {
   894  				if errors.Is(err, iofs.ErrNotExist) && opt.KeyUseAgent {
   895  					pubBytes, err = os.ReadFile(keyFile)
   896  					if err != nil {
   897  						return nil, fmt.Errorf("failed to read public key file: %w", err)
   898  					}
   899  				} else {
   900  					return nil, fmt.Errorf("failed to read public key file: %w", err)
   901  				}
   902  			}
   903  
   904  			pub, _, _, _, err := ssh.ParseAuthorizedKey(pubBytes)
   905  			if err != nil {
   906  				return nil, fmt.Errorf("failed to parse public key file: %w", err)
   907  			}
   908  			pubM := pub.Marshal()
   909  			found := false
   910  			for _, s := range signers {
   911  				if bytes.Equal(pubM, s.PublicKey().Marshal()) {
   912  					sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(s))
   913  					found = true
   914  					break
   915  				}
   916  			}
   917  			if !found {
   918  				return nil, errors.New("private key not found in the ssh-agent")
   919  			}
   920  		} else {
   921  			sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signers...))
   922  		}
   923  	}
   924  
   925  	// Load key file as a private key, if specified. This is only needed when not using an ssh agent.
   926  	if (keyFile != "" && !opt.KeyUseAgent) || opt.KeyPem != "" {
   927  		var key []byte
   928  		if opt.KeyPem == "" {
   929  			key, err = os.ReadFile(keyFile)
   930  			if err != nil {
   931  				return nil, fmt.Errorf("failed to read private key file: %w", err)
   932  			}
   933  		} else {
   934  			// wrap in quotes because the config is a coming as a literal without them.
   935  			opt.KeyPem, err = strconv.Unquote("\"" + opt.KeyPem + "\"")
   936  			if err != nil {
   937  				return nil, fmt.Errorf("pem key not formatted properly: %w", err)
   938  			}
   939  			key = []byte(opt.KeyPem)
   940  		}
   941  		clearpass := ""
   942  		if opt.KeyFilePass != "" {
   943  			clearpass, err = obscure.Reveal(opt.KeyFilePass)
   944  			if err != nil {
   945  				return nil, err
   946  			}
   947  		}
   948  		var signer ssh.Signer
   949  		if clearpass == "" {
   950  			signer, err = ssh.ParsePrivateKey(key)
   951  		} else {
   952  			signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(clearpass))
   953  		}
   954  		if err != nil {
   955  			return nil, fmt.Errorf("failed to parse private key file: %w", err)
   956  		}
   957  
   958  		// If a public key has been specified then use that
   959  		if pubkeyFile != "" {
   960  			certfile, err := os.ReadFile(pubkeyFile)
   961  			if err != nil {
   962  				return nil, fmt.Errorf("unable to read cert file: %w", err)
   963  			}
   964  
   965  			pk, _, _, _, err := ssh.ParseAuthorizedKey(certfile)
   966  			if err != nil {
   967  				return nil, fmt.Errorf("unable to parse cert file: %w", err)
   968  			}
   969  
   970  			// And the signer for this, which includes the private key signer
   971  			// This is what we'll pass to the ssh client.
   972  			// Normally the ssh client will use the public key built
   973  			// into the private key, but we need to tell it to use the user
   974  			// specified public key cert.  This signer is specific to the
   975  			// cert and will include the private key signer.  Now ssh
   976  			// knows everything it needs.
   977  			cert, ok := pk.(*ssh.Certificate)
   978  			if !ok {
   979  				return nil, errors.New("public key file is not a certificate file: " + pubkeyFile)
   980  			}
   981  			pubsigner, err := ssh.NewCertSigner(cert, signer)
   982  			if err != nil {
   983  				return nil, fmt.Errorf("error generating cert signer: %w", err)
   984  			}
   985  			sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(pubsigner))
   986  		} else {
   987  			sshConfig.Auth = append(sshConfig.Auth, ssh.PublicKeys(signer))
   988  		}
   989  	}
   990  
   991  	// Auth from password if specified
   992  	if opt.Pass != "" {
   993  		clearpass, err := obscure.Reveal(opt.Pass)
   994  		if err != nil {
   995  			return nil, err
   996  		}
   997  		sshConfig.Auth = append(sshConfig.Auth,
   998  			ssh.Password(clearpass),
   999  			ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
  1000  				return f.keyboardInteractiveReponse(user, instruction, questions, echos, clearpass)
  1001  			}),
  1002  		)
  1003  	}
  1004  
  1005  	// Config for password if none was defined and we're allowed to
  1006  	// We don't ask now; we ask if the ssh connection succeeds
  1007  	if opt.Pass == "" && opt.AskPassword {
  1008  		sshConfig.Auth = append(sshConfig.Auth,
  1009  			ssh.PasswordCallback(f.getPass),
  1010  			ssh.KeyboardInteractive(func(user, instruction string, questions []string, echos []bool) ([]string, error) {
  1011  				pass, _ := f.getPass()
  1012  				return f.keyboardInteractiveReponse(user, instruction, questions, echos, pass)
  1013  			}),
  1014  		)
  1015  	}
  1016  
  1017  	return NewFsWithConnection(ctx, f, name, root, m, opt, sshConfig)
  1018  }
  1019  
  1020  // Do the keyboard interactive challenge
  1021  //
  1022  // Just send the password back for all questions
  1023  func (f *Fs) keyboardInteractiveReponse(user, instruction string, questions []string, echos []bool, pass string) ([]string, error) {
  1024  	fs.Debugf(f, "Keyboard interactive auth requested")
  1025  	answers := make([]string, len(questions))
  1026  	for i := range answers {
  1027  		answers[i] = pass
  1028  	}
  1029  	return answers, nil
  1030  }
  1031  
  1032  // If we're in password mode and ssh connection succeeds then this
  1033  // callback is called.  First time around we ask the user, and then
  1034  // save it so on reconnection we give back the previous string.
  1035  // This removes the ability to let the user correct a mistaken entry,
  1036  // but means that reconnects are transparent.
  1037  // We'll reuse config.Pass for this, 'cos we know it's not been
  1038  // specified.
  1039  func (f *Fs) getPass() (string, error) {
  1040  	for f.savedpswd == "" {
  1041  		_, _ = fmt.Fprint(os.Stderr, "Enter SFTP password: ")
  1042  		f.savedpswd = config.ReadPassword()
  1043  	}
  1044  	return f.savedpswd, nil
  1045  }
  1046  
  1047  // NewFsWithConnection creates a new Fs object from the name and root and an ssh.ClientConfig. It connects to
  1048  // the host specified in the ssh.ClientConfig
  1049  func NewFsWithConnection(ctx context.Context, f *Fs, name string, root string, m configmap.Mapper, opt *Options, sshConfig *ssh.ClientConfig) (fs.Fs, error) {
  1050  	// Populate the Filesystem Object
  1051  	f.name = name
  1052  	f.root = root
  1053  	f.absRoot = root
  1054  	f.shellRoot = root
  1055  	f.opt = *opt
  1056  	f.m = m
  1057  	f.config = sshConfig
  1058  	f.url = "sftp://" + opt.User + "@" + opt.Host + ":" + opt.Port + "/" + root
  1059  	f.mkdirLock = newStringLock()
  1060  	f.pacer = fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant)))
  1061  	f.savedpswd = ""
  1062  	// set the pool drainer timer going
  1063  	if f.opt.IdleTimeout > 0 {
  1064  		f.drain = time.AfterFunc(time.Duration(f.opt.IdleTimeout), func() { _ = f.drainPool(ctx) })
  1065  	}
  1066  
  1067  	f.features = (&fs.Features{
  1068  		CanHaveEmptyDirectories:  true,
  1069  		SlowHash:                 true,
  1070  		PartialUploads:           true,
  1071  		DirModTimeUpdatesOnWrite: true, // indicate writing files to a directory updates its modtime
  1072  	}).Fill(ctx, f)
  1073  	if !opt.CopyIsHardlink {
  1074  		// Disable server side copy unless --sftp-copy-is-hardlink is set
  1075  		f.features.Copy = nil
  1076  	}
  1077  	// Make a connection and pool it to return errors early
  1078  	c, err := f.getSftpConnection(ctx)
  1079  	if err != nil {
  1080  		return nil, fmt.Errorf("NewFs: %w", err)
  1081  	}
  1082  	// Check remote shell type, try to auto-detect if not configured and save to config for later
  1083  	if f.opt.ShellType != "" {
  1084  		f.shellType = f.opt.ShellType
  1085  		fs.Debugf(f, "Shell type %q from config", f.shellType)
  1086  	} else {
  1087  		session, err := c.sshClient.NewSession()
  1088  		if err != nil {
  1089  			f.shellType = shellTypeNotSupported
  1090  			fs.Debugf(f, "Failed to get shell session for shell type detection command: %v", err)
  1091  		} else {
  1092  			var stdout, stderr bytes.Buffer
  1093  			session.SetStdout(&stdout)
  1094  			session.SetStderr(&stderr)
  1095  			shellCmd := "echo ${ShellId}%ComSpec%"
  1096  			fs.Debugf(f, "Running shell type detection remote command: %s", shellCmd)
  1097  			err = session.Run(shellCmd)
  1098  			_ = session.Close()
  1099  			f.shellType = defaultShellType
  1100  			if err != nil {
  1101  				fs.Debugf(f, "Remote command failed: %v (stdout=%v) (stderr=%v)", err, bytes.TrimSpace(stdout.Bytes()), bytes.TrimSpace(stderr.Bytes()))
  1102  			} else {
  1103  				outBytes := stdout.Bytes()
  1104  				fs.Debugf(f, "Remote command result: %s", outBytes)
  1105  				outString := string(bytes.TrimSpace(stdout.Bytes()))
  1106  				if outString != "" {
  1107  					if strings.HasPrefix(outString, "Microsoft.PowerShell") { // PowerShell: "Microsoft.PowerShell%ComSpec%"
  1108  						f.shellType = "powershell"
  1109  					} else if !strings.HasSuffix(outString, "%ComSpec%") { // Command Prompt: "${ShellId}C:\WINDOWS\system32\cmd.exe"
  1110  						// Additional positive test, to avoid misdetection on unpredicted Unix shell variants
  1111  						s := strings.ToLower(outString)
  1112  						if strings.Contains(s, ".exe") || strings.Contains(s, ".com") {
  1113  							f.shellType = "cmd"
  1114  						}
  1115  					} // POSIX-based Unix shell: "%ComSpec%"
  1116  				} // fish Unix shell: ""
  1117  			}
  1118  		}
  1119  		// Save permanently in config to avoid the extra work next time
  1120  		fs.Debugf(f, "Shell type %q detected (set option shell_type to override)", f.shellType)
  1121  		f.m.Set("shell_type", f.shellType)
  1122  	}
  1123  	// Ensure we have absolute path to root
  1124  	// It appears that WS FTP doesn't like relative paths,
  1125  	// and the openssh sftp tool also uses absolute paths.
  1126  	if !path.IsAbs(f.root) {
  1127  		// Trying RealPath first, to perform proper server-side canonicalize.
  1128  		// It may fail (SSH_FX_FAILURE reported on WS FTP) and will then resort
  1129  		// to simple path join with current directory from Getwd (which can work
  1130  		// on WS FTP, even though it is also based on RealPath).
  1131  		absRoot, err := c.sftpClient.RealPath(f.root)
  1132  		if err != nil {
  1133  			fs.Debugf(f, "Failed to resolve path using RealPath: %v", err)
  1134  			cwd, err := c.sftpClient.Getwd()
  1135  			if err != nil {
  1136  				fs.Debugf(f, "Failed to to read current directory - using relative paths: %v", err)
  1137  			} else {
  1138  				f.absRoot = path.Join(cwd, f.root)
  1139  				fs.Debugf(f, "Relative path joined with current directory to get absolute path %q", f.absRoot)
  1140  			}
  1141  		} else {
  1142  			f.absRoot = absRoot
  1143  			fs.Debugf(f, "Relative path resolved to %q", f.absRoot)
  1144  		}
  1145  	}
  1146  	f.putSftpConnection(&c, err)
  1147  	if root != "" && !strings.HasSuffix(root, "/") {
  1148  		// Check to see if the root is actually an existing file,
  1149  		// and if so change the filesystem root to its parent directory.
  1150  		oldAbsRoot := f.absRoot
  1151  		remote := path.Base(root)
  1152  		f.root = path.Dir(root)
  1153  		f.absRoot = path.Dir(f.absRoot)
  1154  		if f.root == "." {
  1155  			f.root = ""
  1156  		}
  1157  		_, err = f.NewObject(ctx, remote)
  1158  		if err != nil {
  1159  			if err != fs.ErrorObjectNotFound && err != fs.ErrorIsDir {
  1160  				return nil, err
  1161  			}
  1162  			// File doesn't exist so keep the old f
  1163  			f.root = root
  1164  			f.absRoot = oldAbsRoot
  1165  			err = nil
  1166  		} else {
  1167  			// File exists so change fs to point to the parent and return it with an error
  1168  			err = fs.ErrorIsFile
  1169  		}
  1170  	} else {
  1171  		err = nil
  1172  	}
  1173  	fs.Debugf(f, "Using root directory %q", f.absRoot)
  1174  	return f, err
  1175  }
  1176  
  1177  // Name returns the configured name of the file system
  1178  func (f *Fs) Name() string {
  1179  	return f.name
  1180  }
  1181  
  1182  // Root returns the root for the filesystem
  1183  func (f *Fs) Root() string {
  1184  	return f.root
  1185  }
  1186  
  1187  // String returns the URL for the filesystem
  1188  func (f *Fs) String() string {
  1189  	return f.url
  1190  }
  1191  
  1192  // Features returns the optional features of this Fs
  1193  func (f *Fs) Features() *fs.Features {
  1194  	return f.features
  1195  }
  1196  
  1197  // Precision is the remote sftp file system's modtime precision, which we have no way of knowing. We estimate at 1s
  1198  func (f *Fs) Precision() time.Duration {
  1199  	return time.Second
  1200  }
  1201  
  1202  // NewObject creates a new remote sftp file object
  1203  func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) {
  1204  	o := &Object{
  1205  		fs:     f,
  1206  		remote: remote,
  1207  	}
  1208  	err := o.stat(ctx)
  1209  	if err != nil {
  1210  		return nil, err
  1211  	}
  1212  	return o, nil
  1213  }
  1214  
  1215  // dirExists returns true,nil if the directory exists, false, nil if
  1216  // it doesn't or false, err
  1217  func (f *Fs) dirExists(ctx context.Context, dir string) (bool, error) {
  1218  	if dir == "" {
  1219  		dir = "."
  1220  	}
  1221  	c, err := f.getSftpConnection(ctx)
  1222  	if err != nil {
  1223  		return false, fmt.Errorf("dirExists: %w", err)
  1224  	}
  1225  	info, err := c.sftpClient.Stat(dir)
  1226  	f.putSftpConnection(&c, err)
  1227  	if err != nil {
  1228  		if os.IsNotExist(err) {
  1229  			return false, nil
  1230  		}
  1231  		return false, fmt.Errorf("dirExists stat failed: %w", err)
  1232  	}
  1233  	if !info.IsDir() {
  1234  		return false, fs.ErrorIsFile
  1235  	}
  1236  	return true, nil
  1237  }
  1238  
  1239  // List the objects and directories in dir into entries.  The
  1240  // entries can be returned in any order but should be for a
  1241  // complete directory.
  1242  //
  1243  // dir should be "" to list the root, and should not have
  1244  // trailing slashes.
  1245  //
  1246  // This should return ErrDirNotFound if the directory isn't
  1247  // found.
  1248  func (f *Fs) List(ctx context.Context, dir string) (entries fs.DirEntries, err error) {
  1249  	root := path.Join(f.absRoot, dir)
  1250  	sftpDir := root
  1251  	if sftpDir == "" {
  1252  		sftpDir = "."
  1253  	}
  1254  	c, err := f.getSftpConnection(ctx)
  1255  	if err != nil {
  1256  		return nil, fmt.Errorf("List: %w", err)
  1257  	}
  1258  	infos, err := c.sftpClient.ReadDir(sftpDir)
  1259  	f.putSftpConnection(&c, err)
  1260  	if err != nil {
  1261  		if errors.Is(err, os.ErrNotExist) {
  1262  			return nil, fs.ErrorDirNotFound
  1263  		}
  1264  		return nil, fmt.Errorf("error listing %q: %w", dir, err)
  1265  	}
  1266  	for _, info := range infos {
  1267  		remote := path.Join(dir, info.Name())
  1268  		// If file is a symlink (not a regular file is the best cross platform test we can do), do a stat to
  1269  		// pick up the size and type of the destination, instead of the size and type of the symlink.
  1270  		if !info.Mode().IsRegular() && !info.IsDir() {
  1271  			if f.opt.SkipLinks {
  1272  				// skip non regular file if SkipLinks is set
  1273  				continue
  1274  			}
  1275  			oldInfo := info
  1276  			info, err = f.stat(ctx, remote)
  1277  			if err != nil {
  1278  				if !os.IsNotExist(err) {
  1279  					fs.Errorf(remote, "stat of non-regular file failed: %v", err)
  1280  				}
  1281  				info = oldInfo
  1282  			}
  1283  		}
  1284  		if info.IsDir() {
  1285  			d := fs.NewDir(remote, info.ModTime())
  1286  			entries = append(entries, d)
  1287  		} else {
  1288  			o := &Object{
  1289  				fs:     f,
  1290  				remote: remote,
  1291  			}
  1292  			o.setMetadata(info)
  1293  			entries = append(entries, o)
  1294  		}
  1295  	}
  1296  	return entries, nil
  1297  }
  1298  
  1299  // Put data from <in> into a new remote sftp file object described by <src.Remote()> and <src.ModTime(ctx)>
  1300  func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  1301  	err := f.mkParentDir(ctx, src.Remote())
  1302  	if err != nil {
  1303  		return nil, fmt.Errorf("Put mkParentDir failed: %w", err)
  1304  	}
  1305  	// Temporary object under construction
  1306  	o := &Object{
  1307  		fs:     f,
  1308  		remote: src.Remote(),
  1309  	}
  1310  	err = o.Update(ctx, in, src, options...)
  1311  	if err != nil {
  1312  		return nil, err
  1313  	}
  1314  	return o, nil
  1315  }
  1316  
  1317  // PutStream uploads to the remote path with the modTime given of indeterminate size
  1318  func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) {
  1319  	return f.Put(ctx, in, src, options...)
  1320  }
  1321  
  1322  // mkParentDir makes the parent of remote if necessary and any
  1323  // directories above that
  1324  func (f *Fs) mkParentDir(ctx context.Context, remote string) error {
  1325  	parent := path.Dir(remote)
  1326  	return f.mkdir(ctx, path.Join(f.absRoot, parent))
  1327  }
  1328  
  1329  // mkdir makes the directory and parents using native paths
  1330  func (f *Fs) mkdir(ctx context.Context, dirPath string) error {
  1331  	f.mkdirLock.Lock(dirPath)
  1332  	defer f.mkdirLock.Unlock(dirPath)
  1333  	if dirPath == "." || dirPath == "/" {
  1334  		return nil
  1335  	}
  1336  	ok, err := f.dirExists(ctx, dirPath)
  1337  	if err != nil {
  1338  		return fmt.Errorf("mkdir dirExists failed: %w", err)
  1339  	}
  1340  	if ok {
  1341  		return nil
  1342  	}
  1343  	parent := path.Dir(dirPath)
  1344  	err = f.mkdir(ctx, parent)
  1345  	if err != nil {
  1346  		return err
  1347  	}
  1348  	c, err := f.getSftpConnection(ctx)
  1349  	if err != nil {
  1350  		return fmt.Errorf("mkdir: %w", err)
  1351  	}
  1352  	err = c.sftpClient.Mkdir(dirPath)
  1353  	f.putSftpConnection(&c, err)
  1354  	if err != nil {
  1355  		if os.IsExist(err) {
  1356  			fs.Debugf(f, "directory %q exists after Mkdir is attempted", dirPath)
  1357  			return nil
  1358  		}
  1359  		return fmt.Errorf("mkdir %q failed: %w", dirPath, err)
  1360  	}
  1361  	return nil
  1362  }
  1363  
  1364  // Mkdir makes the root directory of the Fs object
  1365  func (f *Fs) Mkdir(ctx context.Context, dir string) error {
  1366  	root := path.Join(f.absRoot, dir)
  1367  	return f.mkdir(ctx, root)
  1368  }
  1369  
  1370  // DirSetModTime sets the directory modtime for dir
  1371  func (f *Fs) DirSetModTime(ctx context.Context, dir string, modTime time.Time) error {
  1372  	o := Object{
  1373  		fs:     f,
  1374  		remote: dir,
  1375  	}
  1376  	return o.SetModTime(ctx, modTime)
  1377  }
  1378  
  1379  // Rmdir removes the root directory of the Fs object
  1380  func (f *Fs) Rmdir(ctx context.Context, dir string) error {
  1381  	// Check to see if directory is empty as some servers will
  1382  	// delete recursively with RemoveDirectory
  1383  	entries, err := f.List(ctx, dir)
  1384  	if err != nil {
  1385  		return fmt.Errorf("Rmdir: %w", err)
  1386  	}
  1387  	if len(entries) != 0 {
  1388  		return fs.ErrorDirectoryNotEmpty
  1389  	}
  1390  	// Remove the directory
  1391  	root := path.Join(f.absRoot, dir)
  1392  	c, err := f.getSftpConnection(ctx)
  1393  	if err != nil {
  1394  		return fmt.Errorf("Rmdir: %w", err)
  1395  	}
  1396  	err = c.sftpClient.RemoveDirectory(root)
  1397  	f.putSftpConnection(&c, err)
  1398  	return err
  1399  }
  1400  
  1401  // Move renames a remote sftp file object
  1402  func (f *Fs) Move(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  1403  	srcObj, ok := src.(*Object)
  1404  	if !ok {
  1405  		fs.Debugf(src, "Can't move - not same remote type")
  1406  		return nil, fs.ErrorCantMove
  1407  	}
  1408  	err := f.mkParentDir(ctx, remote)
  1409  	if err != nil {
  1410  		return nil, fmt.Errorf("Move mkParentDir failed: %w", err)
  1411  	}
  1412  	c, err := f.getSftpConnection(ctx)
  1413  	if err != nil {
  1414  		return nil, fmt.Errorf("Move: %w", err)
  1415  	}
  1416  	srcPath, dstPath := srcObj.path(), path.Join(f.absRoot, remote)
  1417  	if _, ok := c.sftpClient.HasExtension("posix-rename@openssh.com"); ok {
  1418  		err = c.sftpClient.PosixRename(srcPath, dstPath)
  1419  	} else {
  1420  		// If haven't got PosixRename then remove source first before renaming
  1421  		err = c.sftpClient.Remove(dstPath)
  1422  		if err != nil && !errors.Is(err, iofs.ErrNotExist) {
  1423  			fs.Errorf(f, "Move: Failed to remove existing file %q: %v", dstPath, err)
  1424  		}
  1425  		err = c.sftpClient.Rename(srcPath, dstPath)
  1426  	}
  1427  	f.putSftpConnection(&c, err)
  1428  	if err != nil {
  1429  		return nil, fmt.Errorf("Move Rename failed: %w", err)
  1430  	}
  1431  	dstObj, err := f.NewObject(ctx, remote)
  1432  	if err != nil {
  1433  		return nil, fmt.Errorf("Move NewObject failed: %w", err)
  1434  	}
  1435  	return dstObj, nil
  1436  }
  1437  
  1438  // Copy server side copies a remote sftp file object using hardlinks
  1439  func (f *Fs) Copy(ctx context.Context, src fs.Object, remote string) (fs.Object, error) {
  1440  	if !f.opt.CopyIsHardlink {
  1441  		return nil, fs.ErrorCantCopy
  1442  	}
  1443  	srcObj, ok := src.(*Object)
  1444  	if !ok {
  1445  		fs.Debugf(src, "Can't copy - not same remote type")
  1446  		return nil, fs.ErrorCantCopy
  1447  	}
  1448  	err := f.mkParentDir(ctx, remote)
  1449  	if err != nil {
  1450  		return nil, fmt.Errorf("Copy mkParentDir failed: %w", err)
  1451  	}
  1452  	c, err := f.getSftpConnection(ctx)
  1453  	if err != nil {
  1454  		return nil, fmt.Errorf("Copy: %w", err)
  1455  	}
  1456  	srcPath, dstPath := srcObj.path(), path.Join(f.absRoot, remote)
  1457  	err = c.sftpClient.Link(srcPath, dstPath)
  1458  	f.putSftpConnection(&c, err)
  1459  	if err != nil {
  1460  		if sftpErr, ok := err.(*sftp.StatusError); ok {
  1461  			if sftpErr.FxCode() == sftp.ErrSSHFxOpUnsupported {
  1462  				// Remote doesn't support Link
  1463  				return nil, fs.ErrorCantCopy
  1464  			}
  1465  		}
  1466  		return nil, fmt.Errorf("Copy failed: %w", err)
  1467  	}
  1468  	dstObj, err := f.NewObject(ctx, remote)
  1469  	if err != nil {
  1470  		return nil, fmt.Errorf("Copy NewObject failed: %w", err)
  1471  	}
  1472  	return dstObj, nil
  1473  }
  1474  
  1475  // DirMove moves src, srcRemote to this remote at dstRemote
  1476  // using server-side move operations.
  1477  //
  1478  // Will only be called if src.Fs().Name() == f.Name()
  1479  //
  1480  // If it isn't possible then return fs.ErrorCantDirMove
  1481  //
  1482  // If destination exists then return fs.ErrorDirExists
  1483  func (f *Fs) DirMove(ctx context.Context, src fs.Fs, srcRemote, dstRemote string) error {
  1484  	srcFs, ok := src.(*Fs)
  1485  	if !ok {
  1486  		fs.Debugf(srcFs, "Can't move directory - not same remote type")
  1487  		return fs.ErrorCantDirMove
  1488  	}
  1489  	srcPath := path.Join(srcFs.absRoot, srcRemote)
  1490  	dstPath := path.Join(f.absRoot, dstRemote)
  1491  
  1492  	// Check if destination exists
  1493  	ok, err := f.dirExists(ctx, dstPath)
  1494  	if err != nil {
  1495  		return fmt.Errorf("DirMove dirExists dst failed: %w", err)
  1496  	}
  1497  	if ok {
  1498  		return fs.ErrorDirExists
  1499  	}
  1500  
  1501  	// Make sure the parent directory exists
  1502  	err = f.mkdir(ctx, path.Dir(dstPath))
  1503  	if err != nil {
  1504  		return fmt.Errorf("DirMove mkParentDir dst failed: %w", err)
  1505  	}
  1506  
  1507  	// Do the move
  1508  	c, err := f.getSftpConnection(ctx)
  1509  	if err != nil {
  1510  		return fmt.Errorf("DirMove: %w", err)
  1511  	}
  1512  	err = c.sftpClient.Rename(
  1513  		srcPath,
  1514  		dstPath,
  1515  	)
  1516  	f.putSftpConnection(&c, err)
  1517  	if err != nil {
  1518  		return fmt.Errorf("DirMove Rename(%q,%q) failed: %w", srcPath, dstPath, err)
  1519  	}
  1520  	return nil
  1521  }
  1522  
  1523  // run runds cmd on the remote end returning standard output
  1524  func (f *Fs) run(ctx context.Context, cmd string) ([]byte, error) {
  1525  	f.addSession() // Show session in use
  1526  	defer f.removeSession()
  1527  
  1528  	c, err := f.getSftpConnection(ctx)
  1529  	if err != nil {
  1530  		return nil, fmt.Errorf("run: get SFTP connection: %w", err)
  1531  	}
  1532  	defer f.putSftpConnection(&c, err)
  1533  
  1534  	// Send keepalives while the connection is open
  1535  	defer close(c.sendKeepAlives(keepAliveInterval))
  1536  
  1537  	session, err := c.sshClient.NewSession()
  1538  	if err != nil {
  1539  		return nil, fmt.Errorf("run: get SFTP session: %w", err)
  1540  	}
  1541  	err = f.setEnv(session)
  1542  	if err != nil {
  1543  		return nil, err
  1544  	}
  1545  	defer func() {
  1546  		_ = session.Close()
  1547  	}()
  1548  
  1549  	var stdout, stderr bytes.Buffer
  1550  	session.SetStdout(&stdout)
  1551  	session.SetStderr(&stderr)
  1552  
  1553  	fs.Debugf(f, "Running remote command: %s", cmd)
  1554  	err = session.Run(cmd)
  1555  	if err != nil {
  1556  		return nil, fmt.Errorf("failed to run %q: %s: %w", cmd, bytes.TrimSpace(stderr.Bytes()), err)
  1557  	}
  1558  	fs.Debugf(f, "Remote command result: %s", bytes.TrimSpace(stdout.Bytes()))
  1559  
  1560  	return stdout.Bytes(), nil
  1561  }
  1562  
  1563  // Hashes returns the supported hash types of the filesystem
  1564  func (f *Fs) Hashes() hash.Set {
  1565  	ctx := context.TODO()
  1566  
  1567  	if f.cachedHashes != nil {
  1568  		return *f.cachedHashes
  1569  	}
  1570  
  1571  	hashSet := hash.NewHashSet()
  1572  	f.cachedHashes = &hashSet
  1573  
  1574  	if f.opt.DisableHashCheck || f.shellType == shellTypeNotSupported {
  1575  		return hashSet
  1576  	}
  1577  
  1578  	// look for a hash command which works
  1579  	checkHash := func(hashType hash.Type, commands []struct{ hashFile, hashEmpty string }, expected string, hashCommand *string, changed *bool) bool {
  1580  		if *hashCommand == hashCommandNotSupported {
  1581  			return false
  1582  		}
  1583  		if *hashCommand != "" {
  1584  			return true
  1585  		}
  1586  		fs.Debugf(f, "Checking default %v hash commands", hashType)
  1587  		*changed = true
  1588  		for _, command := range commands {
  1589  			output, err := f.run(ctx, command.hashEmpty)
  1590  			if err != nil {
  1591  				fs.Debugf(f, "Hash command skipped: %v", err)
  1592  				continue
  1593  			}
  1594  			output = bytes.TrimSpace(output)
  1595  			if parseHash(output) == expected {
  1596  				*hashCommand = command.hashFile
  1597  				fs.Debugf(f, "Hash command accepted")
  1598  				return true
  1599  			}
  1600  			fs.Debugf(f, "Hash command skipped: Wrong output")
  1601  		}
  1602  		*hashCommand = hashCommandNotSupported
  1603  		return false
  1604  	}
  1605  
  1606  	changed := false
  1607  	md5Commands := []struct {
  1608  		hashFile, hashEmpty string
  1609  	}{
  1610  		{"md5sum", "md5sum"},
  1611  		{"md5 -r", "md5 -r"},
  1612  		{"rclone md5sum", "rclone md5sum"},
  1613  	}
  1614  	sha1Commands := []struct {
  1615  		hashFile, hashEmpty string
  1616  	}{
  1617  		{"sha1sum", "sha1sum"},
  1618  		{"sha1 -r", "sha1 -r"},
  1619  		{"rclone sha1sum", "rclone sha1sum"},
  1620  	}
  1621  	if f.shellType == "powershell" {
  1622  		md5Commands = append(md5Commands, struct {
  1623  			hashFile, hashEmpty string
  1624  		}{
  1625  			"&{param($Path);Get-FileHash -Algorithm MD5 -LiteralPath $Path -ErrorAction Stop|Select-Object -First 1 -ExpandProperty Hash|ForEach-Object{\"$($_.ToLower())  ${Path}\"}}",
  1626  			"Get-FileHash -Algorithm MD5 -InputStream ([System.IO.MemoryStream]::new()) -ErrorAction Stop|Select-Object -First 1 -ExpandProperty Hash|ForEach-Object{$_.ToLower()}",
  1627  		})
  1628  
  1629  		sha1Commands = append(sha1Commands, struct {
  1630  			hashFile, hashEmpty string
  1631  		}{
  1632  			"&{param($Path);Get-FileHash -Algorithm SHA1 -LiteralPath $Path -ErrorAction Stop|Select-Object -First 1 -ExpandProperty Hash|ForEach-Object{\"$($_.ToLower())  ${Path}\"}}",
  1633  			"Get-FileHash -Algorithm SHA1 -InputStream ([System.IO.MemoryStream]::new()) -ErrorAction Stop|Select-Object -First 1 -ExpandProperty Hash|ForEach-Object{$_.ToLower()}",
  1634  		})
  1635  	}
  1636  
  1637  	md5Works := checkHash(hash.MD5, md5Commands, "d41d8cd98f00b204e9800998ecf8427e", &f.opt.Md5sumCommand, &changed)
  1638  	sha1Works := checkHash(hash.SHA1, sha1Commands, "da39a3ee5e6b4b0d3255bfef95601890afd80709", &f.opt.Sha1sumCommand, &changed)
  1639  
  1640  	if changed {
  1641  		// Save permanently in config to avoid the extra work next time
  1642  		fs.Debugf(f, "Setting hash command for %v to %q (set sha1sum_command to override)", hash.MD5, f.opt.Md5sumCommand)
  1643  		f.m.Set("md5sum_command", f.opt.Md5sumCommand)
  1644  		fs.Debugf(f, "Setting hash command for %v to %q (set md5sum_command to override)", hash.SHA1, f.opt.Sha1sumCommand)
  1645  		f.m.Set("sha1sum_command", f.opt.Sha1sumCommand)
  1646  	}
  1647  
  1648  	if sha1Works {
  1649  		hashSet.Add(hash.SHA1)
  1650  	}
  1651  	if md5Works {
  1652  		hashSet.Add(hash.MD5)
  1653  	}
  1654  
  1655  	return hashSet
  1656  }
  1657  
  1658  // About gets usage stats
  1659  func (f *Fs) About(ctx context.Context) (*fs.Usage, error) {
  1660  	// If server implements the vendor-specific VFS statistics extension prefer that
  1661  	// (OpenSSH implements it on using syscall.Statfs on Linux and API function GetDiskFreeSpace on Windows)
  1662  	c, err := f.getSftpConnection(ctx)
  1663  	if err != nil {
  1664  		return nil, err
  1665  	}
  1666  	var vfsStats *sftp.StatVFS
  1667  	if _, found := c.sftpClient.HasExtension("statvfs@openssh.com"); found {
  1668  		fs.Debugf(f, "Server has VFS statistics extension")
  1669  		aboutPath := f.absRoot
  1670  		if aboutPath == "" {
  1671  			aboutPath = "/"
  1672  		}
  1673  		fs.Debugf(f, "About path %q", aboutPath)
  1674  		vfsStats, err = c.sftpClient.StatVFS(aboutPath)
  1675  	}
  1676  	f.putSftpConnection(&c, err) // Return to pool asap, if running shell command below it will be reused
  1677  	if vfsStats != nil {
  1678  		total := vfsStats.TotalSpace()
  1679  		free := vfsStats.FreeSpace()
  1680  		used := total - free
  1681  		return &fs.Usage{
  1682  			Total: fs.NewUsageValue(int64(total)),
  1683  			Used:  fs.NewUsageValue(int64(used)),
  1684  			Free:  fs.NewUsageValue(int64(free)),
  1685  		}, nil
  1686  	} else if err != nil {
  1687  		if errors.Is(err, os.ErrNotExist) {
  1688  			return nil, err
  1689  		}
  1690  		fs.Debugf(f, "Failed to retrieve VFS statistics, trying shell command instead: %v", err)
  1691  	} else {
  1692  		fs.Debugf(f, "Server does not have the VFS statistics extension, trying shell command instead")
  1693  	}
  1694  
  1695  	// Fall back to shell command method if possible
  1696  	if f.shellType == shellTypeNotSupported || f.shellType == "cmd" {
  1697  		fs.Debugf(f, "About shell command is not available for shell type %q (set option shell_type to override)", f.shellType)
  1698  		return nil, fmt.Errorf("not supported with shell type %q", f.shellType)
  1699  	}
  1700  	aboutShellPath := f.remoteShellPath("")
  1701  	if aboutShellPath == "" {
  1702  		aboutShellPath = "/"
  1703  	}
  1704  	fs.Debugf(f, "About path %q", aboutShellPath)
  1705  	aboutShellPathArg, err := f.quoteOrEscapeShellPath(aboutShellPath)
  1706  	if err != nil {
  1707  		return nil, err
  1708  	}
  1709  	// PowerShell
  1710  	if f.shellType == "powershell" {
  1711  		shellCmd := "Get-Item " + aboutShellPathArg + " -ErrorAction Stop|Select-Object -First 1 -ExpandProperty PSDrive|ForEach-Object{\"$($_.Used) $($_.Free)\"}"
  1712  		fs.Debugf(f, "About using shell command for shell type %q", f.shellType)
  1713  		stdout, err := f.run(ctx, shellCmd)
  1714  		if err != nil {
  1715  			fs.Debugf(f, "About shell command for shell type %q failed (set option shell_type to override): %v", f.shellType, err)
  1716  			return nil, fmt.Errorf("powershell command failed: %w", err)
  1717  		}
  1718  		split := strings.Fields(string(stdout))
  1719  		usage := &fs.Usage{}
  1720  		if len(split) == 2 {
  1721  			usedValue, usedErr := strconv.ParseInt(split[0], 10, 64)
  1722  			if usedErr == nil {
  1723  				usage.Used = fs.NewUsageValue(usedValue)
  1724  			}
  1725  			freeValue, freeErr := strconv.ParseInt(split[1], 10, 64)
  1726  			if freeErr == nil {
  1727  				usage.Free = fs.NewUsageValue(freeValue)
  1728  				if usedErr == nil {
  1729  					usage.Total = fs.NewUsageValue(usedValue + freeValue)
  1730  				}
  1731  			}
  1732  		}
  1733  		return usage, nil
  1734  	}
  1735  	// Unix/default shell
  1736  	shellCmd := "df -k " + aboutShellPathArg
  1737  	fs.Debugf(f, "About using shell command for shell type %q", f.shellType)
  1738  	stdout, err := f.run(ctx, shellCmd)
  1739  	if err != nil {
  1740  		fs.Debugf(f, "About shell command for shell type %q failed (set option shell_type to override): %v", f.shellType, err)
  1741  		return nil, fmt.Errorf("your remote may not have the required df utility: %w", err)
  1742  	}
  1743  	usageTotal, usageUsed, usageAvail := parseUsage(stdout)
  1744  	usage := &fs.Usage{}
  1745  	if usageTotal >= 0 {
  1746  		usage.Total = fs.NewUsageValue(usageTotal)
  1747  	}
  1748  	if usageUsed >= 0 {
  1749  		usage.Used = fs.NewUsageValue(usageUsed)
  1750  	}
  1751  	if usageAvail >= 0 {
  1752  		usage.Free = fs.NewUsageValue(usageAvail)
  1753  	}
  1754  	return usage, nil
  1755  }
  1756  
  1757  // Shutdown the backend, closing any background tasks and any
  1758  // cached connections.
  1759  func (f *Fs) Shutdown(ctx context.Context) error {
  1760  	return f.drainPool(ctx)
  1761  }
  1762  
  1763  // Fs is the filesystem this remote sftp file object is located within
  1764  func (o *Object) Fs() fs.Info {
  1765  	return o.fs
  1766  }
  1767  
  1768  // String returns the URL to the remote SFTP file
  1769  func (o *Object) String() string {
  1770  	if o == nil {
  1771  		return "<nil>"
  1772  	}
  1773  	return o.remote
  1774  }
  1775  
  1776  // Remote the name of the remote SFTP file, relative to the fs root
  1777  func (o *Object) Remote() string {
  1778  	return o.remote
  1779  }
  1780  
  1781  // Hash returns the selected checksum of the file
  1782  // If no checksum is available it returns ""
  1783  func (o *Object) Hash(ctx context.Context, r hash.Type) (string, error) {
  1784  	if o.fs.opt.DisableHashCheck {
  1785  		return "", nil
  1786  	}
  1787  	_ = o.fs.Hashes()
  1788  
  1789  	var hashCmd string
  1790  	if r == hash.MD5 {
  1791  		if o.md5sum != nil {
  1792  			return *o.md5sum, nil
  1793  		}
  1794  		hashCmd = o.fs.opt.Md5sumCommand
  1795  	} else if r == hash.SHA1 {
  1796  		if o.sha1sum != nil {
  1797  			return *o.sha1sum, nil
  1798  		}
  1799  		hashCmd = o.fs.opt.Sha1sumCommand
  1800  	} else {
  1801  		return "", hash.ErrUnsupported
  1802  	}
  1803  	if hashCmd == "" || hashCmd == hashCommandNotSupported {
  1804  		return "", hash.ErrUnsupported
  1805  	}
  1806  
  1807  	shellPathArg, err := o.fs.quoteOrEscapeShellPath(o.shellPath())
  1808  	if err != nil {
  1809  		return "", fmt.Errorf("failed to calculate %v hash: %w", r, err)
  1810  	}
  1811  	outBytes, err := o.fs.run(ctx, hashCmd+" "+shellPathArg)
  1812  	if err != nil {
  1813  		return "", fmt.Errorf("failed to calculate %v hash: %w", r, err)
  1814  	}
  1815  	hashString := parseHash(outBytes)
  1816  	fs.Debugf(o, "Parsed hash: %s", hashString)
  1817  	if r == hash.MD5 {
  1818  		o.md5sum = &hashString
  1819  	} else if r == hash.SHA1 {
  1820  		o.sha1sum = &hashString
  1821  	}
  1822  	return hashString, nil
  1823  }
  1824  
  1825  // quoteOrEscapeShellPath makes path a valid string argument in configured shell
  1826  // and also ensures it cannot cause unintended behavior.
  1827  func quoteOrEscapeShellPath(shellType string, shellPath string) (string, error) {
  1828  	// PowerShell
  1829  	if shellType == "powershell" {
  1830  		return "'" + strings.ReplaceAll(shellPath, "'", "''") + "'", nil
  1831  	}
  1832  	// Windows Command Prompt
  1833  	if shellType == "cmd" {
  1834  		if strings.Contains(shellPath, "\"") {
  1835  			return "", fmt.Errorf("path is not valid in shell type %s: %s", shellType, shellPath)
  1836  		}
  1837  		return "\"" + shellPath + "\"", nil
  1838  	}
  1839  	// Unix shell
  1840  	safe := unixShellEscapeRegex.ReplaceAllString(shellPath, `\$0`)
  1841  	return strings.ReplaceAll(safe, "\n", "'\n'"), nil
  1842  }
  1843  
  1844  // quoteOrEscapeShellPath makes path a valid string argument in configured shell
  1845  func (f *Fs) quoteOrEscapeShellPath(shellPath string) (string, error) {
  1846  	return quoteOrEscapeShellPath(f.shellType, shellPath)
  1847  }
  1848  
  1849  // remotePath returns the native SFTP path of the file or directory at the remote given
  1850  func (f *Fs) remotePath(remote string) string {
  1851  	return path.Join(f.absRoot, remote)
  1852  }
  1853  
  1854  // remoteShellPath returns the SSH shell path of the file or directory at the remote given
  1855  func (f *Fs) remoteShellPath(remote string) string {
  1856  	if f.opt.PathOverride != "" {
  1857  		shellPath := path.Join(f.opt.PathOverride, remote)
  1858  		if f.opt.PathOverride[0] == '@' {
  1859  			shellPath = path.Join(strings.TrimPrefix(f.opt.PathOverride, "@"), f.absRoot, remote)
  1860  		}
  1861  		fs.Debugf(f, "Shell path redirected to %q with option path_override", shellPath)
  1862  		return shellPath
  1863  	}
  1864  	shellPath := path.Join(f.absRoot, remote)
  1865  	if f.shellType == "powershell" || f.shellType == "cmd" {
  1866  		// If remote shell is powershell or cmd, then server is probably Windows.
  1867  		// The sftp package converts everything to POSIX paths: Forward slashes, and
  1868  		// absolute paths starts with a slash. An absolute path on a Windows server will
  1869  		// then look like this "/C:/Windows/System32". We must remove the "/" prefix
  1870  		// to make this a valid path for shell commands. In case of PowerShell there is a
  1871  		// possibility that it is a Unix server, with PowerShell Core shell, but assuming
  1872  		// root folders with names such as "C:" are rare, we just take this risk,
  1873  		// and option path_override can always be used to work around corner cases.
  1874  		if posixWinAbsPathRegex.MatchString(shellPath) {
  1875  			shellPath = strings.TrimPrefix(shellPath, "/")
  1876  			fs.Debugf(f, "Shell path adjusted to %q (set option path_override to override)", shellPath)
  1877  			return shellPath
  1878  		}
  1879  	}
  1880  	fs.Debugf(f, "Shell path %q", shellPath)
  1881  	return shellPath
  1882  }
  1883  
  1884  // Converts a byte array from the SSH session returned by
  1885  // an invocation of md5sum/sha1sum to a hash string
  1886  // as expected by the rest of this application
  1887  func parseHash(bytes []byte) string {
  1888  	// For strings with backslash *sum writes a leading \
  1889  	// https://unix.stackexchange.com/q/313733/94054
  1890  	return strings.ToLower(strings.Split(strings.TrimLeft(string(bytes), "\\"), " ")[0]) // Split at hash / filename separator / all convert to lowercase
  1891  }
  1892  
  1893  // Parses the byte array output from the SSH session
  1894  // returned by an invocation of df into
  1895  // the disk size, used space, and available space on the disk, in that order.
  1896  // Only works when `df` has output info on only one disk
  1897  func parseUsage(bytes []byte) (spaceTotal int64, spaceUsed int64, spaceAvail int64) {
  1898  	spaceTotal, spaceUsed, spaceAvail = -1, -1, -1
  1899  	lines := strings.Split(string(bytes), "\n")
  1900  	if len(lines) < 2 {
  1901  		return
  1902  	}
  1903  	split := strings.Fields(lines[1])
  1904  	if len(split) < 6 {
  1905  		return
  1906  	}
  1907  	spaceTotal, err := strconv.ParseInt(split[1], 10, 64)
  1908  	if err != nil {
  1909  		spaceTotal = -1
  1910  	}
  1911  	spaceUsed, err = strconv.ParseInt(split[2], 10, 64)
  1912  	if err != nil {
  1913  		spaceUsed = -1
  1914  	}
  1915  	spaceAvail, err = strconv.ParseInt(split[3], 10, 64)
  1916  	if err != nil {
  1917  		spaceAvail = -1
  1918  	}
  1919  	return spaceTotal * 1024, spaceUsed * 1024, spaceAvail * 1024
  1920  }
  1921  
  1922  // Size returns the size in bytes of the remote sftp file
  1923  func (o *Object) Size() int64 {
  1924  	return o.size
  1925  }
  1926  
  1927  // ModTime returns the modification time of the remote sftp file
  1928  func (o *Object) ModTime(ctx context.Context) time.Time {
  1929  	return o.modTime
  1930  }
  1931  
  1932  // path returns the native SFTP path of the object
  1933  func (o *Object) path() string {
  1934  	return o.fs.remotePath(o.remote)
  1935  }
  1936  
  1937  // shellPath returns the SSH shell path of the object
  1938  func (o *Object) shellPath() string {
  1939  	return o.fs.remoteShellPath(o.remote)
  1940  }
  1941  
  1942  // setMetadata updates the info in the object from the stat result passed in
  1943  func (o *Object) setMetadata(info os.FileInfo) {
  1944  	o.modTime = info.ModTime()
  1945  	o.size = info.Size()
  1946  	o.mode = info.Mode()
  1947  }
  1948  
  1949  // statRemote stats the file or directory at the remote given
  1950  func (f *Fs) stat(ctx context.Context, remote string) (info os.FileInfo, err error) {
  1951  	absPath := remote
  1952  	if !strings.HasPrefix(remote, "/") {
  1953  		absPath = path.Join(f.absRoot, remote)
  1954  	}
  1955  	c, err := f.getSftpConnection(ctx)
  1956  	if err != nil {
  1957  		return nil, fmt.Errorf("stat: %w", err)
  1958  	}
  1959  	info, err = c.sftpClient.Stat(absPath)
  1960  	f.putSftpConnection(&c, err)
  1961  	return info, err
  1962  }
  1963  
  1964  // stat updates the info in the Object
  1965  func (o *Object) stat(ctx context.Context) error {
  1966  	info, err := o.fs.stat(ctx, o.remote)
  1967  	if err != nil {
  1968  		if os.IsNotExist(err) {
  1969  			return fs.ErrorObjectNotFound
  1970  		}
  1971  		return fmt.Errorf("stat failed: %w", err)
  1972  	}
  1973  	if info.IsDir() {
  1974  		return fs.ErrorIsDir
  1975  	}
  1976  	o.setMetadata(info)
  1977  	return nil
  1978  }
  1979  
  1980  // SetModTime sets the modification and access time to the specified time
  1981  //
  1982  // it also updates the info field
  1983  func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error {
  1984  	if !o.fs.opt.SetModTime {
  1985  		return nil
  1986  	}
  1987  	c, err := o.fs.getSftpConnection(ctx)
  1988  	if err != nil {
  1989  		return fmt.Errorf("SetModTime: %w", err)
  1990  	}
  1991  	err = c.sftpClient.Chtimes(o.path(), modTime, modTime)
  1992  	o.fs.putSftpConnection(&c, err)
  1993  	if err != nil {
  1994  		return fmt.Errorf("SetModTime failed: %w", err)
  1995  	}
  1996  	err = o.stat(ctx)
  1997  	if err != nil && err != fs.ErrorIsDir {
  1998  		return fmt.Errorf("SetModTime stat failed: %w", err)
  1999  	}
  2000  	return nil
  2001  }
  2002  
  2003  // Storable returns whether the remote sftp file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc.)
  2004  func (o *Object) Storable() bool {
  2005  	return o.mode.IsRegular()
  2006  }
  2007  
  2008  // objectReader represents a file open for reading on the SFTP server
  2009  type objectReader struct {
  2010  	f          *Fs
  2011  	sftpFile   *sftp.File
  2012  	pipeReader *io.PipeReader
  2013  	done       chan struct{}
  2014  }
  2015  
  2016  func (f *Fs) newObjectReader(sftpFile *sftp.File) *objectReader {
  2017  	pipeReader, pipeWriter := io.Pipe()
  2018  	file := &objectReader{
  2019  		f:          f,
  2020  		sftpFile:   sftpFile,
  2021  		pipeReader: pipeReader,
  2022  		done:       make(chan struct{}),
  2023  	}
  2024  	// Show connection in use
  2025  	f.addSession()
  2026  
  2027  	go func() {
  2028  		// Use sftpFile.WriteTo to pump data so that it gets a
  2029  		// chance to build the window up.
  2030  		_, err := sftpFile.WriteTo(pipeWriter)
  2031  		// Close the pipeWriter so the pipeReader fails with
  2032  		// the same error or EOF if err == nil
  2033  		_ = pipeWriter.CloseWithError(err)
  2034  		// signal that we've finished
  2035  		close(file.done)
  2036  	}()
  2037  
  2038  	return file
  2039  }
  2040  
  2041  // Read from a remote sftp file object reader
  2042  func (file *objectReader) Read(p []byte) (n int, err error) {
  2043  	n, err = file.pipeReader.Read(p)
  2044  	return n, err
  2045  }
  2046  
  2047  // Close a reader of a remote sftp file
  2048  func (file *objectReader) Close() (err error) {
  2049  	// Close the sftpFile - this will likely cause the WriteTo to error
  2050  	err = file.sftpFile.Close()
  2051  	// Close the pipeReader so writes to the pipeWriter fail
  2052  	_ = file.pipeReader.Close()
  2053  	// Wait for the background process to finish
  2054  	<-file.done
  2055  	// Show connection no longer in use
  2056  	file.f.removeSession()
  2057  	return err
  2058  }
  2059  
  2060  // Open a remote sftp file object for reading. Seek is supported
  2061  func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (in io.ReadCloser, err error) {
  2062  	var offset, limit int64 = 0, -1
  2063  	for _, option := range options {
  2064  		switch x := option.(type) {
  2065  		case *fs.SeekOption:
  2066  			offset = x.Offset
  2067  		case *fs.RangeOption:
  2068  			offset, limit = x.Decode(o.Size())
  2069  		default:
  2070  			if option.Mandatory() {
  2071  				fs.Logf(o, "Unsupported mandatory option: %v", option)
  2072  			}
  2073  		}
  2074  	}
  2075  	c, err := o.fs.getSftpConnection(ctx)
  2076  	if err != nil {
  2077  		return nil, fmt.Errorf("Open: %w", err)
  2078  	}
  2079  	sftpFile, err := c.sftpClient.Open(o.path())
  2080  	o.fs.putSftpConnection(&c, err)
  2081  	if err != nil {
  2082  		return nil, fmt.Errorf("Open failed: %w", err)
  2083  	}
  2084  	if offset > 0 {
  2085  		off, err := sftpFile.Seek(offset, io.SeekStart)
  2086  		if err != nil || off != offset {
  2087  			return nil, fmt.Errorf("Open Seek failed: %w", err)
  2088  		}
  2089  	}
  2090  	in = readers.NewLimitedReadCloser(o.fs.newObjectReader(sftpFile), limit)
  2091  	return in, nil
  2092  }
  2093  
  2094  type sizeReader struct {
  2095  	io.Reader
  2096  	size int64
  2097  }
  2098  
  2099  // Size returns the expected size of the stream
  2100  //
  2101  // It is used in sftpFile.ReadFrom as a hint to work out the
  2102  // concurrency needed
  2103  func (sr *sizeReader) Size() int64 {
  2104  	return sr.size
  2105  }
  2106  
  2107  // Update a remote sftp file using the data <in> and ModTime from <src>
  2108  func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error {
  2109  	o.fs.addSession() // Show session in use
  2110  	defer o.fs.removeSession()
  2111  	// Clear the hash cache since we are about to update the object
  2112  	o.md5sum = nil
  2113  	o.sha1sum = nil
  2114  	c, err := o.fs.getSftpConnection(ctx)
  2115  	if err != nil {
  2116  		return fmt.Errorf("Update: %w", err)
  2117  	}
  2118  	// Hang on to the connection for the whole upload so it doesn't get reused while we are uploading
  2119  	file, err := c.sftpClient.OpenFile(o.path(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC)
  2120  	if err != nil {
  2121  		o.fs.putSftpConnection(&c, err)
  2122  		return fmt.Errorf("Update Create failed: %w", err)
  2123  	}
  2124  	// remove the file if upload failed
  2125  	remove := func() {
  2126  		c, removeErr := o.fs.getSftpConnection(ctx)
  2127  		if removeErr != nil {
  2128  			fs.Debugf(src, "Failed to open new SSH connection for delete: %v", removeErr)
  2129  			return
  2130  		}
  2131  		removeErr = c.sftpClient.Remove(o.path())
  2132  		o.fs.putSftpConnection(&c, removeErr)
  2133  		if removeErr != nil {
  2134  			fs.Debugf(src, "Failed to remove: %v", removeErr)
  2135  		} else {
  2136  			fs.Debugf(src, "Removed after failed upload: %v", err)
  2137  		}
  2138  	}
  2139  	_, err = file.ReadFrom(&sizeReader{Reader: in, size: src.Size()})
  2140  	if err != nil {
  2141  		o.fs.putSftpConnection(&c, err)
  2142  		remove()
  2143  		return fmt.Errorf("Update ReadFrom failed: %w", err)
  2144  	}
  2145  	err = file.Close()
  2146  	if err != nil {
  2147  		o.fs.putSftpConnection(&c, err)
  2148  		remove()
  2149  		return fmt.Errorf("Update Close failed: %w", err)
  2150  	}
  2151  	// Release connection only when upload has finished so we don't upload multiple files on the same connection
  2152  	o.fs.putSftpConnection(&c, err)
  2153  
  2154  	// Set the mod time - this stats the object if o.fs.opt.SetModTime == true
  2155  	err = o.SetModTime(ctx, src.ModTime(ctx))
  2156  	if err != nil {
  2157  		return fmt.Errorf("Update SetModTime failed: %w", err)
  2158  	}
  2159  
  2160  	// Stat the file after the upload to read its stats back if o.fs.opt.SetModTime == false
  2161  	if !o.fs.opt.SetModTime {
  2162  		err = o.stat(ctx)
  2163  		if err == fs.ErrorObjectNotFound {
  2164  			// In the specific case of o.fs.opt.SetModTime == false
  2165  			// if the object wasn't found then don't return an error
  2166  			fs.Debugf(o, "Not found after upload with set_modtime=false so returning best guess")
  2167  			o.modTime = src.ModTime(ctx)
  2168  			o.size = src.Size()
  2169  			o.mode = os.FileMode(0666) // regular file
  2170  		} else if err != nil {
  2171  			return fmt.Errorf("Update stat failed: %w", err)
  2172  		}
  2173  	}
  2174  
  2175  	return nil
  2176  }
  2177  
  2178  // Remove a remote sftp file object
  2179  func (o *Object) Remove(ctx context.Context) error {
  2180  	c, err := o.fs.getSftpConnection(ctx)
  2181  	if err != nil {
  2182  		return fmt.Errorf("Remove: %w", err)
  2183  	}
  2184  	err = c.sftpClient.Remove(o.path())
  2185  	o.fs.putSftpConnection(&c, err)
  2186  	return err
  2187  }
  2188  
  2189  // Check the interfaces are satisfied
  2190  var (
  2191  	_ fs.Fs             = &Fs{}
  2192  	_ fs.PutStreamer    = &Fs{}
  2193  	_ fs.Mover          = &Fs{}
  2194  	_ fs.Copier         = &Fs{}
  2195  	_ fs.DirMover       = &Fs{}
  2196  	_ fs.DirSetModTimer = &Fs{}
  2197  	_ fs.Abouter        = &Fs{}
  2198  	_ fs.Shutdowner     = &Fs{}
  2199  	_ fs.Object         = &Object{}
  2200  )