1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 |
|
8 | const 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 |
|
17 | const prompt = inquirer.createPromptModule();
|
18 | const exitText = '---<< exit <<---';
|
19 |
|
20 | class Rollback extends EventEmitter {
|
21 | constructor(deploies) {
|
22 | super();
|
23 | this.deploies = deploies;
|
24 | this.backups = null;
|
25 |
|
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 |
|
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 |
|
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 |
|
219 | _executeUNZIP(deploy) {
|
220 | this._initSSH(deploy);
|
221 | }
|
222 |
|
223 |
|
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 |
|
310 | module.exports = Rollback; |
\ | No newline at end of file |