├── .gitattributes ├── .gitignore ├── .jshintrc ├── .mailmap ├── .travis.yml ├── README.md ├── index.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | fixture 3 | .idea 4 | .project 5 | .settings 6 | ======= 7 | # Logs 8 | logs 9 | *.log 10 | 11 | .DS_Store 12 | test.* 13 | test 14 | out 15 | gulpfile.js 16 | .ftppass 17 | .editorconfig -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "esnext": true, 4 | "bitwise": true, 5 | "camelcase": true, 6 | "curly": true, 7 | "eqeqeq": true, 8 | "immed": true, 9 | "indent": 4, 10 | "latedef": true, 11 | "newcap": true, 12 | "noarg": true, 13 | "quotmark": "single", 14 | "regexp": true, 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "trailing": true, 19 | "smarttabs": true 20 | } 21 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | # 2 | # This list is used by git-shortlog to fix a few botched name translations 3 | # in the git archive, either because the author's full name was messed up 4 | # and/or not always written the same way, making contributions from the 5 | # same person appearing not to be so. 6 | # 7 | 8 | Matthew Drake gtg092x 9 | 10 | Valan Brown 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | script: 'sudo $(which npm) test' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [gulp](http://gulpjs.com)-sftp [![Build Status](https://travis-ci.org/gtg092x/gulp-sftp.svg?branch=master)](https://travis-ci.org/gtg092x/gulp-sftp) 2 | 3 | > Upload files via SSH 4 | 5 | Useful for uploading and deploying things via sftp. Right now this plugin just uploads everything. Caching and hash comparison are two TODO items. 6 | 7 | [![NPM](https://nodei.co/npm/gulp-sftp.png?downloads=true&stars=true)](https://nodei.co/npm/gulp-sftp/) 8 | 9 | ## Install 10 | 11 | ```bash 12 | $ npm install --save-dev gulp-sftp 13 | ``` 14 | 15 | 16 | ## Usage 17 | 18 | ```js 19 | var gulp = require('gulp'); 20 | var sftp = require('gulp-sftp'); 21 | 22 | gulp.task('default', function () { 23 | return gulp.src('src/*') 24 | .pipe(sftp({ 25 | host: 'website.com', 26 | user: 'johndoe', 27 | pass: '1234' 28 | })); 29 | }); 30 | ``` 31 | 32 | 33 | ## API 34 | 35 | ### sftp(options) 36 | 37 | #### options.host 38 | 39 | *Required* 40 | Type: `String` 41 | 42 | #### options.port 43 | 44 | Type: `Number` 45 | Default: `22` 46 | 47 | #### options.user 48 | 49 | Type: `String` 50 | Default: `'anonymous'` 51 | 52 | #### options.pass 53 | 54 | Type: `String` 55 | Default: `null` 56 | 57 | If this option is not set, gulp-sftp assumes the user is using private key authentication and will default to using keys at the following locations: 58 | 59 | `~/.ssh/id_dsa` and `/.ssh/id_rsa` 60 | 61 | If you intend to use anonymous login, use the value '@anonymous'. 62 | 63 | #### options.remotePath 64 | 65 | Type: `String` 66 | Default: `'/'` 67 | 68 | The remote path to upload to. If this path does not yet exist, it will be created, as well as the child directories that house your files. 69 | 70 | #### options.remotePlatform 71 | 72 | Type: `String` 73 | Default: `'unix'` 74 | 75 | The remote platform that you are uploading to. If your destination server is a Windows machine, use the value `windows`. 76 | 77 | #### options.key 78 | 79 | type `String` or `Object` 80 | Default: `null` 81 | 82 | A key file location. If an object, please use the format `{location:'/path/to/file',passphrase:'secretphrase'}` 83 | 84 | 85 | #### options.passphrase 86 | 87 | type `String` 88 | Default: `null` 89 | 90 | A passphrase for secret key authentication. Leave blank if your key does not need a passphrase. 91 | 92 | #### options.keyContents 93 | 94 | type `String` 95 | Default: `null` 96 | 97 | If you wish to pass the key directly through gulp, you can do so by setting it to options.keyContents. 98 | 99 | #### options.auth 100 | 101 | type `String` 102 | Default: `null` 103 | 104 | An identifier to access authentication information from `.ftppass` see [Authentication](#authentication) for more information. 105 | 106 | #### options.authFile 107 | 108 | type `String` 109 | Default: `.ftppass` 110 | 111 | A path relative to the project root to a JSON formatted file containing auth information. 112 | 113 | #### options.timeout 114 | type `int` 115 | Default: Currently set by ssh2 as `10000` milliseconds. 116 | 117 | An integer in milliseconds specifying how long to wait for a server response. 118 | 119 | #### options.agent 120 | type `String` 121 | Default: `null` 122 | 123 | Path to ssh-agent's UNIX socket for ssh-agent-based user authentication. 124 | 125 | #### options.agentForward 126 | type `bool` 127 | Default: `false` 128 | 129 | Set to true to use OpenSSH agent forwarding. Requires that `options.agent` is configured. 130 | 131 | #### options.callback 132 | type `function` 133 | Default: `null` 134 | 135 | Callback function to be called once the SFTP connection is closed. 136 | 137 | 138 | ##Authentication 139 | 140 | For better security, save authentication data in a json formatted file named `.ftppass` (or to whatever value you set options.authFile to). **Be sure to add this file to .gitignore**. You do not typically want auth information stored in version control. 141 | 142 | ```js 143 | var gulp = require('gulp'); 144 | var sftp = require('gulp-sftp'); 145 | 146 | gulp.task('default', function () { 147 | return gulp.src('src/*') 148 | .pipe(sftp({ 149 | host: 'website.com', 150 | auth: 'keyMain' 151 | })); 152 | }); 153 | ``` 154 | 155 | `.ftppass` 156 | 157 | ```json 158 | { 159 | "keyMain": { 160 | "user": "username1", 161 | "pass": "password1" 162 | }, 163 | "keyShort": "username1:password1", 164 | "privateKey": { 165 | "user": "username" 166 | }, 167 | "privateKeyEncrypted": { 168 | "user": "username", 169 | "passphrase": "passphrase1" 170 | }, 171 | "privateKeyCustom": { 172 | "user": "username", 173 | "passphrase": "passphrase1", 174 | "keyLocation": "/full/path/to/key" 175 | } 176 | } 177 | ``` 178 | 179 | 180 | ##Work with [pem](https://github.com/andris9/pem) 181 | 182 | To use [pem](https://github.com/andris9/pem) create private keys and certificates for access your server: 183 | 184 | ```js 185 | var pem = require('pem'); 186 | gulp.task('deploy:test', function () { 187 | pem.createCertificate({}, function (err, kyes) { 188 | return gulp.src('./src/**/*') 189 | .pipe(sftp({ 190 | host: 'testserver.com', 191 | user: 'testuser', 192 | pass: 'testpass', 193 | key: kyes.clientKey, 194 | keyContents: kyes.keyContents 195 | })); 196 | }); 197 | }); 198 | ``` 199 | 200 | ##Known Issues 201 | 202 | ###SFTP error or directory exists: Error: No such file /remote/sub/folder 203 | 204 | Version 0.1.2 has an issue for Windows clients when it comes to resolving remote paths. Please upgrade to 0.1.3. 205 | 206 | ###Error:: SFTP abrupt closure 207 | 208 | ~~Some conditions can cause the [ssh2](https://github.com/mscdex/ssh2) connection to abruptly close. The issues that commonly cause this are large files (though they are checked for and are automatically converted to streams) and heavy memory usage.~~ 209 | 210 | ~~To solve problems related to [ssh2](https://github.com/mscdex/ssh2) closures, try to use streams instead of buffers. Do this by passing `{buffer:false}` as an option with `gulp.src`. This isn't always an option, so I would suggest exploring ways to move between streams and buffers. Lars Kappert has a [great article on managing this](https://medium.com/web-code-junk/a2010c13d3d5).~~ 211 | 212 | Some awesome work via @mscdex addressed this issue. Please make sure you have the latest version or greater of gulp-sftp (0.1.1) and the latest version or greater of ssh2 (0.3.4) and you should not see abrupt disconnects with large files. 213 | 214 | ## License 215 | 216 | [MIT](http://opensource.org/licenses/MIT) 217 | 218 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var fs = require('fs'); 4 | var gutil = require('gulp-util'); 5 | var util = require('util'); 6 | var through = require('through2'); 7 | var Connection = require('ssh2'); 8 | var async = require('async'); 9 | var parents = require('parents'); 10 | var Stream = require('stream'); 11 | var assign = require('object-assign'); 12 | 13 | var normalizePath = function(path){ 14 | return path.replace(/\\/g, '/'); 15 | }; 16 | 17 | module.exports = function (options) { 18 | options = assign({}, options);// credit sindresorhus 19 | 20 | 21 | if (options.host === undefined) { 22 | throw new gutil.PluginError('gulp-sftp', '`host` required.'); 23 | } 24 | 25 | var fileCount = 0; 26 | var remotePath = options.remotePath || '/'; 27 | var remotePlatform = options.remotePlatform || options.platform || 'unix'; 28 | 29 | options.authKey = options.authKey||options.auth; 30 | var authFilePath = options.authFile || '.ftppass'; 31 | var authFile=path.join('./',authFilePath); 32 | if(options.authKey && fs.existsSync(authFile)){ 33 | var auth = JSON.parse(fs.readFileSync(authFile,'utf8'))[options.authKey]; 34 | if(!auth) 35 | this.emit('error', new gutil.PluginError('gulp-sftp', 'Could not find authkey in .ftppass')); 36 | if(typeof auth == "string" && auth.indexOf(":")!=-1){ 37 | var authparts = auth.split(":"); 38 | auth = {user:authparts[0],pass:authparts[1]}; 39 | } 40 | for (var attr in auth) { options[attr] = auth[attr]; } 41 | } 42 | 43 | //option aliases 44 | options.password = options.password||options.pass; 45 | options.username = options.username||options.user||'anonymous'; 46 | 47 | /* 48 | * Lots of ways to present key info 49 | */ 50 | var key = options.key || options.keyLocation || null; 51 | if(key&&typeof key == "string") 52 | key = {location:key}; 53 | 54 | //check for other options that imply a key or if there is no password 55 | if(!key && (options.passphrase||options.keyContents||!options.password)){ 56 | key = {}; 57 | } 58 | 59 | if(key){ 60 | 61 | //aliases 62 | key.contents=key.contents||options.keyContents; 63 | key.passphrase=key.passphrase||options.passphrase; 64 | 65 | //defaults 66 | key.location=key.location||["~/.ssh/id_rsa","/.ssh/id_rsa","~/.ssh/id_dsa","/.ssh/id_dsa"]; 67 | 68 | //type normalization 69 | if(!util.isArray(key.location)) 70 | key.location=[key.location]; 71 | 72 | //resolve all home paths 73 | if(key.location){ 74 | var home = process.env.HOME||process.env.USERPROFILE; 75 | for(var i=0;i=remotePath.length&&!mkDirCache[d];}); 239 | 240 | //while there are dirs to create, create them 241 | //https://github.com/caolan/async#whilst - not the most commonly used async control flow 242 | async.whilst(function(){ 243 | return fileDirs && fileDirs.length; 244 | },function(next){ 245 | var d= fileDirs.pop(); 246 | mkDirCache[d]=true; 247 | //mdrake - TODO: use a default file permission instead of defaulting to 755 248 | if(remotePlatform && remotePlatform.toLowerCase().indexOf('win')!==-1) { 249 | d = d.replace('/','\\'); 250 | } 251 | sftp.exists(d, function(exist) { 252 | if (!exist) { 253 | sftp.mkdir(d, {mode: '0755'}, function(err){//REMOTE PATH 254 | if(err){ 255 | gutil.log('SFTP Mkdir Error:', gutil.colors.red(err + " " +d)); 256 | }else{ 257 | gutil.log('SFTP Created:', gutil.colors.green(d)); 258 | } 259 | next(); 260 | }); 261 | } else { 262 | next(); 263 | } 264 | }); 265 | 266 | },function(){ 267 | 268 | var stream = sftp.createWriteStream(finalRemotePath,{//REMOTE PATH 269 | flags: 'w', 270 | encoding: null, 271 | mode: '0666', 272 | autoClose: true 273 | }); 274 | 275 | //var readStream = fs.createReadStream(fileBase+localRelativePath); 276 | 277 | var uploadedBytes = 0; 278 | 279 | 280 | var highWaterMark = stream.highWaterMark||(16*1000); 281 | var size = file.stat.size; 282 | 283 | 284 | file.pipe(stream); // start upload 285 | 286 | stream.on('drain',function(){ 287 | uploadedBytes+=highWaterMark; 288 | var p = Math.round((uploadedBytes/size)*100); 289 | p = Math.min(100,p); 290 | gutil.log('gulp-sftp:',finalRemotePath,"uploaded",(uploadedBytes/1000)+"kb"); 291 | }); 292 | 293 | 294 | 295 | 296 | stream.on('close', function(err) { 297 | 298 | if(err) 299 | this.emit('error', new gutil.PluginError('gulp-sftp', err)); 300 | else{ 301 | if (logFiles) { 302 | gutil.log('gulp-sftp:', gutil.colors.green('Uploaded: ') + 303 | file.relative + 304 | gutil.colors.green(' => ') + 305 | finalRemotePath); 306 | } 307 | 308 | fileCount++; 309 | } 310 | return cb(err); 311 | }); 312 | 313 | });//async.whilst 314 | }); 315 | 316 | 317 | 318 | this.push(file); 319 | 320 | }, function (cb) { 321 | if (fileCount > 0) { 322 | gutil.log('gulp-sftp:', gutil.colors.green(fileCount, fileCount === 1 ? 'file' : 'files', 'uploaded successfully')); 323 | } else { 324 | gutil.log('gulp-sftp:', gutil.colors.yellow('No files uploaded')); 325 | } 326 | finished=true; 327 | if(sftpCache) 328 | sftpCache.end(); 329 | if(connectionCache) 330 | connectionCache.end(); 331 | 332 | cb(); 333 | }); 334 | }; 335 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-sftp", 3 | "version": "0.1.6", 4 | "description": "Upload files via SSH", 5 | "license": "MIT", 6 | "repository": "gtg092x/gulp-sftp", 7 | "homepage": "https://github.com/gtg092x/gulp-sftp/", 8 | "bugs": { 9 | "url" : "https://github.com/gtg092x/gulp-sftp/issues" 10 | }, 11 | "author": { 12 | "name": "Matthew Drake", 13 | "email": "mdrake@mediadrake.com", 14 | "url": "http://mediadrake.com" 15 | }, 16 | "contributors": [ 17 | "Benjamin P. Jung ", 18 | "Carson Britt ", 19 | "Matthew Drake ", 20 | "Valan Brown ", 21 | "Sorin Guga " 22 | ], 23 | "engines": { 24 | "node": ">=0.10.0" 25 | }, 26 | "scripts": {}, 27 | "files": [ 28 | "index.js" 29 | ], 30 | "keywords": [ 31 | "gulpplugin", 32 | "sftp", 33 | "file", 34 | "files", 35 | "transfer", 36 | "protocol", 37 | "server", 38 | "client", 39 | "upload", 40 | "deploy", 41 | "deployment" 42 | ], 43 | "dependencies": { 44 | "async": "~0.9.0", 45 | "gulp-util": "~3.0.0", 46 | "object-assign": "~0.3.1", 47 | "parents": "~1.0.0", 48 | "ssh2": "~0.6.1", 49 | "through2": "~0.4.2" 50 | }, 51 | "devDependencies": {} 52 | } --------------------------------------------------------------------------------