UNPKG

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