UNPKG

9.5 kBJavaScriptView Raw
1/**
2 * Created by Rodey on 2018/3/15.
3 * 回滚操作
4 */
5
6'use strict';
7
8const util = require('../utils'),
9 extend = require('extend'),
10 SSH2 = require('ssh2').Client,
11 inquirer = require('inquirer'),
12 EventEmitter = require('events').EventEmitter,
13 LoadingORA = require('../loadProgress').LoadingORA,
14 Uploader = require('./uploader'),
15 Backup = require('./backup'),
16 T = require('../tools');
17
18const prompt = inquirer.createPromptModule();
19
20class Rollback extends EventEmitter {
21 constructor(deploies) {
22 super();
23 this.deploies = deploies;
24 this.backups = null;
25 // 回滚时选择需要回滚的版本所在的日期 (命令参数 --backup-date)
26 // 然后将显示所选日期下的所有备份版本供选择回滚
27 // 如果没有指定备份日期,将显示所有备份日期供选择,接着显示已选择日期下的所有可供回滚版本
28 this.selectBackupDate = T.getArg('backup-date');
29 this.selectBackup = T.getArg('backup-name');
30
31 this.init();
32 }
33
34 init() {
35 // 获取当前项目的备份列表
36 this.initBackups();
37 }
38
39 // 项目根目录必须存在backups.json
40 // 回滚操作必须依赖备份
41 initBackups() {
42 const hasBackupDepoly = this.deploies.filter(deploy => deploy.backup)[0];
43 if (!hasBackupDepoly) {
44 T.log.red(`× 未找到backup相关配置对象(config.deploy.backup), 多节点时至少需要一个节点配置backup,以供回滚使用`);
45 }
46 this.hasBackupDepoly = new Backup(hasBackupDepoly);
47 this.backups = this.hasBackupDepoly.getBackups();
48 }
49
50 initSSH() {
51 if (!this.ssh2) {
52 this.ssh2 = new SSH2();
53 }
54 this.ssh2.on('ready', this._onReady.bind(this));
55 this.ssh2.on('error', this.deploy.onError.bind(this.deploy));
56 this.ssh2.on('end', this.deploy.onSSHEnd.bind(this.deploy));
57 this.ssh2.on('close', this.deploy.onClose.bind(this.deploy));
58 }
59
60 initDateList() {
61 // 如果指定了备份的版本名称
62 if (this.selectBackup) {
63 const ms = this.selectBackup.match(/(\d{4}-\d{1,2}-\d{1,2})/g);
64 if (ms && ms.length > 0) {
65 this.selectBackupDate = ms[0];
66 this.selectBackup = this._getBackup(this.selectBackupDate, this.selectBackup);
67 return this._selectedBackup();
68 } else {
69 T.log.error(`× 未找到指定的备份`);
70 }
71 }
72 if (this.selectBackupDate || /^\d{4}-\d{1,2}-\d{1,2}$/.test(this.selectBackupDate)) {
73 // 如果指定命令参数 (--back-date)
74 this.showBackupList(this.selectBackupDate);
75 } else {
76 // 显示备份日期列表供选择
77 this.showDateList();
78 }
79 }
80
81 showDateList() {
82 // 显示备份日期列表供选择
83 const backups = this.backups;
84 // 过滤空的备份列表
85 const dates = Object.keys(backups).filter(date => backups[date].length > 0);
86
87 if (dates.length === 0) {
88 T.log.error('× A version of a rollback version is not found');
89 }
90
91 prompt([
92 {
93 type: 'list',
94 name: 'date',
95 message: `选择需要回滚的备份版本所在的日期 (${dates.length}): `,
96 choices: dates
97 }
98 ]).then(awn => {
99 this.selectBackupDate = awn.date;
100 this.showBackupList();
101 });
102 }
103
104 showBackupList(date) {
105 const backs = this.backups[date || this.selectBackupDate];
106 const names = backs.map(back => `${back.name} (${back.mode})`);
107 prompt([
108 {
109 type: 'list',
110 name: 'name',
111 message: `选择需要回滚的备份版本(${names.length}): `,
112 choices: names
113 }
114 ]).then(awn => {
115 const name = this._trimName(awn.name);
116 this.selectBackup = backs.find(back => back.name === name);
117 this._selectedBackup();
118 });
119 }
120
121 // 执行回滚
122 start() {
123 this.initDateList();
124 }
125
126 stop() {
127 this.sftp && this.sftp.end();
128 this.ssh2 && this.ssh2.end();
129 }
130
131 _selectedBackup() {
132 const { from, mode } = this.selectBackup;
133 this.mode = mode;
134
135 if (!from) {
136 T.log.error('× 未指定回滚目标目录');
137 }
138
139 this._execute();
140 }
141
142 _getBackup(date, name) {
143 const backs = this.backups[date || this.selectBackupDate];
144 return backs.filter(back => this._trimName(back.name) === name)[0];
145 }
146
147 _onDone() {
148 this.stop();
149 process.exit(0);
150 }
151
152 _onReady(deploy, ssh2, recursion) {
153 const { from, to, path, name } = this.selectBackup;
154 const host = deploy.host,
155 auths = deploy._getConnectOptions(),
156 { password, privateKey } = auths;
157 const command = `unzip -o -q ${path} -d ${deploy.remotePath}`;
158
159 this._startRollbackLog(host);
160
161 if (password) {
162 T.log.gray(`→ [${host}] Authenticating with password.`);
163 } else if (privateKey) {
164 T.log.gray(`→ [${host}] Authenticating with private key.`);
165 }
166
167 const loading = new LoadingORA();
168 loading.start(`→ [${host}] Rollback start ......`);
169 const time = Date.now();
170
171 ssh2.exec(command, (err, stream) => {
172 if (err) {
173 return T.log.error(`× [${host}] Remote server exec command failed \n\t ${err.message}`);
174 }
175 stream.on('close', err => {
176 if (err) {
177 return T.log.error(`× [${host}] Rollback failed \n\t ${err.message}`);
178 }
179 stream.end();
180 this._updateBackup();
181 loading.stop(`√ [${host}] Rollback done =^_^= (^_^) =^_^= !!!, after ${T.msg.yellow(((Date.now() - time) / 1000).toFixed(2) + ' s')} \n`);
182
183 // 完成
184 this.deploies.splice(0, 1);
185 if (this.deploies.length > 0) {
186 recursion.call(this, this.deploies[0]);
187 } else {
188 this.emit('rollback_done', { deploy, rollback: this });
189 }
190 });
191 stream.on('data', data => {});
192 });
193 }
194
195 // 开始上传 (根据模式)
196 _execute() {
197 // 监听回滚完成
198 this.on('rollback_done', this._onDone.bind(this));
199 // 如果有多节点,应该递归处理
200 this._recursion.call(this, this.deploies[0]);
201 }
202
203 // 远程备份回滚(采用远程unzip命令)
204 _executeUNZIP(deploy) {
205 this._initSSH(deploy);
206 }
207
208 // 本地备份回滚(采用sftp上传覆盖)
209 _executeSFTP(deploy) {
210 this._initUploader(deploy, this._recursion);
211 }
212
213 // 如果有多节点,应该递归处理
214 _recursion(deploy) {
215 const host = deploy.host,
216 auths = deploy._getConnectOptions();
217
218 if (!host && !util.isString(host)) {
219 return T.log.error(`× server host is undefined`);
220 }
221
222 // 根据备份模式进行回滚
223 if (this.mode === 'local') {
224 this._executeSFTP(deploy);
225 } else if (this.mode === 'remote') {
226 this._executeUNZIP(deploy);
227 }
228 }
229
230 _initSSH(deploy) {
231 const auths = deploy._getConnectOptions();
232 const ssh2 = new SSH2();
233 ssh2.on('ready', this._onReady.bind(this, deploy, ssh2, this._recursion));
234 ssh2.on('error', err => T.log.error(err.message));
235 ssh2.connect(auths);
236 }
237
238 _initUploader(deploy, recursion) {
239 const { from, to, path, name } = this.selectBackup;
240 const host = deploy.host,
241 auths = deploy._getConnectOptions(),
242 localPath = [T.Path.join(path, '/**/*')];
243
244 const uploader = new Uploader(localPath, deploy.remotePath, auths);
245 uploader.on('start', () => {
246 this._startRollbackLog(host);
247 });
248 uploader.on('uploaded', ({ realPath, size }) => {
249 T.log.green(`√ [${T.getTime()}] uploaded '${realPath}', ${T.msg.yellow(size / 1000 + ' kb')}`);
250 });
251 uploader.on('done', ({ fileCount, modCount, status, duration }) => {
252 this._finishRollbackLog(host);
253
254 this.deploies.splice(0, 1);
255 if (this.deploies.length > 0) {
256 recursion.call(this, this.deploies[0]);
257 } else {
258 this.emit('rollback_done', { deploy, rollback: this });
259 }
260 });
261 uploader.setType('full');
262 uploader.start();
263 }
264
265 _startRollbackLog(host) {
266 T.log.yellow(`→ [${host}] Rollback start ......`);
267 }
268 _finishRollbackLog(host) {
269 T.log.yellow(`√ [${host}] Rollback done =^_^= (^_^) =^_^= !!! \n`);
270 }
271 _updateBackup() {
272 this.hasBackupDepoly.updateBackup(this.selectBackup.name);
273 }
274 _trimName(name) {
275 return name.replace(/^([\s\S]+?)\s*\([\s\S]*?\)$/g, '$1').replace(/^\s*|\s*$/g, '');
276 }
277}
278
279module.exports = Rollback;