├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── uploadtest ├── css └── style-index.css ├── html └── index.html ├── img ├── bg-body.png └── bg-logo.png └── sprite ├── style-index.png └── style-index@2x.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .idea/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 TmT Team and other contributors 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## node-sftp-deploy [![NPM Version](http://img.shields.io/npm/v/sftp2.svg?style=flat)](https://www.npmjs.com/package/node-sftp-deploy "Package version") 2 | 3 | [![NPM Downloads](https://img.shields.io/npm/dm/node-sftp-deploy.svg?style=flat)](https://www.npmjs.com/package/node-sftp-deploy "NPM Downloads") 4 | [![dependencies](https://img.shields.io/david/weixin/node-sftp-deploy.svg)](https://ci.appveyor.com/project/weixin/node-sftp-deploy "Dependencies") 5 | [![Join the chat at https://gitter.im/weixin/node-sftp-deploy](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/TmT?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 6 | [![TmT Name](https://img.shields.io/badge/Team-TmT-brightgreen.svg?style=flat)](https://github.com/orgs/TmT/people "Tencent Moe Team") 7 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat)](http://opensource.org/licenses/MIT "Feel free to contribute.") 8 | 9 | > Upload and deploy files from SFTP within username & password. 10 | 11 | ## Install 12 | 13 | 14 | ```bash 15 | npm install --save node-sftp-deploy 16 | ``` 17 | 18 | ## Usage 19 | 20 | ```javascript 21 | var sftp = require('node-sftp-deploy'); 22 | sftp({ 23 | "host": "10.10.10.10", 24 | "port": "20", 25 | "user": "user", 26 | "pass": "pass", 27 | "remotePath": "", 28 | "sourcePath": "./" 29 | }, function(){ 30 | //Success Callback 31 | }); 32 | 33 | //Support Promise 34 | sftp(sftpConfig).then(function(){ 35 | //Success Callback 36 | }); 37 | ``` 38 | 39 | 40 | ## Contributing 41 | 42 | This repo is build and maintained by [TmT Team](https://github.com/orgs/TmT/people). 43 | If you get any bugs or feature requests, please open a new [Issue](https://github.com/weixin/node-sftp-deploy/issues) or send us [Pull Request](https://github.com/weixin/node-sftp-deploy/pulls), Thank you for your contributions. 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var util = require('util'); 4 | var Connection = require('ssh2'); 5 | var async = require('async'); 6 | var parents = require('parents'); 7 | var assign = require('object-assign'); 8 | var fs = require('fs-extra'); 9 | var chalk = require('chalk'); 10 | var Q = require('q'); 11 | 12 | var normalizePath = function (path) { 13 | return path.replace(/\\/g, '/'); 14 | }; 15 | 16 | module.exports = function (options, callback) { 17 | 18 | return Q.Promise(function(resolve, reject){ 19 | options = assign({ 20 | "host": "", 21 | "port": "36000", 22 | "user": "", 23 | "pass": "", 24 | "remotePath": "", 25 | "sourcePath": "./", 26 | "remotePlatform": "unix" 27 | }, options); 28 | 29 | 30 | if (options.host === undefined || options.host === '') { 31 | throw new Error('sftp2', '`host` required.'); 32 | } 33 | 34 | var fileCount = 0; 35 | var fileLength = 0; 36 | 37 | options.password = options.password || options.pass; 38 | options.username = options.username || options.user || 'anonymous'; 39 | 40 | 41 | var remotePath = options.remotePath; 42 | var sourcePath = options.sourcePath.replace(/\\/g, '/'); 43 | var remotePlatform = options.remotePlatform; 44 | 45 | var mkDirCache = {}; 46 | 47 | var finished = false; 48 | var connectionCache = null; 49 | 50 | var items = []; 51 | fs.walk(sourcePath) 52 | .on('data', function (item) { 53 | if (!item.stats.isDirectory()) { 54 | items.push(item); 55 | } 56 | }) 57 | .on('end', function () { 58 | fileLength = items.length; 59 | 60 | if (fileLength <= 0) { 61 | console.log('sftp2:', chalk.yellow('No files uploaded')); 62 | } else { 63 | return uploadFiles(items); 64 | } 65 | }); 66 | 67 | function uploadFiles(files) { 68 | 69 | connectSftp(function (sftp) { 70 | async.eachSeries(files, function (file, done) { 71 | var filepath = file.path.replace(/\\/g, '/'); 72 | 73 | var pathArr = sourcePath.replace(/\/$/, '').split('/'); 74 | 75 | var projectName = pathArr[pathArr.length - 1]; 76 | 77 | var relativePath = filepath.split(projectName + '/')[1]; 78 | var finalRemotePath = normalizePath(path.join(remotePath, relativePath)); 79 | 80 | var dirname = path.dirname(finalRemotePath); 81 | 82 | var fileDirs = parents(dirname) 83 | .map(function (d) { 84 | return d.replace(/^\/~/, "~"); 85 | }) 86 | .map(normalizePath); 87 | 88 | if (dirname.search(/^\//) === 0) { 89 | fileDirs = fileDirs.map(function (dir) { 90 | if (dir.search(/^\//) === 0) { 91 | return dir; 92 | } 93 | return '/' + dir; 94 | }); 95 | } 96 | 97 | 98 | fileDirs = fileDirs.filter(function (d) { 99 | return d.length >= remotePath.length && !mkDirCache[d]; 100 | }); 101 | 102 | async.whilst(function () { 103 | return fileDirs && fileDirs.length; 104 | }, function (next) { 105 | var d = fileDirs.pop(); 106 | mkDirCache[d] = true; 107 | 108 | if (remotePlatform && remotePlatform.toLowerCase().indexOf('win') !== -1) { 109 | d = d.replace('/', '\\'); 110 | } 111 | 112 | sftp.mkdir(d, {mode: '0755'}, function () { 113 | next(); 114 | }); 115 | }, function () { 116 | 117 | var readStream = fs.createReadStream(filepath); 118 | 119 | var stream = sftp.createWriteStream(finalRemotePath, { 120 | flags: 'w', 121 | encoding: null, 122 | mode: '0666', 123 | autoClose: true 124 | }); 125 | 126 | readStream.pipe(stream); 127 | 128 | stream.on('close', function (err) { 129 | 130 | if (err) { 131 | throw new Error('sftp2', err); 132 | } else { 133 | 134 | fileCount++; 135 | 136 | } 137 | 138 | done(); 139 | 140 | }); 141 | 142 | }); 143 | 144 | }, function () { 145 | console.log('sftp2:', chalk.green(fileCount, fileCount === 1 ? 'file' : 'files', 'uploaded successfully')); 146 | 147 | finished = true; 148 | 149 | if (sftp) { 150 | sftp.end(); 151 | } 152 | if (connectionCache) { 153 | connectionCache.end(); 154 | } 155 | 156 | if(callback){ 157 | callback(); 158 | } 159 | 160 | resolve() 161 | 162 | 163 | 164 | }); 165 | }); 166 | } 167 | 168 | function connectSftp(callback) { 169 | console.log('Authenticating with password.'); 170 | 171 | var c = new Connection(); 172 | connectionCache = c; 173 | c.on('ready', function () { 174 | 175 | c.sftp(function (err, sftp) { 176 | if (err) { 177 | throw err; 178 | } 179 | 180 | sftp.on('end', function () { 181 | // console.log('SFTP :: SFTP session closed'); 182 | sftp = null; 183 | if (!finished) { 184 | console.log('error', new Error('sftp2', "SFTP abrupt closure")) 185 | } 186 | }); 187 | 188 | callback(sftp); 189 | }); 190 | 191 | }); 192 | 193 | c.on('error', function (err) { 194 | console.log('sftp2', err); 195 | reject(err); 196 | }); 197 | 198 | c.on('end', function () { 199 | // console.log('Connection :: end'); 200 | }); 201 | 202 | c.on('close', function (had_error) { 203 | if (!finished) { 204 | console.log('sftp2', "SFTP abrupt closure"); 205 | } 206 | // console.log('Connection :: close', had_error !== false ? "with error" : ""); 207 | 208 | }); 209 | 210 | 211 | /* 212 | * connection options, may be a key 213 | */ 214 | var connection_options = { 215 | host: options.host, 216 | port: options.port || 22, 217 | username: options.username 218 | }; 219 | 220 | if (options.password) { 221 | connection_options.password = options.password; 222 | } 223 | 224 | if (options.timeout) { 225 | connection_options.readyTimeout = options.timeout; 226 | } 227 | 228 | 229 | c.connect(connection_options); 230 | } 231 | 232 | }); 233 | 234 | }; 235 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-sftp-deploy", 3 | "version": "1.0.3", 4 | "description": "Upload and deploy files from SFTP within username & password", 5 | "main": "index.js", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/weixin/node-sftp-deploy" 10 | }, 11 | "homepage": "https://github.com/weixin/node-sftp-deploy", 12 | "bugs": { 13 | "url": "https://github.com/weixin/node-sftp-deploy/issues" 14 | }, 15 | "author": { 16 | "name": "Littledu", 17 | "email": "410491325@qq.com", 18 | "url": "https://github.com/littledu" 19 | }, 20 | "engines": { 21 | "node": ">=0.10.0" 22 | }, 23 | "keywords": [ 24 | "sftp", 25 | "file", 26 | "files", 27 | "transfer", 28 | "protocol", 29 | "server", 30 | "client", 31 | "upload", 32 | "deploy", 33 | "deployment" 34 | ], 35 | "dependencies": { 36 | "async": "^0.9.2", 37 | "chalk": "^1.1.1", 38 | "fs-extra": "^0.26.7", 39 | "object-assign": "~0.3.1", 40 | "parents": "~1.0.0", 41 | "q": "^1.4.1", 42 | "ssh2": "0.4.13" 43 | }, 44 | "readmeFilename": "README.md" 45 | } 46 | -------------------------------------------------------------------------------- /test/uploadtest/css/style-index.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:700}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}.header{margin:20px;text-align:center}.header__logo{text-align:center;display:block;margin:0 auto 20px;text-indent:-9999px;background-image:url(../img/bg-logo.png);width:300px;height:300px}.header__social-media{background:url(../img/bg-body.png) repeat;text-align:center;border-top:1px solid #eee;padding:20px;margin:40px 20px 20px;font-size:12px;color:#aaa}.icon{display:inline-block}.icon-twitter{background-position:-4px -4px}.icon-facebook,.icon-twitter{background-image:url(../sprite/style-index.png);width:32px;height:32px}.icon-facebook{background-position:-40px -4px}.icon-github{background-position:-4px -40px}.icon-github,.icon-instagram{background-image:url(../sprite/style-index.png);width:32px;height:32px}.icon-instagram{background-position:-40px -40px}.icon-dribbble{background-image:url(../sprite/style-index.png);background-position:-76px -4px;width:32px;height:32px}.tips{border-left:4px solid #b7e3f6;background-color:#e9f6fb;display:inline;padding:4px 0 4px 5px;font-size:14px}.tips strong{font-weight:bolder;color:#ea5798}.tips__code{padding:4px}@media only screen and (-webkit-min-device-pixel-ratio:2),only screen and (-webkit-min-device-pixel-ratio:2.5),only screen and (min--moz-device-pixel-ratio:2),only screen and (min-resolution:240dpi){.icon-twitter{background-position:-4px -4px}.icon-facebook,.icon-twitter{background-image:url(../sprite/style-index@2x.png);background-size:108px}.icon-facebook{background-position:-40px -4px}.icon-github{background-position:-4px -40px}.icon-github,.icon-instagram{background-image:url(../sprite/style-index@2x.png);background-size:108px}.icon-instagram{background-position:-40px -40px}.icon-dribbble{background-image:url(../sprite/style-index@2x.png);background-size:108px;background-position:-76px -4px}} -------------------------------------------------------------------------------- /test/uploadtest/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | tmt-workflow Test 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |

tmt-workflow Test

16 | 17 |
Normal image still ../img/bg-logo.png
18 |
19 | Twitter 20 | Facebook 21 | Github 22 | Instagram 23 | Dribbble 24 |
25 |
Icons images ../slice/icon-twitter.png change to -> ../sprite/test.png
26 |
27 | 28 | -------------------------------------------------------------------------------- /test/uploadtest/img/bg-body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin/node-sftp-deploy/e77d71c513449b5e7c63496196ae7417923a12f1/test/uploadtest/img/bg-body.png -------------------------------------------------------------------------------- /test/uploadtest/img/bg-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin/node-sftp-deploy/e77d71c513449b5e7c63496196ae7417923a12f1/test/uploadtest/img/bg-logo.png -------------------------------------------------------------------------------- /test/uploadtest/sprite/style-index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin/node-sftp-deploy/e77d71c513449b5e7c63496196ae7417923a12f1/test/uploadtest/sprite/style-index.png -------------------------------------------------------------------------------- /test/uploadtest/sprite/style-index@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/weixin/node-sftp-deploy/e77d71c513449b5e7c63496196ae7417923a12f1/test/uploadtest/sprite/style-index@2x.png --------------------------------------------------------------------------------