UNPKG

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