1 |
|
2 |
|
3 |
|
4 |
|
5 | 'use strict';
|
6 |
|
7 | const 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 |
|
18 | const prompt = inquirer.createPromptModule();
|
19 |
|
20 | class 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 |
|
35 |
|
36 |
|
37 | this.loop = false;
|
38 |
|
39 |
|
40 |
|
41 | this.mode = 'local';
|
42 |
|
43 | this.backupFile = T.Path.resolve(process.cwd(), 'backup.json');
|
44 |
|
45 |
|
46 |
|
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 |
|
71 | this.initMode();
|
72 |
|
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 |
|
99 | if (util.isString(this.options)) {
|
100 | outPath = this.options;
|
101 | }
|
102 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
475 | mode: this.mode,
|
476 |
|
477 | rollback: false,
|
478 |
|
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 |
|
495 | module.exports = Backup; |
\ | No newline at end of file |