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 )