UNPKG

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