import { TransformCallback } from 'stream' ; import fs from 'fs' ; import { LooseObject , BalmError } from '@balm-core/index' ; interface SshConfig { host : string ; port : number ; username : string ; password ? : string ; agent ? : string ; agentForward ? : boolean ; privateKey ? : string | Buffer ; passphrase ? : string ; readyTimeout ? : number ; } const PLUGIN_NAME = 'sftp' ; const normalizePath = ( _path ) : string => _path . replace ( /\\/g , '/' ) ; function resolveHomePath ( key ) { if ( key . location ) { const HOME = process . env . HOME || process . env . USERPROFILE || '' ; for ( let i = 0 , len = key . location . length ; i < len ; i ++ ) { if ( key . location [ i ] . substr ( 0 , 2 ) === '~/' ) { key . location [ i ] = path . resolve ( HOME , key . location [ i ] . replace ( /^~\// , '' ) ) ; } } for ( let i = 0 , keyPath ; ( keyPath = key . location [ i ++ ] ) ; ) { if ( fs . existsSync ( keyPath ) ) { key . contents = fs . readFileSync ( keyPath ) ; break ; } } } else if ( ! key . contents ) { const rsaKey = key . location . join ( ', ' ) ; throw new PluginError ( PLUGIN_NAME , `Cannot find RSA key, searched: ${ rsaKey }` ) ; } return key ; } /**
 * Lots of ways to present key info
 */ function getKey ( options ) { let key = options . key || options . keyLocation || null ; if ( key && BalmJS . utils . isString ( key ) ) { key = { location : key } ; } if ( ! key && ( options . passphrase || options . keyContents || ! options . password ) ) { key = { } ; } if ( key ) { key . contents = key . contents || options . keyContents ; key . passphrase = key . passphrase || options . passphrase ; key . location = key . location || [ '~/.ssh/id_rsa' , '/.ssh/id_rsa' , '~/.ssh/id_dsa' , '/.ssh/id_dsa' ] ; if ( ! BalmJS . utils . isArray ( key . location ) ) { key . location = [ key . location ] ; } key = resolveHomePath ( key ) ; } return key ; } function gulpSftp ( options ) { options = Object . assign ( { } , options ) ; if ( options . host === undefined ) { throw new PluginError ( PLUGIN_NAME , '`host` required' ) ; } let fileCount = 0 ; const remotePath = options . remotePath || '/' ; const remotePlatform = options . remotePlatform || options . platform || 'unix' ; options . authKey = options . authKey || options . auth ; const authFilePath = options . authFile || '.ftppass' ; const authFile = path . join ( './' , authFilePath ) ; if ( options . authKey && fs . existsSync ( authFile ) ) { let auth = JSON . parse ( fs . readFileSync ( authFile , 'utf8' ) ) [ options . authKey ] ; if ( ! auth ) { throw new PluginError ( PLUGIN_NAME , 'Could not find authkey in .ftppass' ) ; } if ( BalmJS . utils . isString ( auth ) && auth . indexOf ( ':' ) !== - 1 ) { const authparts = auth . split ( ':' ) ; auth = { user : authparts [ 0 ] , pass : authparts [ 1 ] } ; } const authKeys = Object . keys ( auth ) ; for ( let i = 0 , len = authKeys . length ; i < len ; i ++ ) { const attr = authKeys [ i ] ; if ( auth [ attr ] ) { options [ attr ] = auth [ attr ] ; } } } const password = options . password ; const username = options . username || 'anonymous' ; const key = getKey ( options ) ; const mkDirCache = { } ; let finished = false ; let sftpCache = null ; let connCache = null ; function _pool ( this , uploader ) { const ssh2 = require ( 'ssh2' ) ; if ( sftpCache ) { return uploader ( sftpCache ) ; } if ( password ) { BalmJS . logger . debug ( PLUGIN_NAME , 'Authenticating with password' ) ; } else if ( key ) { BalmJS . logger . debug ( PLUGIN_NAME , 'Authenticating with private key' ) ; } const connectionOptions = { host : options . host , port : options . port || 22 , username } ; if ( password ) { connectionOptions . password = password ; } else if ( options . agent ) { connectionOptions . agent = options . agent ; connectionOptions . agentForward = options . agentForward || false ; } else if ( key ) { connectionOptions . privateKey = key . contents ; connectionOptions . passphrase = key . passphrase ; } if ( options . timeout ) { connectionOptions . readyTimeout = options . timeout ; } const conn = new ssh2 . Client ( ) ; connCache = conn ; conn . on ( 'ready' , ( ) => { BalmJS . logger . debug ( PLUGIN_NAME , 'Connection :: ready' ) ; conn . sftp ( ( err , sftp ) => { if ( err ) { throw err ; } sftp . on ( 'end' , function ( this ) { BalmJS . logger . debug ( PLUGIN_NAME , 'SFTP session closed' ) ; sftpCache = null ; if ( ! finished ) { this . emit ( 'error' , new PluginError ( PLUGIN_NAME , 'SFTP abrupt closure' ) ) ; } } ) ; sftpCache = sftp ; uploader ( sftpCache ) ; } ) ; } ) ; conn . on ( 'error' , ( err ) => { this . emit ( 'error' , new PluginError ( PLUGIN_NAME , err ) ) ; } ) ; conn . on ( 'end' , ( ) => { BalmJS . logger . debug ( PLUGIN_NAME , 'Connection :: end' ) ; } ) ; conn . on ( 'close' , ( ) => { if ( ! finished ) { this . emit ( 'error' , new PluginError ( PLUGIN_NAME , 'SFTP abrupt closure' ) ) ; } if ( options . callback ) { options . callback ( ) ; } } ) ; conn . connect ( connectionOptions ) ; } function transform ( this , file , encoding , callback ) { if ( file . isNull ( ) ) { return callback ( ) ; } const finalRemotePath = normalizePath ( path . join ( remotePath , file . relative ) ) ; _pool . call ( this , ( sftp ) => { const dirname = path . dirname ( finalRemotePath ) ; let fileDirs = require ( 'parents' ) ( dirname ) . map ( ( d ) => d . replace ( /^\/~/ , '~' ) ) . map ( normalizePath ) ; if ( dirname . search ( /^\// ) === 0 ) { fileDirs = fileDirs . map ( ( dir ) => dir . search ( /^\// ) === 0 ? dir : `/${ dir }` ) ; } fileDirs = fileDirs . filter ( ( d ) => d . length >= remotePath . length && ! mkDirCache [ d ] ) ; require ( 'async' ) . whilst ( ( cb ) => cb ( null , fileDirs && fileDirs . length ) , ( cb ) => { let d = fileDirs . pop ( ) as string ; mkDirCache [ d ] = true ; if ( remotePlatform && remotePlatform . toLowerCase ( ) . indexOf ( 'win' ) !== - 1 ) { d = d . replace ( '/' , '\\' ) ; } sftp . exists ( d , ( exist ) => { if ( exist ) { cb ( ) ; } else { sftp . mkdir ( d , { mode : '0755' } , ( err ) => { if ( err ) { BalmJS . logger . warn ( PLUGIN_NAME , `Error or directory exists: ${ err } ${ d }` ) ; } else { BalmJS . logger . debug ( PLUGIN_NAME , `Created: ${ d }` ) ; } cb ( ) ; } ) ; } } ) ; } , ( ) => { const stream = sftp . createWriteStream ( finalRemotePath , { flags : 'w' , encoding : null , mode : '0666' , autoClose : true } ) ; if ( file . isStream ( ) ) { file . contents . pipe ( stream ) ; } else if ( file . isBuffer ( ) ) { stream . end ( file . contents ) ; } stream . on ( 'close' , function ( this , error ) { if ( error ) { this . emit ( 'error' , new PluginError ( PLUGIN_NAME , error ) ) ; } else { const fileRelativePath = file . relative as string ; BalmJS . logger . info ( PLUGIN_NAME , `Uploaded: ${ fileRelativePath } => ${ finalRemotePath }` ) ; fileCount ++ ; } return callback ( error ) ; } ) ; } ) ; } ) ; } function flush ( callback ) { if ( fileCount > 0 ) { const unit = fileCount === 1 ? 'file' : 'files' ; BalmJS . logger . info ( PLUGIN_NAME , `${ fileCount } ${ unit } uploaded successfully` ) ; } else { BalmJS . logger . warn ( PLUGIN_NAME , 'No files uploaded' ) ; } finished = true ; if ( sftpCache ) { sftpCache . end ( ) ; } if ( connCache ) { connCache . end ( ) ; } callback ( ) ; } return through2 . obj ( transform , flush ) ; } export default gulpSftp ;