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