UNPKG

11.2 kBJavaScriptView Raw
1/**
2 * Created by Rodey on 2017/7/7.
3 */
4
5'use strict';
6
7const util = require('../utils'),
8 extend = require('extend'),
9 SSH2 = require('ssh2'),
10 EventEmitter = require('events').EventEmitter,
11 LoadingORA = require('../loadProgress').LoadingORA,
12 dateFormat = require('dateformat'),
13 Uploader = require('./uploader'),
14 Backup = require('./backup'),
15 T = require('../tools');
16
17class Deploy extends EventEmitter {
18 constructor(config) {
19 super();
20 this.basePath = T.getArg('cwdir') || process.cwd();
21
22 this.host = null;
23 this.port = 22;
24 this.username = 'anonymous';
25 this.password = null;
26 this.timeout = 50000;
27
28 // 本地目录
29 this.localPath = '';
30 this.filters = [];
31
32 // 远程目录
33 this.remotePath = null;
34 this.platform = 'unix';
35 this.onUploadedComplete = null;
36 this.onUploadedFileSuccess = null;
37 this.onUploadedFileError = null;
38 // 是否执行上传
39 this.isExecute = true;
40 // 是否编译之后进行部署
41 // 有时候我们希望编译生产环境后不进行部署,而是单独执行部署命令
42 this.isBuildAfter = true;
43 // 上传方式 increment: 增量; full: 全量
44 this.type = 'full';
45 // 提示方式 all: 显示详细信息; progress: 显示进度 + 详细信息
46 this.log = 'progress';
47 // 将日志输出到文件
48 this.logFile = null;
49 // 链接方式 sftp or ftp
50 this.connectType = 'sftp';
51
52 this.agent = null;
53 this.agentForward = false;
54
55 this.privateKey = null;
56 this.passphrase = null;
57
58 this.keepaliveCountMax = 3;
59
60 this.authKey = '';
61 this.auth = null;
62 this.authFile = '.ftppass';
63
64 this.key = {};
65 this.keyLocation = null;
66 this.keyContents = null;
67
68 this.uploader = null;
69 this.isRollback = true;
70
71 this.sftp = null;
72 this.ssh2 = null;
73
74 this.options = config;
75
76 if (util.isObject(config)) {
77 extend(true, this, config);
78 }
79
80 this.init();
81 this.loading = new LoadingORA();
82 this.uploader = new Uploader(this.localPath, this.remotePath, this._getConnectOptions());
83 }
84
85 init() {
86 // init type
87 this.initType();
88
89 // localpath
90 this.initPath();
91
92 // remotepath
93 this.initRemotePath();
94
95 // localpath filters
96 this.initFilters();
97
98 // auto
99 this.initAuth();
100
101 // present key info
102 this.initPresentKey();
103
104 // init log type
105 this.initLog();
106
107 // init timeout
108 this.initTimeout();
109 }
110
111 start() {
112 // connent host as fstp
113 this.isExecute ? this._initUploader() : this.stop();
114 return this;
115 }
116
117 stop() {
118 this.uploader && this.uploader.stop();
119 // this._onDone();
120 }
121
122 initType() {
123 // 是否增量
124 this._isIncrementType = this.type === 'increment';
125 // 是否全量
126 this._isFullType = this.type === 'full';
127 }
128
129 initLog() {
130 this._isProgressLog = this.log === 'progress';
131 }
132
133 initTimeout() {
134 // ftp timeout options
135 if (this.connectType === 'ftp') {
136 this.connTimeout = this.pasvTimeout = this.keepalive = this.timeout;
137 }
138 }
139
140 // path
141 initPath() {
142 if (util.isString(this.localPath) && this.localPath.length > 0) {
143 this.localPath = (!T.isAbsolutePath(this.localPath) && [T.Path.resolve(process.cwd(), this.localPath)]) || [this.localPath];
144 } else if (util.isArray(this.localPath)) {
145 this.localPath = this.localPath
146 .filter(p => {
147 return p && util.isString(p);
148 })
149 .map(p => {
150 if (p && p.length > 0) {
151 return !T.isAbsolutePath(p) ? T.Path.resolve(process.cwd(), p) : p;
152 }
153 });
154 } else {
155 T.log.error('× LocalPath Error: localPath is not found ( localPath can be String or Array => vinyl-fs module )');
156 }
157 }
158
159 initRemotePath() {
160 if (!this.remotePath) {
161 T.log.error('× RemotePath Error: remotePath is not found ( remotePath can be String )');
162 }
163 }
164
165 // path filters
166 initFilters() {
167 if (this.filters && this.filters.length > 0) {
168 this.filters = this.filters.map(f => {
169 f = `!${f}`;
170 this.localPath.push(f);
171 return f;
172 });
173 }
174 }
175
176 // auto
177 initAuth() {
178 if (util.isObject(this.auth)) {
179 this.auth['key'] && (this.authFile = this.auth['key']);
180 this.auth['file'] && (this.authFile = this.auth['file']);
181 }
182 let authFile = T.Path.join(__dirname, this.authFile);
183 if (this.authKey && T.fs.existsSync(authFile)) {
184 let auth = JSON.parse(T.fs.readFileSync(authFile, 'utf8'))[this.authKey];
185 if (!auth) this.emit('error', new Error('Could not find authkey in .ftppass'));
186 if (typeof auth === 'string' && auth.indexOf(':') !== -1) {
187 let authparts = auth.split(':');
188 auth = { user: authparts[0], pass: authparts[1] };
189 }
190 this.user = auth.user;
191 this.pass = auth.pass;
192 }
193 // aliases
194 this.password = this.pass;
195 this.username = this.user;
196 }
197
198 // present key info
199 initPresentKey() {
200 let key = this.key || this.keyLocation || null;
201 if (key && typeof key === 'string') key = { location: key };
202
203 //check for other options that imply a key or if there is no password
204 if (!key && (this.passphrase || this.keyContents || !this.password)) {
205 key = {};
206 }
207
208 if (key) {
209 //aliases
210 key.contents = key.contents || this.keyContents;
211 key.passphrase = key.passphrase || this.passphrase;
212
213 //defaults
214 key.location = key.location || ['~/.ssh/id_rsa', '/.ssh/id_rsa', '~/.ssh/id_dsa', '/.ssh/id_dsa'];
215
216 //type normalization
217 if (!util.isArray(key.location)) key.location = [key.location];
218
219 //resolve all home paths
220 if (key.location) {
221 let home = process.env.HOME || process.env.USERPROFILE;
222 for (let i = 0; i < key.location.length; ++i) if (key.location[i].substr(0, 2) === '~/') key.location[i] = T.Path.resolve(home, key.location[i].replace(/^~\//, ''));
223
224 for (let i = 0, keyPath; (keyPath = key.location[i++]); ) {
225 if (T.fs.existsSync(keyPath)) {
226 key.contents = T.fs.readFileSync(keyPath);
227 break;
228 }
229 }
230 } else if (!key.contents) {
231 this.emit('error', new Error(`Cannot find RSA key, searched: ${key.location.join(', ')} `));
232 }
233 }
234 this.key = key;
235 if (this.key && this.key.contents) {
236 this.keyContents = this.key.contents;
237 this.privateKey = this.keyContents;
238 this.passphrase = this.key.passphrase || this.passphrase;
239 }
240 }
241
242 // new Uploader
243 _initUploader() {
244 this.uploader.on('start', this._onStart.bind(this));
245 this.uploader.on('uploaded', this._onUploaded.bind(this));
246 this.uploader.on('done', this._onDone.bind(this));
247 this.uploader.setType(this.type);
248 this.uploader.start();
249 }
250
251 _onStart({ message }) {
252 this._writeLogFile(`\n-------------Start Log [${dateFormat(Date.now(), 'yyyy-mm-dd')}]-------------\n`);
253 T.log.yellow(this._startText());
254 this._writeLogFile(this._startText());
255 message && T.log.gray(message);
256 message && this._writeLogFile(message);
257 this._isProgressLog && this.loading.start(T.msg.green(this._startText()));
258 }
259
260 _onUploaded(payload) {
261 const { file, realPath, size, error, message } = payload;
262 if (error) {
263 this._writeLogFile(message);
264 return this._isProgressLog ? this.loading.text(message) : T.log.green(T.msg.red(message));
265 }
266 this._writeLogFile(message);
267 util.isFunction(this.onUploadedFileSuccess) ? this.onUploadedFileSuccess.call(this, { file, realPath, size }) : this._isProgressLog ? this.loading.text(message) : T.log.green(message);
268 }
269
270 _onDone(payload) {
271 this._isProgressLog && this.loading.stop();
272 const { fileCount, modCount, status, duration, uploader } = payload;
273 let iText = this._isIncrementType ? `√ [${this.host}] Deploy Status: ` + T.msg.cyan(status) : '';
274 let dText = iText + T.msg.green(`√ [${this.host}] ${this._isFullType ? fileCount + ' ' : ''}files uploaded OK =^_^= (^_^) =^_^= !!!`);
275 util.isFunction(this.onUploadedComplete) ? this.onUploadedComplete.call(this) : T.log.green(dText);
276 this._writeLogFile(dText);
277 this.uploader.stop();
278 T.log.yellow(this._stopText(duration));
279 this._writeLogFile(this._stopText(duration));
280 this._writeLogFile(`-------------End Log-------------\n`);
281 this.emit('deploy_done', { deploy: this });
282 }
283
284 // SSH链接配置项
285 _getConnectOptions() {
286 let options = {
287 host: this.host,
288 port: this.port,
289 username: this.username
290 };
291
292 if (this.password) {
293 options.password = this.password;
294 } else if (this.agent) {
295 options.agent = this.agent;
296 options.agentForward = this.agentForward || false;
297 } else if (this.privateKey && this.passphrase) {
298 options.privateKey = this.privateKey;
299 options.passphrase = this.passphrase;
300 }
301 options.readyTimeout = this.timeout;
302 options.platform = this.platform;
303 options.keepaliveCountMax = this.keepaliveCountMax;
304 return options;
305 }
306
307 _startText() {
308 return `→ [${this.host}] Deploy start ...... `;
309 }
310 _stopText(duration) {
311 return `√ [${this.host}] Deploy done =^_^= (^_^) =^_^= !!!, after ${T.msg.yellow((duration / 1000).toFixed(2) + ' s')} \n`;
312 }
313 _uploadedText(realPath, size) {
314 return `√ [${T.getTime()}] uploaded '${realPath}', ${T.msg.yellow(size / 1000 + ' kb')}`;
315 }
316 _writeLogFile(txt) {
317 txt = txt.replace(/(()?\[\d{2}m)/g, '') + '\n';
318 if (!this.logFile || !util.isString(this.logFile)) return false;
319 if (!T.fs.existsSync(this.logFile)) {
320 T.fs.writeFileSync(this.logFile, txt, 'utf8');
321 } else {
322 T.fs.appendFileSync(this.logFile, txt, 'utf8');
323 }
324 }
325}
326
327module.exports = Deploy;