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