2016-11-13 07:36:08 +08:00
// Package sftp provides a filesystem interface using github.com/pkg/sftp
2017-02-01 04:34:11 +08:00
2019-04-16 03:03:33 +08:00
// +build !plan9
2017-02-01 04:34:11 +08:00
2016-11-13 07:36:08 +08:00
package sftp
import (
2018-04-19 16:45:46 +08:00
"bytes"
2018-04-07 02:13:27 +08:00
"context"
2018-03-15 07:17:09 +08:00
"fmt"
2016-11-13 07:36:08 +08:00
"io"
2017-06-23 23:25:35 +08:00
"io/ioutil"
2016-11-13 07:36:08 +08:00
"os"
2018-01-07 20:57:46 +08:00
"os/user"
2016-11-13 07:36:08 +08:00
"path"
2017-08-07 21:50:31 +08:00
"regexp"
"strings"
2017-08-08 00:19:37 +08:00
"sync"
2016-11-13 07:36:08 +08:00
"time"
"github.com/ncw/rclone/fs"
2018-01-13 00:30:54 +08:00
"github.com/ncw/rclone/fs/config"
2018-05-15 01:06:57 +08:00
"github.com/ncw/rclone/fs/config/configmap"
"github.com/ncw/rclone/fs/config/configstruct"
2018-01-19 04:19:55 +08:00
"github.com/ncw/rclone/fs/config/obscure"
2018-01-13 00:30:54 +08:00
"github.com/ncw/rclone/fs/fshttp"
"github.com/ncw/rclone/fs/hash"
2018-01-24 04:21:19 +08:00
"github.com/ncw/rclone/lib/readers"
2017-02-01 04:34:11 +08:00
"github.com/pkg/errors"
2016-11-13 07:36:08 +08:00
"github.com/pkg/sftp"
2018-12-04 18:11:57 +08:00
sshagent "github.com/xanzy/ssh-agent"
2017-05-24 22:39:17 +08:00
"golang.org/x/crypto/ssh"
2017-08-08 01:01:31 +08:00
"golang.org/x/time/rate"
)
const (
connectionsPerSecond = 10 // don't make more than this many ssh connections/s
2016-11-13 07:36:08 +08:00
)
2018-01-07 20:57:46 +08:00
var (
currentUser = readCurrentUser ( )
)
2016-11-13 07:36:08 +08:00
func init ( ) {
fsi := & fs . RegInfo {
Name : "sftp" ,
Description : "SSH/SFTP Connection" ,
NewFs : NewFs ,
Options : [ ] fs . Option { {
Name : "host" ,
Help : "SSH host to connect to" ,
2018-05-15 01:06:57 +08:00
Required : true ,
2016-11-13 07:36:08 +08:00
Examples : [ ] fs . OptionExample { {
Value : "example.com" ,
Help : "Connect to example.com" ,
} } ,
} , {
2018-05-15 01:06:57 +08:00
Name : "user" ,
Help : "SSH username, leave blank for current username, " + currentUser ,
2016-11-13 07:36:08 +08:00
} , {
2018-05-15 01:06:57 +08:00
Name : "port" ,
Help : "SSH port, leave blank to use default (22)" ,
2016-11-13 07:36:08 +08:00
} , {
Name : "pass" ,
2017-06-23 23:25:35 +08:00
Help : "SSH password, leave blank to use ssh-agent." ,
2016-11-13 07:36:08 +08:00
IsPassword : true ,
2017-06-23 23:25:35 +08:00
} , {
2018-05-15 01:06:57 +08:00
Name : "key_file" ,
2019-01-03 19:25:13 +08:00
Help : "Path to PEM-encoded private key file, leave blank or set key-use-agent to use ssh-agent." ,
2019-01-03 19:24:31 +08:00
} , {
Name : "key_file_pass" ,
Help : ` The passphrase to decrypt the PEM - encoded private key file .
Only PEM encrypted key files ( old OpenSSH format ) are supported . Encrypted keys
in the new OpenSSH format can ' t be used . ` ,
IsPassword : true ,
2019-01-03 19:25:13 +08:00
} , {
Name : "key_use_agent" ,
Help : ` When set forces the usage of the ssh - agent .
When key - file is also set , the ".pub" file of the specified key - file is read and only the associated key is
requested from the ssh - agent . This allows to avoid ` + " ` Too many authentication failures for * username * ` " + ` errors
when the ssh - agent contains many keys . ` ,
Default : false ,
2017-12-08 20:22:09 +08:00
} , {
2018-05-15 01:06:57 +08:00
Name : "use_insecure_cipher" ,
Help : "Enable the use of the aes128-cbc cipher. This cipher is insecure and may allow plaintext data to be recovered by an attacker." ,
Default : false ,
2017-12-08 20:22:09 +08:00
Examples : [ ] fs . OptionExample {
{
Value : "false" ,
Help : "Use default Cipher list." ,
} , {
Value : "true" ,
Help : "Enables the use of the aes128-cbc cipher." ,
} ,
} ,
2018-01-05 17:01:35 +08:00
} , {
2018-05-15 01:06:57 +08:00
Name : "disable_hashcheck" ,
Default : false ,
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." ,
} , {
Name : "ask_password" ,
Default : false ,
Help : "Allow asking for SFTP password when needed." ,
Advanced : true ,
} , {
2018-10-02 01:36:15 +08:00
Name : "path_override" ,
Default : "" ,
Help : ` Override path used by SSH connection .
This allows checksum calculation when SFTP and SSH paths are
different . This issue affects among others Synology NAS boxes .
Shared folders can be found in directories representing volumes
rclone sync / home / local / directory remote : / directory -- ssh - path - override / volume2 / directory
Home directory can be found in a shared folder called "home"
rclone sync / home / local / directory remote : / home / directory -- ssh - path - override / volume1 / homes / USER / directory ` ,
2018-05-15 01:06:57 +08:00
Advanced : true ,
} , {
Name : "set_modtime" ,
Default : true ,
Help : "Set the modified time on the remote if set." ,
Advanced : true ,
2016-11-13 07:36:08 +08:00
} } ,
}
fs . Register ( fsi )
}
2018-05-15 01:06:57 +08:00
// Options defines the configuration for this backend
type Options struct {
Host string ` config:"host" `
User string ` config:"user" `
Port string ` config:"port" `
Pass string ` config:"pass" `
KeyFile string ` config:"key_file" `
2019-01-03 19:24:31 +08:00
KeyFilePass string ` config:"key_file_pass" `
2019-01-03 19:25:13 +08:00
KeyUseAgent bool ` config:"key_use_agent" `
2018-05-15 01:06:57 +08:00
UseInsecureCipher bool ` config:"use_insecure_cipher" `
DisableHashCheck bool ` config:"disable_hashcheck" `
AskPassword bool ` config:"ask_password" `
PathOverride string ` config:"path_override" `
SetModTime bool ` config:"set_modtime" `
}
2016-11-13 07:36:08 +08:00
// Fs stores the interface to the remote SFTP files
type Fs struct {
2018-05-15 01:06:57 +08:00
name string
root string
opt Options // parsed options
features * fs . Features // optional features
config * ssh . ClientConfig
url string
mkdirLock * stringLock
cachedHashes * hash . Set
poolMu sync . Mutex
pool [ ] * conn
connLimit * rate . Limiter // for limiting number of connections per second
2016-11-13 07:36:08 +08:00
}
// Object is a remote SFTP file that has been stat'd (so it exists, but is not necessarily open for reading)
type Object struct {
2017-06-30 17:24:06 +08:00
fs * Fs
remote string
size int64 // size of the object
modTime time . Time // modification time of the object
mode os . FileMode // mode bits from the file
2017-08-06 18:49:52 +08:00
md5sum * string // Cached MD5 checksum
sha1sum * string // Cached SHA1 checksum
2016-11-13 07:36:08 +08:00
}
2018-01-07 20:57:46 +08:00
// readCurrentUser finds the current user name or "" if not found
func readCurrentUser ( ) ( userName string ) {
usr , err := user . Current ( )
if err == nil {
return usr . Username
}
2018-01-09 05:39:16 +08:00
// Fall back to reading $USER then $LOGNAME
userName = os . Getenv ( "USER" )
2018-01-07 20:57:46 +08:00
if userName != "" {
return userName
}
return os . Getenv ( "LOGNAME" )
}
2017-07-23 23:10:23 +08:00
// Dial starts a client connection to the given SSH server. It is a
// convenience function that connects to the given network address,
// initiates the SSH handshake, and then sets up a Client.
2018-01-13 00:30:54 +08:00
func Dial ( network , addr string , sshConfig * ssh . ClientConfig ) ( * ssh . Client , error ) {
dialer := fshttp . NewDialer ( fs . Config )
2017-07-23 23:10:23 +08:00
conn , err := dialer . Dial ( network , addr )
if err != nil {
return nil , err
}
2018-01-13 00:30:54 +08:00
c , chans , reqs , err := ssh . NewClientConn ( conn , addr , sshConfig )
2017-07-23 23:10:23 +08:00
if err != nil {
return nil , err
}
return ssh . NewClient ( c , chans , reqs ) , nil
}
2017-08-08 00:19:37 +08:00
// conn encapsulates an ssh client and corresponding sftp client
type conn struct {
sshClient * ssh . Client
sftpClient * sftp . Client
err chan error
}
// Wait for connection to close
func ( c * conn ) wait ( ) {
c . err <- c . sshClient . Conn . Wait ( )
}
// Closes the connection
func ( c * conn ) close ( ) error {
sftpErr := c . sftpClient . Close ( )
sshErr := c . sshClient . Close ( )
if sftpErr != nil {
return sftpErr
}
return sshErr
}
// Returns an error if closed
func ( c * conn ) closed ( ) error {
select {
case err := <- c . err :
return err
default :
}
return nil
}
// Open a new connection to the SFTP server.
func ( f * Fs ) sftpConnection ( ) ( c * conn , err error ) {
2017-08-08 01:01:31 +08:00
// Rate limit rate of new connections
err = f . connLimit . Wait ( context . Background ( ) )
if err != nil {
return nil , errors . Wrap ( err , "limiter failed in connect" )
}
2017-08-08 00:19:37 +08:00
c = & conn {
err : make ( chan error , 1 ) ,
}
2018-05-15 01:06:57 +08:00
c . sshClient , err = Dial ( "tcp" , f . opt . Host + ":" + f . opt . Port , f . config )
2017-08-08 00:19:37 +08:00
if err != nil {
return nil , errors . Wrap ( err , "couldn't connect SSH" )
}
c . sftpClient , err = sftp . NewClient ( c . sshClient )
if err != nil {
_ = c . sshClient . Close ( )
return nil , errors . Wrap ( err , "couldn't initialise SFTP" )
}
go c . wait ( )
return c , nil
}
// Get an SFTP connection from the pool, or open a new one
func ( f * Fs ) getSftpConnection ( ) ( c * conn , err error ) {
f . poolMu . Lock ( )
for len ( f . pool ) > 0 {
c = f . pool [ 0 ]
f . pool = f . pool [ 1 : ]
err := c . closed ( )
if err == nil {
break
}
fs . Errorf ( f , "Discarding closed SSH connection: %v" , err )
c = nil
}
f . poolMu . Unlock ( )
if c != nil {
return c , nil
}
return f . sftpConnection ( )
}
// Return an SFTP connection to the pool
//
// It nils the pointed to connection out so it can't be reused
//
// if err is not nil then it checks the connection is alive using a
// Getwd request
func ( f * Fs ) putSftpConnection ( pc * * conn , err error ) {
c := * pc
* pc = nil
if err != nil {
// work out if this is an expected error
underlyingErr := errors . Cause ( err )
isRegularError := false
switch underlyingErr {
case os . ErrNotExist :
isRegularError = true
default :
switch underlyingErr . ( type ) {
case * sftp . StatusError , * os . PathError :
isRegularError = true
}
}
// If not a regular SFTP error code then check the connection
if ! isRegularError {
_ , nopErr := c . sftpClient . Getwd ( )
if nopErr != nil {
fs . Debugf ( f , "Connection failed, closing: %v" , nopErr )
_ = c . close ( )
return
}
fs . Debugf ( f , "Connection OK after error: %v" , err )
}
}
f . poolMu . Lock ( )
f . pool = append ( f . pool , c )
f . poolMu . Unlock ( )
}
2019-01-03 20:42:13 +08:00
// shellExpand replaces a leading "~" with "${HOME}" and expands all environment
// variables afterwards.
func shellExpand ( s string ) string {
if s != "" {
if s [ 0 ] == '~' {
s = "${HOME}" + s [ 1 : ]
}
s = os . ExpandEnv ( s )
}
return s
}
2016-11-13 07:36:08 +08:00
// NewFs creates a new Fs object from the name and root. It connects to
// the host specified in the config file.
2018-05-15 01:06:57 +08:00
func NewFs ( name , root string , m configmap . Mapper ) ( fs . Fs , error ) {
// Parse config into Options struct
opt := new ( Options )
err := configstruct . Set ( m , opt )
if err != nil {
return nil , err
}
if opt . User == "" {
opt . User = currentUser
}
if opt . Port == "" {
opt . Port = "22"
2016-11-13 07:36:08 +08:00
}
2018-01-13 00:30:54 +08:00
sshConfig := & ssh . ClientConfig {
2018-05-15 01:06:57 +08:00
User : opt . User ,
2017-05-15 21:00:07 +08:00
Auth : [ ] ssh . AuthMethod { } ,
HostKeyCallback : ssh . InsecureIgnoreHostKey ( ) ,
Timeout : fs . Config . ConnectTimeout ,
2016-11-13 07:36:08 +08:00
}
2017-06-23 23:25:35 +08:00
2018-05-15 01:06:57 +08:00
if opt . UseInsecureCipher {
2018-01-13 00:30:54 +08:00
sshConfig . Config . SetDefaults ( )
sshConfig . Config . Ciphers = append ( sshConfig . Config . Ciphers , "aes128-cbc" )
2017-12-08 20:22:09 +08:00
}
2019-01-03 20:42:13 +08:00
keyFile := shellExpand ( opt . KeyFile )
2017-06-23 23:25:35 +08:00
// Add ssh agent-auth if no password or file specified
2019-01-03 20:42:13 +08:00
if ( opt . Pass == "" && keyFile == "" ) || opt . KeyUseAgent {
2017-04-10 21:50:06 +08:00
sshAgentClient , _ , err := sshagent . New ( )
2017-02-01 04:34:11 +08:00
if err != nil {
return nil , errors . Wrap ( err , "couldn't connect to ssh-agent" )
}
signers , err := sshAgentClient . Signers ( )
if err != nil {
return nil , errors . Wrap ( err , "couldn't read ssh agent signers" )
}
2019-01-03 20:42:13 +08:00
if keyFile != "" {
pubBytes , err := ioutil . ReadFile ( keyFile + ".pub" )
2019-01-03 19:25:13 +08:00
if err != nil {
return nil , errors . Wrap ( err , "failed to read public key file" )
}
pub , _ , _ , _ , err := ssh . ParseAuthorizedKey ( pubBytes )
if err != nil {
return nil , errors . Wrap ( err , "failed to parse public key file" )
}
pubM := pub . Marshal ( )
found := false
for _ , s := range signers {
if bytes . Equal ( pubM , s . PublicKey ( ) . Marshal ( ) ) {
sshConfig . Auth = append ( sshConfig . Auth , ssh . PublicKeys ( s ) )
found = true
break
}
}
if ! found {
return nil , errors . New ( "private key not found in the ssh-agent" )
}
} else {
sshConfig . Auth = append ( sshConfig . Auth , ssh . PublicKeys ( signers ... ) )
}
2017-06-23 23:25:35 +08:00
}
// Load key file if specified
2019-01-03 20:42:13 +08:00
if keyFile != "" {
key , err := ioutil . ReadFile ( keyFile )
2017-06-23 23:25:35 +08:00
if err != nil {
return nil , errors . Wrap ( err , "failed to read private key file" )
}
2019-01-03 19:24:31 +08:00
clearpass := ""
if opt . KeyFilePass != "" {
clearpass , err = obscure . Reveal ( opt . KeyFilePass )
if err != nil {
return nil , err
}
}
signer , err := ssh . ParsePrivateKeyWithPassphrase ( key , [ ] byte ( clearpass ) )
2017-06-23 23:25:35 +08:00
if err != nil {
return nil , errors . Wrap ( err , "failed to parse private key file" )
}
2018-01-13 00:30:54 +08:00
sshConfig . Auth = append ( sshConfig . Auth , ssh . PublicKeys ( signer ) )
2017-06-23 23:25:35 +08:00
}
// Auth from password if specified
2018-05-15 01:06:57 +08:00
if opt . Pass != "" {
clearpass , err := obscure . Reveal ( opt . Pass )
2016-11-13 07:36:08 +08:00
if err != nil {
return nil , err
}
2018-01-13 00:30:54 +08:00
sshConfig . Auth = append ( sshConfig . Auth , ssh . Password ( clearpass ) )
2016-11-13 07:36:08 +08:00
}
2017-06-23 23:25:35 +08:00
2018-03-15 07:17:09 +08:00
// Ask for password if none was defined and we're allowed to
2018-05-15 01:06:57 +08:00
if opt . Pass == "" && opt . AskPassword {
2018-05-22 16:41:13 +08:00
_ , _ = fmt . Fprint ( os . Stderr , "Enter SFTP password: " )
2018-03-15 07:17:09 +08:00
clearpass := config . ReadPassword ( )
sshConfig . Auth = append ( sshConfig . Auth , ssh . Password ( clearpass ) )
}
2019-02-20 00:40:15 +08:00
return NewFsWithConnection ( name , root , opt , sshConfig )
}
// NewFsWithConnection creates a new Fs object from the name and root and a ssh.ClientConfig. It connects to
// the host specified in the ssh.ClientConfig
func NewFsWithConnection ( name string , root string , opt * Options , sshConfig * ssh . ClientConfig ) ( fs . Fs , error ) {
2017-02-01 04:34:11 +08:00
f := & Fs {
2018-05-15 01:06:57 +08:00
name : name ,
root : root ,
opt : * opt ,
config : sshConfig ,
url : "sftp://" + opt . User + "@" + opt . Host + ":" + opt . Port + "/" + root ,
mkdirLock : newStringLock ( ) ,
connLimit : rate . NewLimiter ( rate . Limit ( connectionsPerSecond ) , 1 ) ,
2017-02-01 04:34:11 +08:00
}
2017-08-09 22:27:43 +08:00
f . features = ( & fs . Features {
CanHaveEmptyDirectories : true ,
} ) . Fill ( f )
2017-08-08 00:19:37 +08:00
// Make a connection and pool it to return errors early
c , err := f . getSftpConnection ( )
if err != nil {
return nil , errors . Wrap ( err , "NewFs" )
}
f . putSftpConnection ( & c , nil )
2017-02-01 04:34:11 +08:00
if root != "" {
// Check to see if the root actually an existing file
remote := path . Base ( root )
f . root = path . Dir ( root )
if f . root == "." {
f . root = ""
}
_ , err := f . NewObject ( remote )
if err != nil {
2017-02-25 22:31:27 +08:00
if err == fs . ErrorObjectNotFound || errors . Cause ( err ) == fs . ErrorNotAFile {
2017-02-01 04:34:11 +08:00
// File doesn't exist so return old f
f . root = root
return f , nil
}
return nil , err
2016-11-13 07:36:08 +08:00
}
2017-02-01 04:34:11 +08:00
// return an error with an fs which points to the parent
return f , fs . ErrorIsFile
2016-11-13 07:36:08 +08:00
}
2017-02-01 04:34:11 +08:00
return f , nil
2016-11-13 07:36:08 +08:00
}
// Name returns the configured name of the file system
func ( f * Fs ) Name ( ) string {
return f . name
}
// Root returns the root for the filesystem
func ( f * Fs ) Root ( ) string {
return f . root
}
// String returns the URL for the filesystem
func ( f * Fs ) String ( ) string {
return f . url
}
// Features returns the optional features of this Fs
func ( f * Fs ) Features ( ) * fs . Features {
return f . features
}
// Precision is the remote sftp file system's modtime precision, which we have no way of knowing. We estimate at 1s
func ( f * Fs ) Precision ( ) time . Duration {
return time . Second
}
// NewObject creates a new remote sftp file object
func ( f * Fs ) NewObject ( remote string ) ( fs . Object , error ) {
2017-02-01 04:34:11 +08:00
o := & Object {
2016-11-13 07:36:08 +08:00
fs : f ,
remote : remote ,
}
2017-02-01 04:34:11 +08:00
err := o . stat ( )
if err != nil {
return nil , err
}
return o , nil
2016-11-13 07:36:08 +08:00
}
2017-02-01 04:34:11 +08:00
// dirExists returns true,nil if the directory exists, false, nil if
// it doesn't or false, err
func ( f * Fs ) dirExists ( dir string ) ( bool , error ) {
if dir == "" {
dir = "."
}
2017-08-08 00:19:37 +08:00
c , err := f . getSftpConnection ( )
if err != nil {
return false , errors . Wrap ( err , "dirExists" )
}
info , err := c . sftpClient . Stat ( dir )
f . putSftpConnection ( & c , err )
2017-02-01 04:34:11 +08:00
if err != nil {
if os . IsNotExist ( err ) {
return false , nil
}
return false , errors . Wrap ( err , "dirExists stat failed" )
}
if ! info . IsDir ( ) {
return false , fs . ErrorIsFile
}
return true , nil
}
2017-06-12 05:43:31 +08:00
// List the objects and directories in dir into entries. The
// entries can be returned in any order but should be for a
// complete directory.
//
// dir should be "" to list the root, and should not have
// trailing slashes.
//
// This should return ErrDirNotFound if the directory isn't
// found.
func ( f * Fs ) List ( dir string ) ( entries fs . DirEntries , err error ) {
root := path . Join ( f . root , dir )
ok , err := f . dirExists ( root )
if err != nil {
return nil , errors . Wrap ( err , "List failed" )
}
if ! ok {
return nil , fs . ErrorDirNotFound
}
sftpDir := root
2017-02-01 04:34:11 +08:00
if sftpDir == "" {
sftpDir = "."
}
2017-08-08 00:19:37 +08:00
c , err := f . getSftpConnection ( )
if err != nil {
return nil , errors . Wrap ( err , "List" )
}
infos , err := c . sftpClient . ReadDir ( sftpDir )
f . putSftpConnection ( & c , err )
2017-02-01 04:34:11 +08:00
if err != nil {
2017-06-12 05:43:31 +08:00
return nil , errors . Wrapf ( err , "error listing %q" , dir )
2017-02-01 04:34:11 +08:00
}
for _ , info := range infos {
remote := path . Join ( dir , info . Name ( ) )
2018-03-16 23:36:47 +08:00
// If file is a symlink (not a regular file is the best cross platform test we can do), do a stat to
// pick up the size and type of the destination, instead of the size and type of the symlink.
if ! info . Mode ( ) . IsRegular ( ) {
2019-01-29 01:10:00 +08:00
oldInfo := info
2018-03-16 23:36:47 +08:00
info , err = f . stat ( remote )
if err != nil {
2019-01-29 01:10:00 +08:00
if ! os . IsNotExist ( err ) {
fs . Errorf ( remote , "stat of non-regular file/dir failed: %v" , err )
}
info = oldInfo
2018-03-16 23:36:47 +08:00
}
}
2017-02-01 04:34:11 +08:00
if info . IsDir ( ) {
2017-06-30 20:37:29 +08:00
d := fs . NewDir ( remote , info . ModTime ( ) )
2017-06-12 05:43:31 +08:00
entries = append ( entries , d )
2017-02-01 04:34:11 +08:00
} else {
2017-06-12 05:43:31 +08:00
o := & Object {
2017-02-01 04:34:11 +08:00
fs : f ,
remote : remote ,
}
2017-06-30 17:24:06 +08:00
o . setMetadata ( info )
2017-06-12 05:43:31 +08:00
entries = append ( entries , o )
2016-11-13 07:36:08 +08:00
}
}
2017-06-12 05:43:31 +08:00
return entries , nil
2016-11-13 07:36:08 +08:00
}
// Put data from <in> into a new remote sftp file object described by <src.Remote()> and <src.ModTime()>
2017-05-28 19:44:22 +08:00
func ( f * Fs ) Put ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
2017-02-01 04:34:11 +08:00
err := f . mkParentDir ( src . Remote ( ) )
2016-11-13 07:36:08 +08:00
if err != nil {
2017-02-01 04:34:11 +08:00
return nil , errors . Wrap ( err , "Put mkParentDir failed" )
2016-11-13 07:36:08 +08:00
}
2017-02-01 04:34:11 +08:00
// Temporary object under construction
o := & Object {
fs : f ,
remote : src . Remote ( ) ,
2016-11-13 07:36:08 +08:00
}
2017-05-28 19:44:22 +08:00
err = o . Update ( in , src , options ... )
2016-11-13 07:36:08 +08:00
if err != nil {
return nil , err
}
return o , nil
}
2017-08-04 03:42:35 +08:00
// PutStream uploads to the remote path with the modTime given of indeterminate size
func ( f * Fs ) PutStream ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) ( fs . Object , error ) {
return f . Put ( in , src , options ... )
}
2017-02-01 04:34:11 +08:00
// mkParentDir makes the parent of remote if necessary and any
// directories above that
func ( f * Fs ) mkParentDir ( remote string ) error {
parent := path . Dir ( remote )
return f . mkdir ( path . Join ( f . root , parent ) )
}
// mkdir makes the directory and parents using native paths
2017-05-24 22:39:17 +08:00
func ( f * Fs ) mkdir ( dirPath string ) error {
f . mkdirLock . Lock ( dirPath )
defer f . mkdirLock . Unlock ( dirPath )
if dirPath == "." || dirPath == "/" {
2017-02-01 04:34:11 +08:00
return nil
}
2017-05-24 22:39:17 +08:00
ok , err := f . dirExists ( dirPath )
2017-02-01 04:34:11 +08:00
if err != nil {
return errors . Wrap ( err , "mkdir dirExists failed" )
}
if ok {
return nil
}
2017-05-24 22:39:17 +08:00
parent := path . Dir ( dirPath )
2017-02-01 04:34:11 +08:00
err = f . mkdir ( parent )
if err != nil {
return err
}
2017-08-08 00:19:37 +08:00
c , err := f . getSftpConnection ( )
if err != nil {
return errors . Wrap ( err , "mkdir" )
}
err = c . sftpClient . Mkdir ( dirPath )
f . putSftpConnection ( & c , err )
2017-02-01 04:34:11 +08:00
if err != nil {
2017-05-24 22:39:17 +08:00
return errors . Wrapf ( err , "mkdir %q failed" , dirPath )
2016-11-13 07:36:08 +08:00
}
2017-02-01 04:34:11 +08:00
return nil
2016-11-13 07:36:08 +08:00
}
// Mkdir makes the root directory of the Fs object
func ( f * Fs ) Mkdir ( dir string ) error {
root := path . Join ( f . root , dir )
2017-02-01 04:34:11 +08:00
return f . mkdir ( root )
2016-11-13 07:36:08 +08:00
}
// Rmdir removes the root directory of the Fs object
func ( f * Fs ) Rmdir ( dir string ) error {
2018-12-01 01:37:55 +08:00
// Check to see if directory is empty as some servers will
// delete recursively with RemoveDirectory
entries , err := f . List ( dir )
if err != nil {
return errors . Wrap ( err , "Rmdir" )
}
if len ( entries ) != 0 {
return fs . ErrorDirectoryNotEmpty
}
// Remove the directory
2016-11-13 07:36:08 +08:00
root := path . Join ( f . root , dir )
2017-08-08 00:19:37 +08:00
c , err := f . getSftpConnection ( )
if err != nil {
return errors . Wrap ( err , "Rmdir" )
}
2018-11-30 05:34:37 +08:00
err = c . sftpClient . RemoveDirectory ( root )
2017-08-08 00:19:37 +08:00
f . putSftpConnection ( & c , err )
return err
2016-11-13 07:36:08 +08:00
}
// Move renames a remote sftp file object
func ( f * Fs ) Move ( src fs . Object , remote string ) ( fs . Object , error ) {
2017-02-01 04:34:11 +08:00
srcObj , ok := src . ( * Object )
if ! ok {
2017-02-09 19:01:20 +08:00
fs . Debugf ( src , "Can't move - not same remote type" )
2017-02-01 04:34:11 +08:00
return nil , fs . ErrorCantMove
}
err := f . mkParentDir ( remote )
2016-11-13 07:36:08 +08:00
if err != nil {
2017-02-01 04:34:11 +08:00
return nil , errors . Wrap ( err , "Move mkParentDir failed" )
}
2017-08-08 00:19:37 +08:00
c , err := f . getSftpConnection ( )
if err != nil {
return nil , errors . Wrap ( err , "Move" )
}
err = c . sftpClient . Rename (
2017-02-01 04:34:11 +08:00
srcObj . path ( ) ,
path . Join ( f . root , remote ) ,
)
2017-08-08 00:19:37 +08:00
f . putSftpConnection ( & c , err )
2017-02-01 04:34:11 +08:00
if err != nil {
return nil , errors . Wrap ( err , "Move Rename failed" )
2016-11-13 07:36:08 +08:00
}
dstObj , err := f . NewObject ( remote )
2017-02-01 04:34:11 +08:00
if err != nil {
return nil , errors . Wrap ( err , "Move NewObject failed" )
}
return dstObj , nil
}
2017-02-06 05:20:56 +08:00
// DirMove moves src, srcRemote to this remote at dstRemote
// using server side move operations.
2017-02-01 04:34:11 +08:00
//
// Will only be called if src.Fs().Name() == f.Name()
//
// If it isn't possible then return fs.ErrorCantDirMove
//
// If destination exists then return fs.ErrorDirExists
2017-02-06 05:20:56 +08:00
func ( f * Fs ) DirMove ( src fs . Fs , srcRemote , dstRemote string ) error {
2017-02-01 04:34:11 +08:00
srcFs , ok := src . ( * Fs )
if ! ok {
2017-02-09 19:01:20 +08:00
fs . Debugf ( srcFs , "Can't move directory - not same remote type" )
2017-02-01 04:34:11 +08:00
return fs . ErrorCantDirMove
}
2017-02-06 05:20:56 +08:00
srcPath := path . Join ( srcFs . root , srcRemote )
dstPath := path . Join ( f . root , dstRemote )
2017-02-01 04:34:11 +08:00
// Check if destination exists
2017-02-06 05:20:56 +08:00
ok , err := f . dirExists ( dstPath )
2017-02-01 04:34:11 +08:00
if err != nil {
return errors . Wrap ( err , "DirMove dirExists dst failed" )
}
if ok {
return fs . ErrorDirExists
}
// Make sure the parent directory exists
2017-02-06 05:20:56 +08:00
err = f . mkdir ( path . Dir ( dstPath ) )
2017-02-01 04:34:11 +08:00
if err != nil {
2017-02-06 05:20:56 +08:00
return errors . Wrap ( err , "DirMove mkParentDir dst failed" )
2017-02-01 04:34:11 +08:00
}
// Do the move
2017-08-08 00:19:37 +08:00
c , err := f . getSftpConnection ( )
if err != nil {
return errors . Wrap ( err , "DirMove" )
}
err = c . sftpClient . Rename (
2017-02-06 05:20:56 +08:00
srcPath ,
dstPath ,
2017-02-01 04:34:11 +08:00
)
2017-08-08 00:19:37 +08:00
f . putSftpConnection ( & c , err )
2017-02-01 04:34:11 +08:00
if err != nil {
2017-02-06 05:20:56 +08:00
return errors . Wrapf ( err , "DirMove Rename(%q,%q) failed" , srcPath , dstPath )
2017-02-01 04:34:11 +08:00
}
return nil
2016-11-13 07:36:08 +08:00
}
2017-08-07 21:50:31 +08:00
// Hashes returns the supported hash types of the filesystem
2018-01-13 00:30:54 +08:00
func ( f * Fs ) Hashes ( ) hash . Set {
2017-08-06 18:49:52 +08:00
if f . cachedHashes != nil {
return * f . cachedHashes
}
2018-05-15 01:06:57 +08:00
if f . opt . DisableHashCheck {
2018-01-19 04:27:52 +08:00
return hash . Set ( hash . None )
2018-01-05 17:01:35 +08:00
}
2017-08-08 00:19:37 +08:00
c , err := f . getSftpConnection ( )
if err != nil {
fs . Errorf ( f , "Couldn't get SSH connection to figure out Hashes: %v" , err )
2018-01-19 04:27:52 +08:00
return hash . Set ( hash . None )
2017-08-08 00:19:37 +08:00
}
defer f . putSftpConnection ( & c , err )
session , err := c . sshClient . NewSession ( )
2017-08-06 18:49:52 +08:00
if err != nil {
2018-01-19 04:27:52 +08:00
return hash . Set ( hash . None )
2017-08-06 18:49:52 +08:00
}
sha1Output , _ := session . Output ( "echo 'abc' | sha1sum" )
expectedSha1 := "03cfd743661f07975fa2f1220c5194cbaff48451"
_ = session . Close ( )
2017-08-08 00:19:37 +08:00
session , err = c . sshClient . NewSession ( )
2017-08-06 18:49:52 +08:00
if err != nil {
2018-01-19 04:27:52 +08:00
return hash . Set ( hash . None )
2017-08-06 18:49:52 +08:00
}
md5Output , _ := session . Output ( "echo 'abc' | md5sum" )
expectedMd5 := "0bee89b07a248e27c83fc3d5951213c1"
_ = session . Close ( )
sha1Works := parseHash ( sha1Output ) == expectedSha1
md5Works := parseHash ( md5Output ) == expectedMd5
2018-01-13 00:30:54 +08:00
set := hash . NewHashSet ( )
2017-08-06 18:49:52 +08:00
if ! sha1Works && ! md5Works {
2018-01-19 04:27:52 +08:00
set . Add ( hash . None )
2017-08-06 18:49:52 +08:00
}
if sha1Works {
2018-01-19 04:27:52 +08:00
set . Add ( hash . SHA1 )
2017-08-06 18:49:52 +08:00
}
if md5Works {
2018-01-19 04:27:52 +08:00
set . Add ( hash . MD5 )
2017-08-06 18:49:52 +08:00
}
_ = session . Close ( )
f . cachedHashes = & set
return set
2016-11-13 07:36:08 +08:00
}
// Fs is the filesystem this remote sftp file object is located within
func ( o * Object ) Fs ( ) fs . Info {
return o . fs
}
// String returns the URL to the remote SFTP file
func ( o * Object ) String ( ) string {
if o == nil {
return "<nil>"
}
2017-02-01 04:34:11 +08:00
return o . remote
2016-11-13 07:36:08 +08:00
}
// Remote the name of the remote SFTP file, relative to the fs root
func ( o * Object ) Remote ( ) string {
return o . remote
}
2017-08-07 21:50:31 +08:00
// Hash returns the selected checksum of the file
// If no checksum is available it returns ""
2018-01-13 00:30:54 +08:00
func ( o * Object ) Hash ( r hash . Type ) ( string , error ) {
2018-04-19 16:45:46 +08:00
var hashCmd string
if r == hash . MD5 {
if o . md5sum != nil {
return * o . md5sum , nil
}
hashCmd = "md5sum"
} else if r == hash . SHA1 {
if o . sha1sum != nil {
return * o . sha1sum , nil
}
hashCmd = "sha1sum"
} else {
return "" , hash . ErrUnsupported
2017-08-06 18:49:52 +08:00
}
2018-10-22 18:01:41 +08:00
if o . fs . opt . DisableHashCheck {
return "" , nil
}
2017-08-08 00:19:37 +08:00
c , err := o . fs . getSftpConnection ( )
if err != nil {
2018-04-19 16:45:46 +08:00
return "" , errors . Wrap ( err , "Hash get SFTP connection" )
2017-08-08 00:19:37 +08:00
}
session , err := c . sshClient . NewSession ( )
o . fs . putSftpConnection ( & c , err )
2017-08-06 18:49:52 +08:00
if err != nil {
2018-04-19 16:45:46 +08:00
return "" , errors . Wrap ( err , "Hash put SFTP connection" )
2017-08-06 18:49:52 +08:00
}
2018-04-19 16:45:46 +08:00
var stdout , stderr bytes . Buffer
session . Stdout = & stdout
session . Stderr = & stderr
2017-08-06 18:49:52 +08:00
escapedPath := shellEscape ( o . path ( ) )
2018-05-15 01:06:57 +08:00
if o . fs . opt . PathOverride != "" {
escapedPath = shellEscape ( path . Join ( o . fs . opt . PathOverride , o . remote ) )
2018-05-01 00:05:10 +08:00
}
2018-04-19 16:45:46 +08:00
err = session . Run ( hashCmd + " " + escapedPath )
2017-08-06 18:49:52 +08:00
if err != nil {
_ = session . Close ( )
2018-04-19 16:45:46 +08:00
fs . Debugf ( o , "Failed to calculate %v hash: %v (%s)" , r , err , bytes . TrimSpace ( stderr . Bytes ( ) ) )
return "" , nil
2017-08-06 18:49:52 +08:00
}
_ = session . Close ( )
2018-04-19 16:45:46 +08:00
str := parseHash ( stdout . Bytes ( ) )
2018-01-19 04:27:52 +08:00
if r == hash . MD5 {
2017-08-06 18:49:52 +08:00
o . md5sum = & str
2018-01-19 04:27:52 +08:00
} else if r == hash . SHA1 {
2017-08-06 18:49:52 +08:00
o . sha1sum = & str
}
return str , nil
}
var shellEscapeRegex = regexp . MustCompile ( ` [^A-Za-z0-9_.,:/@\n-] ` )
// Escape a string s.t. it cannot cause unintended behavior
// when sending it to a shell.
func shellEscape ( str string ) string {
safe := shellEscapeRegex . ReplaceAllString ( str , ` \$0 ` )
return strings . Replace ( safe , "\n" , "'\n'" , - 1 )
}
// Converts a byte array from the SSH session returned by
// an invocation of md5sum/sha1sum to a hash string
// as expected by the rest of this application
func parseHash ( bytes [ ] byte ) string {
return strings . Split ( string ( bytes ) , " " ) [ 0 ] // Split at hash / filename separator
2016-11-13 07:36:08 +08:00
}
// Size returns the size in bytes of the remote sftp file
func ( o * Object ) Size ( ) int64 {
2017-06-30 17:24:06 +08:00
return o . size
2016-11-13 07:36:08 +08:00
}
// ModTime returns the modification time of the remote sftp file
func ( o * Object ) ModTime ( ) time . Time {
2017-06-30 17:24:06 +08:00
return o . modTime
2016-11-13 07:36:08 +08:00
}
2017-02-01 04:34:11 +08:00
// path returns the native path of the object
func ( o * Object ) path ( ) string {
return path . Join ( o . fs . root , o . remote )
}
2017-06-30 17:24:06 +08:00
// setMetadata updates the info in the object from the stat result passed in
func ( o * Object ) setMetadata ( info os . FileInfo ) {
o . modTime = info . ModTime ( )
o . size = info . Size ( )
o . mode = info . Mode ( )
}
2018-03-16 23:36:47 +08:00
// statRemote stats the file or directory at the remote given
func ( f * Fs ) stat ( remote string ) ( info os . FileInfo , err error ) {
c , err := f . getSftpConnection ( )
2017-08-08 00:19:37 +08:00
if err != nil {
2018-03-16 23:36:47 +08:00
return nil , errors . Wrap ( err , "stat" )
2017-08-08 00:19:37 +08:00
}
2018-03-16 23:36:47 +08:00
absPath := path . Join ( f . root , remote )
info , err = c . sftpClient . Stat ( absPath )
f . putSftpConnection ( & c , err )
return info , err
}
// stat updates the info in the Object
func ( o * Object ) stat ( ) error {
info , err := o . fs . stat ( o . remote )
2017-02-01 04:34:11 +08:00
if err != nil {
if os . IsNotExist ( err ) {
return fs . ErrorObjectNotFound
}
return errors . Wrap ( err , "stat failed" )
}
if info . IsDir ( ) {
2017-02-25 19:09:57 +08:00
return errors . Wrapf ( fs . ErrorNotAFile , "%q" , o . remote )
2017-02-01 04:34:11 +08:00
}
2017-06-30 17:24:06 +08:00
o . setMetadata ( info )
2017-02-01 04:34:11 +08:00
return nil
}
2016-11-13 07:36:08 +08:00
// SetModTime sets the modification and access time to the specified time
2017-02-01 04:34:11 +08:00
//
// it also updates the info field
2016-11-13 07:36:08 +08:00
func ( o * Object ) SetModTime ( modTime time . Time ) error {
2017-08-08 00:19:37 +08:00
c , err := o . fs . getSftpConnection ( )
if err != nil {
return errors . Wrap ( err , "SetModTime" )
}
2018-05-15 01:06:57 +08:00
if o . fs . opt . SetModTime {
2018-01-04 22:52:47 +08:00
err = c . sftpClient . Chtimes ( o . path ( ) , modTime , modTime )
o . fs . putSftpConnection ( & c , err )
if err != nil {
return errors . Wrap ( err , "SetModTime failed" )
}
2016-11-13 07:36:08 +08:00
}
2017-02-01 04:34:11 +08:00
err = o . stat ( )
if err != nil {
2018-01-04 22:52:47 +08:00
return errors . Wrap ( err , "SetModTime stat failed" )
2017-02-01 04:34:11 +08:00
}
return nil
2016-11-13 07:36:08 +08:00
}
// Storable returns whether the remote sftp file is a regular file (not a directory, symbolic link, block device, character device, named pipe, etc)
func ( o * Object ) Storable ( ) bool {
2017-06-30 17:24:06 +08:00
return o . mode . IsRegular ( )
2016-11-13 07:36:08 +08:00
}
2018-05-24 22:03:57 +08:00
// objectReader represents a file open for reading on the SFTP server
type objectReader struct {
sftpFile * sftp . File
pipeReader * io . PipeReader
done chan struct { }
}
func newObjectReader ( sftpFile * sftp . File ) * objectReader {
pipeReader , pipeWriter := io . Pipe ( )
file := & objectReader {
sftpFile : sftpFile ,
pipeReader : pipeReader ,
done : make ( chan struct { } ) ,
}
go func ( ) {
// Use sftpFile.WriteTo to pump data so that it gets a
// chance to build the window up.
_ , err := sftpFile . WriteTo ( pipeWriter )
// Close the pipeWriter so the pipeReader fails with
// the same error or EOF if err == nil
_ = pipeWriter . CloseWithError ( err )
// signal that we've finished
close ( file . done )
} ( )
return file
}
2016-11-13 07:36:08 +08:00
// Read from a remote sftp file object reader
2018-05-24 22:03:57 +08:00
func ( file * objectReader ) Read ( p [ ] byte ) ( n int , err error ) {
n , err = file . pipeReader . Read ( p )
2016-11-13 07:36:08 +08:00
return n , err
}
// Close a reader of a remote sftp file
2018-05-24 22:03:57 +08:00
func ( file * objectReader ) Close ( ) ( err error ) {
// Close the sftpFile - this will likely cause the WriteTo to error
2016-11-13 07:36:08 +08:00
err = file . sftpFile . Close ( )
2018-05-24 22:03:57 +08:00
// Close the pipeReader so writes to the pipeWriter fail
_ = file . pipeReader . Close ( )
// Wait for the background process to finish
<- file . done
2016-11-13 07:36:08 +08:00
return err
}
// Open a remote sftp file object for reading. Seek is supported
func ( o * Object ) Open ( options ... fs . OpenOption ) ( in io . ReadCloser , err error ) {
2018-01-27 18:07:17 +08:00
var offset , limit int64 = 0 , - 1
2016-11-13 07:36:08 +08:00
for _ , option := range options {
switch x := option . ( type ) {
case * fs . SeekOption :
2018-01-27 18:07:17 +08:00
offset = x . Offset
2018-01-24 04:21:19 +08:00
case * fs . RangeOption :
offset , limit = x . Decode ( o . Size ( ) )
2016-11-13 07:36:08 +08:00
default :
if option . Mandatory ( ) {
2017-02-09 19:01:20 +08:00
fs . Logf ( o , "Unsupported mandatory option: %v" , option )
2016-11-13 07:36:08 +08:00
}
}
}
2017-08-08 00:19:37 +08:00
c , err := o . fs . getSftpConnection ( )
if err != nil {
return nil , errors . Wrap ( err , "Open" )
}
sftpFile , err := c . sftpClient . Open ( o . path ( ) )
o . fs . putSftpConnection ( & c , err )
2016-11-13 07:36:08 +08:00
if err != nil {
2017-02-01 04:34:11 +08:00
return nil , errors . Wrap ( err , "Open failed" )
2016-11-13 07:36:08 +08:00
}
if offset > 0 {
2018-04-07 02:53:06 +08:00
off , err := sftpFile . Seek ( offset , io . SeekStart )
2016-11-13 07:36:08 +08:00
if err != nil || off != offset {
2017-02-01 04:34:11 +08:00
return nil , errors . Wrap ( err , "Open Seek failed" )
2016-11-13 07:36:08 +08:00
}
}
2018-05-24 22:03:57 +08:00
in = readers . NewLimitedReadCloser ( newObjectReader ( sftpFile ) , limit )
2016-11-13 07:36:08 +08:00
return in , nil
}
// Update a remote sftp file using the data <in> and ModTime from <src>
2017-05-28 19:44:22 +08:00
func ( o * Object ) Update ( in io . Reader , src fs . ObjectInfo , options ... fs . OpenOption ) error {
2017-08-08 00:36:59 +08:00
// Clear the hash cache since we are about to update the object
o . md5sum = nil
o . sha1sum = nil
2017-08-08 00:19:37 +08:00
c , err := o . fs . getSftpConnection ( )
if err != nil {
return errors . Wrap ( err , "Update" )
}
file , err := c . sftpClient . Create ( o . path ( ) )
o . fs . putSftpConnection ( & c , err )
2017-02-01 04:34:11 +08:00
if err != nil {
return errors . Wrap ( err , "Update Create failed" )
}
// remove the file if upload failed
remove := func ( ) {
2017-08-08 00:19:37 +08:00
c , removeErr := o . fs . getSftpConnection ( )
if removeErr != nil {
fs . Debugf ( src , "Failed to open new SSH connection for delete: %v" , removeErr )
return
}
removeErr = c . sftpClient . Remove ( o . path ( ) )
o . fs . putSftpConnection ( & c , removeErr )
2017-02-01 04:34:11 +08:00
if removeErr != nil {
2017-02-09 19:01:20 +08:00
fs . Debugf ( src , "Failed to remove: %v" , removeErr )
2017-02-01 04:34:11 +08:00
} else {
2017-02-09 19:01:20 +08:00
fs . Debugf ( src , "Removed after failed upload: %v" , err )
2016-11-13 07:36:08 +08:00
}
}
2017-02-01 04:34:11 +08:00
_ , err = file . ReadFrom ( in )
if err != nil {
remove ( )
return errors . Wrap ( err , "Update ReadFrom failed" )
}
err = file . Close ( )
if err != nil {
remove ( )
return errors . Wrap ( err , "Update Close failed" )
}
err = o . SetModTime ( src . ModTime ( ) )
if err != nil {
return errors . Wrap ( err , "Update SetModTime failed" )
}
return nil
2016-11-13 07:36:08 +08:00
}
// Remove a remote sftp file object
func ( o * Object ) Remove ( ) error {
2017-08-08 00:19:37 +08:00
c , err := o . fs . getSftpConnection ( )
if err != nil {
return errors . Wrap ( err , "Remove" )
}
err = c . sftpClient . Remove ( o . path ( ) )
o . fs . putSftpConnection ( & c , err )
return err
2016-11-13 07:36:08 +08:00
}
// Check the interfaces are satisfied
var (
2017-08-04 03:42:35 +08:00
_ fs . Fs = & Fs { }
_ fs . PutStreamer = & Fs { }
_ fs . Mover = & Fs { }
_ fs . DirMover = & Fs { }
_ fs . Object = & Object { }
2016-11-13 07:36:08 +08:00
)