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