UNPKG

16.3 kBJavaScriptView Raw
1/**
2 * Created by Rodey on 2018/3/15.
3 * 运程备份操作
4 */
5'use strict';
6
7const util = require('../utils'),
8 extend = require('extend'),
9 SSH2 = require('ssh2').Client,
10 EventEmitter = require('events').EventEmitter,
11 LoadingORA = require('../loadProgress').LoadingORA,
12 dateFormat = require('dateformat'),
13 inquirer = require('inquirer'),
14 Downloader = require('./downloader'),
15 os = require('os'),
16 T = require('../tools');
17
18const prompt = inquirer.createPromptModule();
19
20class Backup extends EventEmitter {
21 constructor(deploy) {
22 super();
23
24 this.deploy = deploy;
25
26 this.ssh2 = null;
27 this.sftp = null;
28
29 this.outPath = null;
30 this.zipPath = null;
31 this.zipName = null;
32 this.created = null;
33 this.count = 0;
34 // 是否即时备份 (每次部署前都备份) * Deprecated
35 // 有时候我们测试环境并非每次部署都备份,或者开发环境需要将编译部署到另外的机器
36 // 这时就可以开发单备份模式,或者通过指定编译环境进行设置
37 this.loop = false;
38 // 备份模式:
39 // remote (默认,远程备份,将备份文件存在远程服务器上,需要有server shell的执行权限 [ zip, unzip, cd ])
40 // local (默认,本地备份,将备份到本地,直接将服务器目录拉取到本地)
41 this.mode = 'local';
42 // 在项目根目录下存储备份列表,可用于rollback操作
43 this.backupFile = T.Path.resolve(process.cwd(), 'backup.json');
44 // 打印方式:
45 // all (打印详细信息)
46 // progress (默认,进度条方式)
47 this.log = this.deploy.log || 'progress';
48 // 进度条
49 this.loadingORA = null;
50 // 备份过滤列表
51 this.filters = [];
52
53 this.options = extend(true, (this.deploy && this.deploy.backup) || {});
54 // 默认不执行备份
55 this.isExecute = true;
56 // 初始化是否执行
57 this.initExecute();
58 // 如果设置不执行,则不进行系列初始化
59 this.options && this.isExecute && this.init();
60 this.deploy && (this.auths = this.deploy._getConnectOptions());
61 // 备注信息
62 this.remark = T.getArg(['m', 'message']);
63 }
64
65 init() {
66 // 初始化备份列表文件
67 this.initStoreFile();
68 // 初始化备份压缩包名和路径
69 this.initZIP();
70 // 初始化模式 (远程 or 本地)
71 this.initMode();
72 // 初始化打印方式 (详细信息 or 简单进度条)
73 this.initLog();
74 // 初始化备份过滤列表
75 this.initFilters();
76 }
77
78 initExecute() {
79 const options = this.options;
80 if (!options) return (this.isExecute = false);
81 if (util.isObject(options)) {
82 this.isExecute = !!options['isExecute'];
83 }
84 if (util.isArray(options)) {
85 this.isExecute = !!options[4];
86 }
87 }
88
89 initStoreFile() {
90 this.backupFile = T.Path.resolve(this.deploy ? this.deploy.basePath : process.cwd(), 'backup.json');
91 const isExists = T.fs.existsSync(this.backupFile);
92 !isExists && this._writeBackupJsonFile();
93 }
94
95 initZIP() {
96 let outPath, name;
97
98 // backup: <备份输出路径>
99 if (util.isString(this.options)) {
100 outPath = this.options;
101 }
102 // backup: { outPath: <备份输出路径>, name: <备份输出名称>, mode: <备份模式>, log: <显示方式> }
103 if (util.isObject(this.options)) {
104 outPath = this.options['outPath'];
105 name = this.options['name'];
106 'mode' in this.options && (this.mode = this.options['mode']);
107 'log' in this.options && (this.log = this.options['log']);
108 }
109 // backup: [<备份输出路径>, <备份输出名称>]
110 if (util.isArray(this.options)) {
111 outPath = this.options[0];
112 name = this.options[1];
113 this.mode = this.options[2] || this.mode;
114 this.log = this.options[3] || this.log;
115 }
116
117 name = (name || '').trim();
118
119 // 如果没有设置name, 则已remotePath的最后目录为name
120 if (!name) {
121 name = this.deploy.remotePath.split(/\/|\\\\/).slice(-1)[0] + '-';
122 }
123
124 this.created = Date.now();
125 const dateString = dateFormat(Date.now(), 'yyyy-mm-dd_HHMMss');
126
127 if (/@time/g.test(name)) {
128 name = name.replace('@time', dateString);
129 } else {
130 name = name + dateString;
131 }
132
133 // 备份名称中不能包含空格或特殊字符
134 if (/[^\w_-]/.test(name)) {
135 T.log.error(`× [${T.getTime()}] backup name field not has special characters or spaces can be included`);
136 }
137
138 this.outPath = outPath;
139 this.zipName = name + (this.mode === 'remote' ? '.zip' : '');
140 this.zipPath = T.Path.join(this.outPath || this.deploy.remotePath, this.zipName);
141 }
142
143 initMode() {
144 this._isRemoteMode = this.mode === 'remote';
145 this._isLocalMode = this.mode === 'local';
146 }
147
148 initLog() {
149 this._isLogAll = this.log === 'all';
150 this.loadingORA = new LoadingORA();
151 }
152
153 initFilters() {
154 if (util.isObject(this.options)) {
155 this.filters = this.options.filters || [];
156 }
157 if (util.isArray(this.options)) {
158 this.filters = this.options[5] || [];
159 }
160 }
161
162 // 清理备份
163 removeBackup() {
164 const name = T.getArg('name');
165 const backlist = this.getBackups();
166 let dates = Object.keys(backlist);
167 dates = dates.filter(date => {
168 let backs = backlist[date];
169 return util.isArray(backs) && backs.length > 0;
170 });
171 if (dates.length === 0) {
172 T.log.error(`× 当前无备份列表`);
173 }
174 if (!name) {
175 prompt([{
176 type: 'list',
177 name: 'date',
178 message: `选择需要清除的备份版本所在的日期 (${dates.length}): `,
179 choices: dates
180 }]).then(awn => {
181 // 显示当前日期下的备份列表
182 this._showBacks(awn.date);
183 });
184 } else {
185 this._removeBacks(name);
186 }
187 }
188
189 _showBacks(date) {
190 let backlist = this.getBackups();
191 let backNames = backlist[date].map(back => back.name);
192 prompt([{
193 type: 'checkbox',
194 name: 'backs',
195 message: `选择需要清除的备份版本 (支持多选): `,
196 choices: backNames
197 }]).then(awn => {
198 // 显示当前日期下的备份列表
199 backNames = awn.backs;
200 backNames.length > 0 &&
201 backNames.map(backName => {
202 this._removeBacks(backName);
203 });
204 });
205 }
206
207 _removeBacks(backName) {
208 backName = backName || T.getArg('name');
209 let backlist = this.getBackups();
210 let backs, backup, date;
211
212 const ms = backName.match(/(\d{4}-\d{1,2}-\d{1,2})/g);
213 if (ms && ms.length > 0) {
214 date = ms[0];
215 } else {
216 T.log.error(`× 未找到指定的备份`);
217 }
218 if (date) {
219 backs = backlist[date];
220 backup = backs.find(back => back.name === backName);
221 }
222
223 if (backup) {
224 if (backup.mode === 'local') {
225 // 本地
226 T.fs.existsSync(backup.path) && T.fsa.removeSync(backup.path);
227 } else if (backup.mode === 'remote') {
228 // 远程
229 }
230
231 // 更新backup.json
232 const index = backs.indexOf(backup);
233 backs.splice(index, 1);
234 backlist[date] = backs;
235 this._writeBackupJsonFile(JSON.stringify(backlist, null, 4));
236 T.log.yellow(`√ [${backup.server.host}] backup '${backName}]' is remove done `);
237 this.emit('remove_done', {
238 backName,
239 backup
240 });
241 }
242 }
243
244 start() {
245 if (this._isLocalMode) {
246 this._startLocalBackup();
247 } else {
248 this._startRemoteBackup();
249 }
250 }
251
252 // 备份到本地
253 _startLocalBackup() {
254 // 判断本地目录是否存在
255 if (/^\//.test(this.outPath)) {
256 T.log.error(`× Mode is Local, the outPath is not found`);
257 }
258
259 !T.fs.existsSync(this.zipPath) && T.fsa.mkdirsSync(this.zipPath);
260 const remotePath = this.deploy.remotePath;
261
262 const downloader = new Downloader(remotePath, this.zipPath, this.auths, this.filters);
263 downloader.on('start', ({
264 message
265 }) => {
266 T.log.gray(message);
267 });
268 downloader.on('before_download', ({
269 message
270 }) => {
271 T.log.yellow(this._startText());
272 !this._isLogAll && this.loadingORA.start(this._startText());
273 });
274 downloader.on('file_downloaded', ({
275 message
276 }) => {
277 this._isLogAll ? T.log.green(message) : this.loadingORA.text(T.msg.green(message));
278 });
279 downloader.on('error', ({
280 message,
281 directory
282 }) => {
283 message = `${this.auths.host} ${message}`;
284 if (directory === remotePath) {
285 this._isLogAll ? T.log.error(message) : this.loadingORA.text(T.msg.red(message));
286 }
287 });
288 downloader.on('done', this._onDone.bind(this));
289 downloader.start();
290 }
291
292 _mkdirectory(dir, cb) {
293 this.sftp.mkdir(dir, {
294 mode: '0755'
295 }, err => {
296 if (err) {
297 T.log.error(`× [${T.getTime()}] '${dir}' mkdir Failed`);
298 } else {
299 T.log.green(`√ [${T.getTime()}] '${dir}' mkdir Successfully`);
300 cb && util.isFunction(cb) && cb.call(this, dir);
301 }
302 });
303 }
304
305 _hasExists(dir, cb) {
306 this.sftp.exists(dir, isExists => {
307 !isExists && this.mkdirectory(dir);
308 cb && util.isFunction(cb) && cb.call(this, isExists, dir);
309 });
310 }
311
312 _onReady() {
313 this.ssh2.sftp((err, sftp) => {
314 if (err) T.log.error(err.message);
315 this.sftp = sftp;
316 this._preBackup.call(this);
317 });
318 }
319
320 _preBackup() {
321 // 检查备份输入路径
322 this._hasExists(this.deploy.remotePath, isExists => {
323 if (isExists) {
324 // 检查备份输出路径
325 this._hasExists(this.outPath, isExistsZipPath => {
326 if (isExistsZipPath) {
327 this._backup.call(this);
328 } else {
329 this._mkdirectory(this.outPath, () => {
330 this._backup.call(this);
331 });
332 }
333 });
334 }
335 });
336 }
337
338 // 远程服务器上备份
339 _startRemoteBackup() {
340 if (!this.ssh2) {
341 this.ssh2 = new SSH2();
342 }
343
344 this.ssh2.on('ready', this._onReady.bind(this));
345 this.ssh2.connect(this.auths);
346 }
347
348 _backup() {
349 const host = this.deploy.host;
350 const {
351 password,
352 privateKey
353 } = this.auths;
354 this.zipPath = this.zipPath.replace(/\\\\?/g, '/');
355 const command = `cd ${this.deploy.remotePath} && zip -q -r ${this.zipPath} *`;
356
357 let message = this._startText();
358 T.log.yellow(message);
359
360 if (password) {
361 T.log.gray(`→ [${host}] Authenticating with password.`);
362 } else if (privateKey) {
363 T.log.gray(`→ [${host}] Authenticating with private key.`);
364 }
365
366 this.loadingORA.start(message);
367
368 this.ssh2.exec(command, (err, stream) => {
369 if (err) {
370 this.loadingORA.stop();
371 T.log.error(`× [${host}] Remote server exec command failed \n\t ${err.message}`);
372 }
373 stream.on('close', err => {
374 if (err) {
375 this.loadingORA.fail(`× [${host}] Backup failed`);
376 }
377 this.loadingORA.stop();
378 this._onDone();
379 });
380
381 stream.on('data', data => {});
382 });
383 }
384
385 _onDone({
386 message,
387 state,
388 directory
389 }) {
390 !this._isLogAll && this.loadingORA.stop();
391 if (state !== 200) {
392 T.log.yellow(T.msg.red(message));
393 T.fs.existsSync(this.zipPath) && this.deploy.remotePath === directory && T.fsa.removeSync(this.zipPath);
394 } else {
395 this.setBackup();
396 }
397 // 打印相关信息
398 let info = this._getBackupInfo();
399 ['name', 'date', 'user', 'rollback'].forEach(name => {
400 delete info[name];
401 });
402 const status = JSON.stringify(info, null, 4)
403 .replace(/^\{\n|\n\}$/g, '')
404 .replace(/\"/g, '');
405 T.log.yellow(`√ [${this.deploy.host}] Backup Status: `);
406 T.log.cyan(status);
407 T.log.yellow(this._stopText());
408 this.emit('backup_done', {
409 deploy: this.deploy,
410 backup: this
411 });
412 }
413
414 // 写入备份列表文件
415 _writeBackupJsonFile(jsonData) {
416 T.fsa.writeFileSync(this.backupFile, jsonData || '{}', 'utf8');
417 }
418
419 // 将备份信息写入文件
420 setBackup(backupObj) {
421 const today = dateFormat(Date.now(), 'yyyy-mm-dd');
422
423 let backs = this.getBackups();
424 let back = backs[today] || [];
425 !util.isArray(back) && (back = []);
426 back.push(backupObj || this._getBackupInfo());
427
428 backs[today] = back;
429 // write
430 this._writeBackupJsonFile(JSON.stringify(backs, null, 4));
431 }
432
433 // 获取备份列表
434 getBackups() {
435 let backs = null;
436 try {
437 backs = require(this.backupFile);
438 } catch (e) {
439 T.log.error(`× [${T.getTime()}] '${this.backupFile}' is not found \n ${e.message}`);
440 }
441 return backs || {};
442 }
443
444 // 更新备份(回滚操作之后)
445 updateBackup(name) {
446 let backs = this.getBackups();
447 for (let back in backs) {
448 if (backs.hasOwnProperty(back)) {
449 backs[back].forEach(b => {
450 if (b.name === name) {
451 b.rollback = true;
452 }
453 });
454 }
455 }
456 this._writeBackupJsonFile(JSON.stringify(backs, null, 4));
457 }
458
459 _getBackupInfo() {
460 const userInfo = os.userInfo();
461 return {
462 // 创建备份时间
463 date: dateFormat(this.created, 'yyyy-mm-dd HH:MM:ss'),
464 // 输入路径
465 from: this.deploy.remotePath,
466 // 输出路径
467 to: this.outPath,
468 // 备份包地址
469 path: this.zipPath,
470 // 名称
471 name: this.zipName,
472 // 当前执行备份的本地系统用户名
473 user: userInfo.username,
474 // 备份类型,远程 or 本地
475 mode: this.mode,
476 // 此备份是否已回滚
477 rollback: false,
478 // 远程server
479 server: {
480 user: this.deploy.username,
481 host: this.deploy.host
482 },
483 remark: this.remark || ''
484 };
485 }
486
487 _startText() {
488 return `→ [${this.deploy.host}] Backup start ......`;
489 }
490 _stopText() {
491 return `√ [${this.deploy.host}] Backup done =^_^= (^_^) =^_^= !!! \n`;
492 }
493}
494
495module.exports = Backup;
\No newline at end of file