add eslint, apply formatting
This commit is contained in:
parent
90a0060f56
commit
62a8da129a
15
.eslintrc.json
Normal file
15
.eslintrc.json
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"commonjs": true,
|
||||||
|
"es2021": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"google"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
}
|
||||||
|
}
|
135
archiver.js
135
archiver.js
|
@ -1,78 +1,79 @@
|
||||||
|
/* eslint-disable require-jsdoc */
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { EventEmitter } = require('events');
|
const {EventEmitter} = require('events');
|
||||||
|
|
||||||
class Archiver {
|
class Archiver {
|
||||||
constructor(backupPath, backupPaths, gzip, gzipLevel) {
|
constructor(backupPath, backupPaths, gzip, gzipLevel) {
|
||||||
this.backupPath = backupPath;
|
this.backupPath = backupPath;
|
||||||
this.backupPaths = backupPaths;
|
this.backupPaths = backupPaths;
|
||||||
this.eventEmitter = new EventEmitter();
|
this.eventEmitter = new EventEmitter();
|
||||||
this.archive = require('archiver')('tar', {
|
this.archive = require('archiver')('tar', {
|
||||||
gzip: gzip,
|
gzip: gzip,
|
||||||
gzipOptions: { level: gzipLevel }
|
gzipOptions: {level: gzipLevel},
|
||||||
});
|
});
|
||||||
this.totalFiles = 0;
|
this.totalFiles = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
const EVENT_EMITTER = this.eventEmitter;
|
||||||
|
const ARCHIVE_LOCATION = path.join(this.backupPath);
|
||||||
|
const OUTPUT = fs.createWriteStream(ARCHIVE_LOCATION);
|
||||||
|
|
||||||
|
OUTPUT.on('close', function() {
|
||||||
|
EVENT_EMITTER.emit('finish');
|
||||||
|
});
|
||||||
|
|
||||||
|
OUTPUT.on('end', function() {
|
||||||
|
console.log('Data has been drained');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.archive.on('warning', function(err) {
|
||||||
|
if (err.code === 'ENOENT') console.log(err);
|
||||||
|
else throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.archive.on('error', function(err) {
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.archive.on('progress', function(progressInfo) {
|
||||||
|
EVENT_EMITTER.emit('progress', progressInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
// pipe archive data to the file
|
||||||
|
this.archive.pipe(OUTPUT);
|
||||||
|
|
||||||
|
for (const ITEM of this.backupPaths) {
|
||||||
|
if (!fs.existsSync(ITEM)) continue;
|
||||||
|
const IS_DIR = fs.lstatSync(ITEM).isDirectory();
|
||||||
|
if (IS_DIR) {
|
||||||
|
this.archive.directory(ITEM, path.basename(ITEM));
|
||||||
|
const COUNTED = require('count-files-dirs').countSync(ITEM);
|
||||||
|
this.totalFiles += COUNTED.fileCount + COUNTED.dirCount;
|
||||||
|
} else {
|
||||||
|
this.archive.file(ITEM, {name: path.basename(ITEM)});
|
||||||
|
this.totalFiles++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
start() {
|
add(item) {
|
||||||
const EVENT_EMITTER = this.eventEmitter;
|
if (!fs.existsSync(item)) return;
|
||||||
const ARCHIVE_LOCATION = path.join(this.backupPath);
|
const IS_DIR = fs.lstatSync(item).isDirectory();
|
||||||
const OUTPUT = fs.createWriteStream(ARCHIVE_LOCATION);
|
if (IS_DIR) {
|
||||||
|
this.archive.directory(item, path.basename(item));
|
||||||
OUTPUT.on('close', function () {
|
const COUNTED = require('count-files-dirs').countSync(item);
|
||||||
EVENT_EMITTER.emit('finish');
|
this.totalFiles += COUNTED.fileCount + COUNTED.dirCount;
|
||||||
});
|
} else {
|
||||||
|
this.archive.file(item, {name: path.basename(item)});
|
||||||
OUTPUT.on('end', function () {
|
this.totalFiles++;
|
||||||
console.log('Data has been drained');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.archive.on('warning', function (err) {
|
|
||||||
if (err.code === 'ENOENT') console.log(err);
|
|
||||||
else throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.archive.on('error', function (err) {
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
this.archive.on('progress', function (progressInfo) {
|
|
||||||
EVENT_EMITTER.emit('progress', progressInfo);
|
|
||||||
});
|
|
||||||
|
|
||||||
// pipe archive data to the file
|
|
||||||
this.archive.pipe(OUTPUT);
|
|
||||||
|
|
||||||
for (const ITEM of this.backupPaths) {
|
|
||||||
if (!fs.existsSync(ITEM)) continue;
|
|
||||||
const IS_DIR = fs.lstatSync(ITEM).isDirectory();
|
|
||||||
if (IS_DIR) {
|
|
||||||
this.archive.directory(ITEM, path.basename(ITEM));
|
|
||||||
const COUNTED = require('count-files-dirs').countSync(ITEM);
|
|
||||||
this.totalFiles += COUNTED.fileCount + COUNTED.dirCount;
|
|
||||||
} else {
|
|
||||||
this.archive.file(ITEM, { name: path.basename(ITEM) });
|
|
||||||
this.totalFiles++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
add(item) {
|
pack() {
|
||||||
if (!fs.existsSync(item)) return;
|
this.archive.finalize();
|
||||||
const IS_DIR = fs.lstatSync(item).isDirectory();
|
}
|
||||||
if (IS_DIR) {
|
|
||||||
this.archive.directory(item, path.basename(item));
|
|
||||||
const COUNTED = require('count-files-dirs').countSync(item);
|
|
||||||
this.totalFiles += COUNTED.fileCount + COUNTED.dirCount;
|
|
||||||
} else {
|
|
||||||
this.archive.file(item, { name: path.basename(item) });
|
|
||||||
this.totalFiles++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pack() {
|
|
||||||
this.archive.finalize();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Archiver;
|
module.exports = Archiver;
|
908
index.js
908
index.js
|
@ -1,469 +1,477 @@
|
||||||
const PROMPTS = require('prompts');
|
/* eslint-disable max-len */
|
||||||
const PATH = require('path');
|
const prompts = require('prompts');
|
||||||
const FS = require('fs');
|
const path = require('path');
|
||||||
const GLOB = require('tiny-glob');
|
const fs = require('fs');
|
||||||
const LOGGER = require("./logger");
|
const glob = require('tiny-glob');
|
||||||
const ORA = require('ora');
|
const logger = require('./logger');
|
||||||
const BACKUP_ARCHIVE = require("./archiver");
|
const ora = require('ora');
|
||||||
const MYSQLDUMP = require("mysqldump");
|
const BackupArchive = require('./archiver');
|
||||||
|
const mysqldump = require('mysqldump');
|
||||||
|
|
||||||
const CURRENT_FOLDER = __dirname.startsWith("/snapshot") ? PATH.dirname(process.env._) : __dirname;
|
const CURRENT_FOLDER = __dirname.startsWith('/snapshot') ? path.dirname(process.env._) : __dirname;
|
||||||
const TASKS_DIRECTORY = PATH.join(CURRENT_FOLDER, 'tasks');
|
const TASKS_DIRECTORY = path.join(CURRENT_FOLDER, 'tasks');
|
||||||
const BACKUPS_DIRECTORY = PATH.join(CURRENT_FOLDER, 'backups');
|
const BACKUPS_DIRECTORY = path.join(CURRENT_FOLDER, 'backups');
|
||||||
const TEMP_DIR = PATH.join(CURRENT_FOLDER, '.temp');
|
const TEMP_DIR = path.join(CURRENT_FOLDER, '.temp');
|
||||||
|
|
||||||
const run = async (programArgs) => {
|
const run = async (programArgs) => {
|
||||||
switch (programArgs.action) {
|
const ACTION = programArgs.action.toLowerCase();
|
||||||
case "exit":
|
switch (ACTION) {
|
||||||
LOGGER.info("exiting...");
|
case 'exit':
|
||||||
break;
|
logger.info('exiting...');
|
||||||
case "backup": {
|
break;
|
||||||
let taskName = programArgs.filename ? programArgs.filename + ".json" : "";
|
case 'createcrontab': {
|
||||||
const AUTO_COMPLETE_ARRAY = new Array();
|
logger.info('not added yet.');
|
||||||
const FILES = await GLOB(PATH.join('*.json'), {
|
|
||||||
filesOnly: true,
|
|
||||||
cwd: TASKS_DIRECTORY
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const FILE of FILES) {
|
|
||||||
AUTO_COMPLETE_ARRAY.push({
|
|
||||||
title: FILE,
|
|
||||||
value: FILE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskName === undefined || taskName === "") {
|
|
||||||
const PROMPT = await PROMPTS({
|
|
||||||
type: "autocomplete",
|
|
||||||
name: "filename",
|
|
||||||
message: "Select a Task file to use",
|
|
||||||
choices: AUTO_COMPLETE_ARRAY,
|
|
||||||
validate: value => FILES.includes(value + ".json") ? true : "Task file does not exist, please specify a valid task file."
|
|
||||||
});
|
|
||||||
|
|
||||||
taskName = PROMPT.filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskName === undefined)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!FS.existsSync(PATH.join(TASKS_DIRECTORY, taskName))) {
|
|
||||||
LOGGER.error("task file does not exist");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const TASK_FILE_CONTENTS = JSON.parse(FS.readFileSync(PATH.join(TASKS_DIRECTORY, taskName), 'utf8'));
|
|
||||||
const VALID_TASK_FILE = validate(TASK_FILE_CONTENTS);
|
|
||||||
if (VALID_TASK_FILE.length > 0) {
|
|
||||||
LOGGER.error("task file is not valid, " + VALID_TASK_FILE.length + " errors found");
|
|
||||||
LOGGER.info("check with '" + process.argv0 + " checkTaskConf " + PATH.parse(taskName).name + "'");
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
LOGGER.success("task file is valid");
|
|
||||||
const TIMER = new (require('./timer'))().startTimer();
|
|
||||||
const SPINNER = ORA('initialization...').start();
|
|
||||||
if (!FS.existsSync(PATH.join(BACKUPS_DIRECTORY, PATH.parse(taskName).name)))
|
|
||||||
await FS.promises.mkdir(PATH.join(BACKUPS_DIRECTORY, PATH.parse(taskName).name));
|
|
||||||
else {
|
|
||||||
if (TASK_FILE_CONTENTS.vacuum.enabled) {
|
|
||||||
SPINNER.text = "cleaning up old backups...";
|
|
||||||
const ALL_FILES = FS.readdirSync(PATH.join(BACKUPS_DIRECTORY, PATH.parse(taskName).name));
|
|
||||||
const CURRENT_DATE = Date.now();
|
|
||||||
for (const FILE of ALL_FILES) {
|
|
||||||
const FILE_DATE = new Date(FS.statSync(PATH.join(BACKUPS_DIRECTORY, PATH.parse(taskName).name, FILE)).birthtime).getTime();
|
|
||||||
let timeAdd = 0;
|
|
||||||
switch (TASK_FILE_CONTENTS.vacuum.unit) {
|
|
||||||
case "DAYS":
|
|
||||||
timeAdd = TASK_FILE_CONTENTS.vacuum.time * 24 * 60 * 60 * 1000;
|
|
||||||
break;
|
|
||||||
case "HOURS":
|
|
||||||
timeAdd = TASK_FILE_CONTENTS.vacuum.time * 60 * 60 * 1000;
|
|
||||||
break;
|
|
||||||
case "MINUTES":
|
|
||||||
timeAdd = TASK_FILE_CONTENTS.vacuum.time * 60 * 1000;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const DELETE_DATE = new Date(FILE_DATE + timeAdd);
|
|
||||||
if (DELETE_DATE.getTime() < CURRENT_DATE) {
|
|
||||||
await FS.unlinkSync(PATH.join(BACKUPS_DIRECTORY, PATH.parse(taskName).name, FILE));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const UNREPLACED_FILENAME = TASK_FILE_CONTENTS.general.outputFile;
|
|
||||||
const REPLACED_FILENAME = UNREPLACED_FILENAME.replace("{date}", require('moment')().format(TASK_FILE_CONTENTS.general.dateFormat))
|
|
||||||
.replace("{taskName}", PATH.parse(taskName).name);
|
|
||||||
|
|
||||||
const DB_FILE = PATH.join(TEMP_DIR, require('crypto').randomBytes(16).toString('hex') + ".sql");
|
|
||||||
const ARCHIVE = new BACKUP_ARCHIVE(PATH.join(BACKUPS_DIRECTORY, PATH.parse(taskName).name,
|
|
||||||
REPLACED_FILENAME + ".tar.gz"),
|
|
||||||
TASK_FILE_CONTENTS.filesystem.targets,
|
|
||||||
TASK_FILE_CONTENTS.general.gzip,
|
|
||||||
TASK_FILE_CONTENTS.general.gzipLevel);
|
|
||||||
|
|
||||||
ARCHIVE.eventEmitter.on('progress', (progressInfo) => {
|
|
||||||
const TOTAL = ARCHIVE.totalFiles;
|
|
||||||
const PROCESSED = progressInfo.entries.processed;
|
|
||||||
const PERCENTAGE = Math.round((PROCESSED / TOTAL) * 100);
|
|
||||||
SPINNER.text = `Compressing files... ${PROCESSED}/${TOTAL}(${PERCENTAGE}%)`;
|
|
||||||
});
|
|
||||||
|
|
||||||
ARCHIVE.eventEmitter.on('finish', async () => {
|
|
||||||
if (TASK_FILE_CONTENTS.mysql.enabled) {
|
|
||||||
if (DB_FILE !== undefined && FS.existsSync(DB_FILE))
|
|
||||||
await FS.unlinkSync(DB_FILE);
|
|
||||||
}
|
|
||||||
SPINNER.succeed("backup complete, took " + TIMER.endTimer());
|
|
||||||
});
|
|
||||||
|
|
||||||
if (TASK_FILE_CONTENTS.mysql.enabled) {
|
|
||||||
SPINNER.text = `Dumping Database "${TASK_FILE_CONTENTS.mysql.database}"...`;
|
|
||||||
try {
|
|
||||||
await MYSQLDUMP({
|
|
||||||
connection: {
|
|
||||||
host: TASK_FILE_CONTENTS.mysql.host,
|
|
||||||
port: TASK_FILE_CONTENTS.mysql.port,
|
|
||||||
user: TASK_FILE_CONTENTS.mysql.user,
|
|
||||||
password: TASK_FILE_CONTENTS.mysql.password,
|
|
||||||
database: TASK_FILE_CONTENTS.mysql.database
|
|
||||||
},
|
|
||||||
dumpToFile: DB_FILE
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
LOGGER.error(err);
|
|
||||||
await FS.unlinkSync(DB_FILE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SPINNER.text = "Adding Files...";
|
|
||||||
await ARCHIVE.start();
|
|
||||||
|
|
||||||
if (TASK_FILE_CONTENTS.mysql.enabled) {
|
|
||||||
if (FS.existsSync(DB_FILE)) {
|
|
||||||
SPINNER.text = "Adding Database...";
|
|
||||||
await ARCHIVE.add(DB_FILE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ARCHIVE.pack();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "checkTaskConf": {
|
|
||||||
let taskName = programArgs.filename ? programArgs.filename + ".json" : "";
|
|
||||||
const AUTO_COMPLETE_ARRAY = new Array();
|
|
||||||
const FILES = await GLOB(PATH.join('*.json'), {
|
|
||||||
filesOnly: true,
|
|
||||||
cwd: TASKS_DIRECTORY
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const FILE of FILES) {
|
|
||||||
AUTO_COMPLETE_ARRAY.push({
|
|
||||||
title: FILE,
|
|
||||||
value: FILE,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskName === undefined || taskName === "") {
|
|
||||||
const PROMPT = await PROMPTS({
|
|
||||||
type: "autocomplete",
|
|
||||||
name: "filename",
|
|
||||||
message: "Select a Task file to check",
|
|
||||||
choices: AUTO_COMPLETE_ARRAY,
|
|
||||||
validate: value => FILES.includes(value + ".json") ? true : "Task file does not exist, please specify a valid task file."
|
|
||||||
});
|
|
||||||
|
|
||||||
taskName = PROMPT.filename;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskName === undefined)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const TASK_FILE = PATH.join(TASKS_DIRECTORY, taskName);
|
|
||||||
if (!FS.existsSync(TASK_FILE)) {
|
|
||||||
LOGGER.error("Task file does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const TASK_FILE_CONTENTS = await FS.readFileSync(TASK_FILE, 'utf8');
|
|
||||||
const TASK_FILE_TO_CHECK = JSON.parse(TASK_FILE_CONTENTS);
|
|
||||||
|
|
||||||
const VALIDATION_ERRORS = validate(TASK_FILE_TO_CHECK);
|
|
||||||
if (VALIDATION_ERRORS.length > 0) {
|
|
||||||
LOGGER.error("task file is not valid, " + VALIDATION_ERRORS.length + " errors found");
|
|
||||||
for (const ERROR of VALIDATION_ERRORS) LOGGER.error(ERROR);
|
|
||||||
} else {
|
|
||||||
LOGGER.success("Task file is valid!");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "generateTaskConf": {
|
|
||||||
let taskName = programArgs.filename ? programArgs.filename : "";
|
|
||||||
|
|
||||||
if (taskName === undefined || taskName === "") {
|
|
||||||
const PROMPT = await PROMPTS({
|
|
||||||
type: "text",
|
|
||||||
name: "filename",
|
|
||||||
message: "Define a name for the new task file",
|
|
||||||
validate: value => value.length <= 0 || value == "" ? "Please specify a valid file name" : true
|
|
||||||
});
|
|
||||||
|
|
||||||
taskName = PROMPT.filename;
|
|
||||||
|
|
||||||
const TASK_FILE_PATH = PATH.join(TASKS_DIRECTORY, taskName + ".json");
|
|
||||||
if (await FS.existsSync(TASK_FILE_PATH)) {
|
|
||||||
const PROMPT_2 = await PROMPTS({
|
|
||||||
type: "toggle",
|
|
||||||
name: "overwrite",
|
|
||||||
message: "A task file with the same name already exists. Do you want to overwrite it?",
|
|
||||||
active: 'yes',
|
|
||||||
inactive: 'no',
|
|
||||||
initial: false
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!PROMPT_2.overwrite) {
|
|
||||||
LOGGER.info("exiting...");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskName === undefined)
|
|
||||||
return;
|
|
||||||
|
|
||||||
const TASK_FILE_PATH = PATH.join(TASKS_DIRECTORY, taskName + ".json");
|
|
||||||
|
|
||||||
const TASK_CONFIG = {
|
|
||||||
"general": {
|
|
||||||
"dateFormat": "yyyy-MM-DD HH-mm-ss",
|
|
||||||
"outputFile": "{date} - {taskName}",
|
|
||||||
"gzip": true,
|
|
||||||
"gzipLevel": 6
|
|
||||||
},
|
|
||||||
"vacuum": {
|
|
||||||
"enabled": true,
|
|
||||||
"unit": "DAYS",
|
|
||||||
"time": 7
|
|
||||||
},
|
|
||||||
"mysql": {
|
|
||||||
"enabled": true,
|
|
||||||
"host": "localhost",
|
|
||||||
"port": 3306,
|
|
||||||
"user": "",
|
|
||||||
"password": "",
|
|
||||||
"database": "",
|
|
||||||
},
|
|
||||||
"filesystem": {
|
|
||||||
"enabled": true,
|
|
||||||
"targets": [
|
|
||||||
"/home/magento/",
|
|
||||||
"/home/test/testfile.txt"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
const SAVE_FILE = FS.createWriteStream(TASK_FILE_PATH);
|
|
||||||
SAVE_FILE.write(JSON.stringify(TASK_CONFIG, null, 4));
|
|
||||||
SAVE_FILE.end();
|
|
||||||
LOGGER.success("Task file \"" + PATH.basename(TASK_FILE_PATH) + "\" saved successfully");
|
|
||||||
} catch (err) {
|
|
||||||
LOGGER.error(err);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
LOGGER.warn("Unknown action.");
|
|
||||||
cli(true);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
|
case 'backup': {
|
||||||
|
let taskName = programArgs.filename ? programArgs.filename + '.json' : '';
|
||||||
|
const AUTO_COMPLETE_ARRAY = [];
|
||||||
|
const FILES = await glob(path.join('*.json'), {
|
||||||
|
filesOnly: true,
|
||||||
|
cwd: TASKS_DIRECTORY,
|
||||||
|
});
|
||||||
|
|
||||||
const validate = (taskConfig) => {
|
for (const FILE of FILES) {
|
||||||
const ERRORS = new Array();
|
AUTO_COMPLETE_ARRAY.push({
|
||||||
|
title: FILE,
|
||||||
|
value: FILE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (taskConfig.general && typeof taskConfig.general === "object") {
|
if (taskName === undefined || taskName === '') {
|
||||||
if (taskConfig.general.dateFormat && typeof taskConfig.general.dateFormat === "string") {
|
const PROMPT = await prompts({
|
||||||
const DATE_FORMAT = taskConfig.general.dateFormat;
|
type: 'autocomplete',
|
||||||
const DATE_FORMAT_REGEX = /d{1,4}|D{3,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|W{1,2}|[LlopSZN]|"[^"]*"|'[^']*'/g;
|
name: 'filename',
|
||||||
if (!DATE_FORMAT_REGEX.test(DATE_FORMAT)) {
|
message: 'Select a Task file to use',
|
||||||
ERRORS.push("general.dateFormat is not a valid date format");
|
choices: AUTO_COMPLETE_ARRAY,
|
||||||
}
|
validate: (value) => FILES.includes(value + '.json') ? true : 'Task file does not exist, please specify a valid task file.',
|
||||||
} else {
|
|
||||||
ERRORS.push("general.dateFormat is not defined or is not a string");
|
|
||||||
}
|
|
||||||
if (taskConfig.general.outputFile && typeof taskConfig.general.outputFile === "string") {
|
|
||||||
const OUTPUT_FILE = taskConfig.general.outputFile;
|
|
||||||
const OUTPUT_FILE_REGEX = /^([^.]+)$/g;
|
|
||||||
if (!OUTPUT_FILE_REGEX.test(OUTPUT_FILE)) {
|
|
||||||
ERRORS.push("general.outputFile is not a valid output file name, maybe you added a file extension?");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("general.outputFile is not defined or is not a string");
|
|
||||||
}
|
|
||||||
if (typeof taskConfig.general.gzip === "boolean") {
|
|
||||||
if (taskConfig.general.gzip) {
|
|
||||||
if (!taskConfig.general.gzipLevel && typeof taskConfig.general.gzipLevel !== "number") {
|
|
||||||
ERRORS.push("general.gzipLevel is not defined or is not a number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("general.gzip is not defined or is not a boolean");
|
|
||||||
ERRORS.push("general.gzipLevel is not defined or is not a number");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("general section is missing or not an object");
|
|
||||||
ERRORS.push("general.dateFormat is not defined or is not a string");
|
|
||||||
ERRORS.push("general.outputFile is not defined or is not a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskConfig.vacuum && typeof taskConfig.vacuum === "object") {
|
|
||||||
if (typeof taskConfig.vacuum.enabled !== "boolean") {
|
|
||||||
ERRORS.push("vacuum.enabled is not defined or is not a boolean");
|
|
||||||
} else if (taskConfig.vacuum.enabled) {
|
|
||||||
|
|
||||||
if (taskConfig.vacuum.unit && typeof taskConfig.vacuum.unit === "string") {
|
|
||||||
const UNIT = taskConfig.vacuum.unit;
|
|
||||||
const UNIT_REGEX = /^(DAYS|HOURS|MINUTES)$/g;
|
|
||||||
if (!UNIT_REGEX.test(UNIT)) {
|
|
||||||
ERRORS.push("vacuum.unit is not a valid unit, please use DAYS, HOURS or MINUTES");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("vacuum.unit is not defined or is not a string");
|
|
||||||
}
|
|
||||||
if (taskConfig.vacuum.time && typeof taskConfig.vacuum.time === "number") {
|
|
||||||
const TIME = taskConfig.vacuum.time;
|
|
||||||
if (TIME < 1) {
|
|
||||||
ERRORS.push("vacuum.time is not a valid time, please use a number greater than 0");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("vacuum.time is not defined or is not a number");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("vacuum section is missing or not an object");
|
|
||||||
ERRORS.push("vacuum.enabled is not defined or is not a boolean");
|
|
||||||
ERRORS.push("vacuum.unit is not defined or is not a string");
|
|
||||||
ERRORS.push("vacuum.time is not defined or is not a number");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskConfig.mysql && typeof taskConfig.mysql === "object") {
|
|
||||||
if (typeof taskConfig.mysql.enabled !== "boolean") {
|
|
||||||
ERRORS.push("mysql.enabled is not defined or is not a boolean");
|
|
||||||
} else if (taskConfig.mysql.enabled) {
|
|
||||||
if (!taskConfig.mysql.host || typeof taskConfig.mysql.host !== "string") {
|
|
||||||
ERRORS.push("mysql.host is not defined or is not a string");
|
|
||||||
}
|
|
||||||
if (!taskConfig.mysql.port || typeof taskConfig.mysql.port !== "number") {
|
|
||||||
ERRORS.push("mysql.port is not defined or is not a number");
|
|
||||||
}
|
|
||||||
if (!taskConfig.mysql.user || typeof taskConfig.mysql.user !== "string") {
|
|
||||||
ERRORS.push("mysql.user is not defined or is not a string");
|
|
||||||
}
|
|
||||||
if (typeof taskConfig.mysql.password !== "string") {
|
|
||||||
ERRORS.push("mysql.password is not defined or is not a string");
|
|
||||||
}
|
|
||||||
if (!taskConfig.mysql.database || typeof taskConfig.mysql.database !== "string") {
|
|
||||||
ERRORS.push("mysql.database is not defined or is not a string");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("mysql section is missing or not an object");
|
|
||||||
ERRORS.push("mysql.host is not defined or is not a string");
|
|
||||||
ERRORS.push("mysql.port is not defined or is not a number");
|
|
||||||
ERRORS.push("mysql.user is not defined or is not a string");
|
|
||||||
ERRORS.push("mysql.password is not defined or is not a string");
|
|
||||||
ERRORS.push("mysql.database is not defined or is not a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (taskConfig.filesystem && typeof taskConfig.filesystem === "object") {
|
|
||||||
if (typeof taskConfig.filesystem.enabled !== "boolean") {
|
|
||||||
ERRORS.push("filesystem.enabled is not defined or is not a boolean");
|
|
||||||
} else if (taskConfig.filesystem.enabled) {
|
|
||||||
if (taskConfig.filesystem.targets && typeof taskConfig.filesystem.targets === "object") {
|
|
||||||
const TARGETS = taskConfig.filesystem.targets;
|
|
||||||
if (TARGETS.length < 1) {
|
|
||||||
ERRORS.push("filesystem.targets is not defined or is not an array");
|
|
||||||
} else {
|
|
||||||
for (const target of TARGETS) {
|
|
||||||
if (!target || typeof target !== "string") {
|
|
||||||
ERRORS.push("filesystem.targets[] is not defined or is not a valid path");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("filesystem.targets is not defined or is not an array");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ERRORS.push("filesystem section is missing or not an object");
|
|
||||||
ERRORS.push("filesystem.enabled is not defined or is not a boolean");
|
|
||||||
ERRORS.push("filesystem.targets is not defined or is not an array");
|
|
||||||
}
|
|
||||||
|
|
||||||
return ERRORS;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cli = async (forcePrompt) => {
|
|
||||||
const FIRST_RUN = !FS.existsSync(TASKS_DIRECTORY);
|
|
||||||
try {
|
|
||||||
if (!FS.existsSync(TASKS_DIRECTORY)) {
|
|
||||||
LOGGER.info("tasks directory does not exist, creating it...");
|
|
||||||
await FS.mkdirSync(TASKS_DIRECTORY);
|
|
||||||
}
|
|
||||||
if (!FS.existsSync(BACKUPS_DIRECTORY)) {
|
|
||||||
LOGGER.info("backups directory does not exist, creating it...");
|
|
||||||
await FS.mkdirSync(BACKUPS_DIRECTORY);
|
|
||||||
}
|
|
||||||
if (!FS.existsSync(TEMP_DIR)) {
|
|
||||||
await FS.mkdirSync(TEMP_DIR);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
LOGGER.error(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (FIRST_RUN) {
|
|
||||||
const PROMPT = await PROMPTS({
|
|
||||||
type: "toggle",
|
|
||||||
name: "createTask",
|
|
||||||
message: "This is the first time you run this script, do you want to create a new task?",
|
|
||||||
active: 'yes',
|
|
||||||
inactive: 'no',
|
|
||||||
initial: false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (PROMPT.createTask) {
|
taskName = PROMPT.filename;
|
||||||
run({ action: "generateTaskConf" });
|
}
|
||||||
} else cli(true);
|
|
||||||
|
|
||||||
|
if (taskName === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
if (!fs.existsSync(path.join(TASKS_DIRECTORY, taskName))) {
|
||||||
const ARGS = process.argv.slice(2);
|
logger.error('task file does not exist');
|
||||||
let action = ARGS[0];
|
process.exit(1);
|
||||||
let fileName = ARGS[1];
|
}
|
||||||
|
|
||||||
if (action === undefined || forcePrompt) {
|
const TASK_FILE_CONTENTS = JSON.parse(fs.readFileSync(path.join(TASKS_DIRECTORY, taskName), 'utf8'));
|
||||||
const PROMPT = await PROMPTS({
|
const VALID_TASK_FILE = validate(TASK_FILE_CONTENTS);
|
||||||
type: "select",
|
if (VALID_TASK_FILE.length > 0) {
|
||||||
name: "action",
|
logger.error('task file is not valid, ' + VALID_TASK_FILE.length + ' errors found');
|
||||||
message: "What would you like to do?",
|
logger.info('check with \'' + process.argv0 + ' checkTaskConf ' + path.parse(taskName).name + '\'');
|
||||||
choices: [
|
process.exit(1);
|
||||||
{ title: "Create a new task file", value: "generateTaskConf" },
|
}
|
||||||
{ title: "Check a task file", value: "checkTaskConf" },
|
logger.success('task file is valid');
|
||||||
{ title: "Perform a Backup", value: "backup" },
|
const TIMER = new (require('./timer'))().startTimer();
|
||||||
{ title: "Exit", value: "exit" }
|
const SPINNER = ora('initialization...').start();
|
||||||
],
|
if (!fs.existsSync(path.join(BACKUPS_DIRECTORY, path.parse(taskName).name))) {
|
||||||
});
|
await fs.promises.mkdir(path.join(BACKUPS_DIRECTORY, path.parse(taskName).name));
|
||||||
action = PROMPT.action;
|
} else {
|
||||||
|
if (TASK_FILE_CONTENTS.vacuum.enabled) {
|
||||||
|
SPINNER.text = 'cleaning up old backups...';
|
||||||
|
const ALL_FILES = fs.readdirSync(path.join(BACKUPS_DIRECTORY, path.parse(taskName).name));
|
||||||
|
const CURRENT_DATE = Date.now();
|
||||||
|
for (const FILE of ALL_FILES) {
|
||||||
|
const FILE_DATE = new Date(fs.statSync(path.join(BACKUPS_DIRECTORY, path.parse(taskName).name, FILE)).birthtime).getTime();
|
||||||
|
let timeAdd = 0;
|
||||||
|
switch (TASK_FILE_CONTENTS.vacuum.unit) {
|
||||||
|
case 'DAYS':
|
||||||
|
timeAdd = TASK_FILE_CONTENTS.vacuum.time * 24 * 60 * 60 * 1000;
|
||||||
|
break;
|
||||||
|
case 'HOURS':
|
||||||
|
timeAdd = TASK_FILE_CONTENTS.vacuum.time * 60 * 60 * 1000;
|
||||||
|
break;
|
||||||
|
case 'MINUTES':
|
||||||
|
timeAdd = TASK_FILE_CONTENTS.vacuum.time * 60 * 1000;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const DELETE_DATE = new Date(FILE_DATE + timeAdd);
|
||||||
|
if (DELETE_DATE.getTime() < CURRENT_DATE) {
|
||||||
|
await fs.unlinkSync(path.join(BACKUPS_DIRECTORY, path.parse(taskName).name, FILE));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const programArgs = {
|
const UNREPLACED_FILENAME = TASK_FILE_CONTENTS.general.outputFile;
|
||||||
action,
|
const REPLACED_FILENAME = UNREPLACED_FILENAME.replace('{date}', require('moment')().format(TASK_FILE_CONTENTS.general.dateFormat))
|
||||||
filename: fileName
|
.replace('{taskName}', path.parse(taskName).name);
|
||||||
|
|
||||||
|
const DB_FILE = path.join(TEMP_DIR, require('crypto').randomBytes(16).toString('hex') + '.sql');
|
||||||
|
const ARCHIVE = new BackupArchive(path.join(BACKUPS_DIRECTORY, path.parse(taskName).name,
|
||||||
|
REPLACED_FILENAME + '.tar.gz'),
|
||||||
|
TASK_FILE_CONTENTS.filesystem.targets,
|
||||||
|
TASK_FILE_CONTENTS.general.gzip,
|
||||||
|
TASK_FILE_CONTENTS.general.gzipLevel);
|
||||||
|
|
||||||
|
ARCHIVE.eventEmitter.on('progress', (progressInfo) => {
|
||||||
|
const TOTAL = ARCHIVE.totalFiles;
|
||||||
|
const PROCESSED = progressInfo.entries.processed;
|
||||||
|
const PERCENTAGE = Math.round((PROCESSED / TOTAL) * 100);
|
||||||
|
SPINNER.text = `Compressing files... ${PROCESSED}/${TOTAL}(${PERCENTAGE}%)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
ARCHIVE.eventEmitter.on('finish', async () => {
|
||||||
|
if (TASK_FILE_CONTENTS.mysql.enabled) {
|
||||||
|
if (DB_FILE !== undefined && fs.existsSync(DB_FILE)) {
|
||||||
|
await fs.unlinkSync(DB_FILE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
SPINNER.succeed('backup complete, took ' + TIMER.endTimer());
|
||||||
|
});
|
||||||
|
|
||||||
run(programArgs);
|
if (TASK_FILE_CONTENTS.mysql.enabled) {
|
||||||
|
SPINNER.text = `Dumping Database "${TASK_FILE_CONTENTS.mysql.database}"...`;
|
||||||
|
try {
|
||||||
|
await mysqldump({
|
||||||
|
connection: {
|
||||||
|
host: TASK_FILE_CONTENTS.mysql.host,
|
||||||
|
port: TASK_FILE_CONTENTS.mysql.port,
|
||||||
|
user: TASK_FILE_CONTENTS.mysql.user,
|
||||||
|
password: TASK_FILE_CONTENTS.mysql.password,
|
||||||
|
database: TASK_FILE_CONTENTS.mysql.database,
|
||||||
|
},
|
||||||
|
dumpToFile: DB_FILE,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
await fs.unlinkSync(DB_FILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SPINNER.text = 'Adding Files...';
|
||||||
|
await ARCHIVE.start();
|
||||||
|
|
||||||
|
if (TASK_FILE_CONTENTS.mysql.enabled) {
|
||||||
|
if (fs.existsSync(DB_FILE)) {
|
||||||
|
SPINNER.text = 'Adding Database...';
|
||||||
|
await ARCHIVE.add(DB_FILE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ARCHIVE.pack();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
case 'checktaskconf': {
|
||||||
|
let taskName = programArgs.filename ? programArgs.filename + '.json' : '';
|
||||||
|
const AUTO_COMPLETE_ARRAY = [];
|
||||||
|
const FILES = await glob(path.join('*.json'), {
|
||||||
|
filesOnly: true,
|
||||||
|
cwd: TASKS_DIRECTORY,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const FILE of FILES) {
|
||||||
|
AUTO_COMPLETE_ARRAY.push({
|
||||||
|
title: FILE,
|
||||||
|
value: FILE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskName === undefined || taskName === '') {
|
||||||
|
const PROMPT = await prompts({
|
||||||
|
type: 'autocomplete',
|
||||||
|
name: 'filename',
|
||||||
|
message: 'Select a Task file to check',
|
||||||
|
choices: AUTO_COMPLETE_ARRAY,
|
||||||
|
validate: (value) => FILES.includes(value + '.json') ? true : 'Task file does not exist, please specify a valid task file.',
|
||||||
|
});
|
||||||
|
|
||||||
|
taskName = PROMPT.filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskName === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TASK_FILE = path.join(TASKS_DIRECTORY, taskName);
|
||||||
|
if (!fs.existsSync(TASK_FILE)) {
|
||||||
|
logger.error('Task file does not exist');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const TASK_FILE_CONTENTS = await fs.readFileSync(TASK_FILE, 'utf8');
|
||||||
|
const TASK_FILE_TO_CHECK = JSON.parse(TASK_FILE_CONTENTS);
|
||||||
|
|
||||||
|
const VALIDATION_ERRORS = validate(TASK_FILE_TO_CHECK);
|
||||||
|
if (VALIDATION_ERRORS.length > 0) {
|
||||||
|
logger.error('task file is not valid, ' + VALIDATION_ERRORS.length + ' errors found');
|
||||||
|
for (const ERROR of VALIDATION_ERRORS) logger.error(ERROR);
|
||||||
|
} else {
|
||||||
|
logger.success('Task file is valid!');
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'generatetaskconf': {
|
||||||
|
let taskName = programArgs.filename ? programArgs.filename : '';
|
||||||
|
|
||||||
|
if (taskName === undefined || taskName === '') {
|
||||||
|
const PROMPT = await prompts({
|
||||||
|
type: 'text',
|
||||||
|
name: 'filename',
|
||||||
|
message: 'Define a name for the new task file',
|
||||||
|
validate: (value) => value.length <= 0 || value == '' ? 'Please specify a valid file name' : true,
|
||||||
|
});
|
||||||
|
|
||||||
|
taskName = PROMPT.filename;
|
||||||
|
|
||||||
|
const TASK_FILE_PATH = path.join(TASKS_DIRECTORY, taskName + '.json');
|
||||||
|
if (await fs.existsSync(TASK_FILE_PATH)) {
|
||||||
|
const PROMPT_2 = await prompts({
|
||||||
|
type: 'toggle',
|
||||||
|
name: 'overwrite',
|
||||||
|
message: 'A task file with the same name already exists. Do you want to overwrite it?',
|
||||||
|
active: 'yes',
|
||||||
|
inactive: 'no',
|
||||||
|
initial: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!PROMPT_2.overwrite) {
|
||||||
|
logger.info('exiting...');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskName === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TASK_FILE_PATH = path.join(TASKS_DIRECTORY, taskName + '.json');
|
||||||
|
|
||||||
|
const TASK_CONFIG = {
|
||||||
|
'general': {
|
||||||
|
'dateFormat': 'yyyy-MM-DD HH-mm-ss',
|
||||||
|
'outputFile': '{date} - {taskName}',
|
||||||
|
'gzip': true,
|
||||||
|
'gzipLevel': 6,
|
||||||
|
},
|
||||||
|
'vacuum': {
|
||||||
|
'enabled': true,
|
||||||
|
'unit': 'DAYS',
|
||||||
|
'time': 7,
|
||||||
|
},
|
||||||
|
'mysql': {
|
||||||
|
'enabled': true,
|
||||||
|
'host': 'localhost',
|
||||||
|
'port': 3306,
|
||||||
|
'user': '',
|
||||||
|
'password': '',
|
||||||
|
'database': '',
|
||||||
|
},
|
||||||
|
'filesystem': {
|
||||||
|
'enabled': true,
|
||||||
|
'targets': [
|
||||||
|
'/home/magento/',
|
||||||
|
'/home/test/testfile.txt',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const SAVE_FILE = fs.createWriteStream(TASK_FILE_PATH);
|
||||||
|
SAVE_FILE.write(JSON.stringify(TASK_CONFIG, null, 4));
|
||||||
|
SAVE_FILE.end();
|
||||||
|
logger.success('Task file "' + path.basename(TASK_FILE_PATH) + '" saved successfully');
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
logger.warn('Unknown action.');
|
||||||
|
cli(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = (taskConfig) => {
|
||||||
|
const ERRORS = [];
|
||||||
|
|
||||||
|
if (taskConfig.general && typeof taskConfig.general === 'object') {
|
||||||
|
if (taskConfig.general.dateFormat && typeof taskConfig.general.dateFormat === 'string') {
|
||||||
|
const DATE_FORMAT = taskConfig.general.dateFormat;
|
||||||
|
const DATE_FORMAT_REGEX = /d{1,4}|D{3,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|W{1,2}|[LlopSZN]|"[^"]*"|'[^']*'/g;
|
||||||
|
if (!DATE_FORMAT_REGEX.test(DATE_FORMAT)) {
|
||||||
|
ERRORS.push('general.dateFormat is not a valid date format');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('general.dateFormat is not defined or is not a string');
|
||||||
|
}
|
||||||
|
if (taskConfig.general.outputFile && typeof taskConfig.general.outputFile === 'string') {
|
||||||
|
const OUTPUT_FILE = taskConfig.general.outputFile;
|
||||||
|
const OUTPUT_FILE_REGEX = /^([^.]+)$/g;
|
||||||
|
if (!OUTPUT_FILE_REGEX.test(OUTPUT_FILE)) {
|
||||||
|
ERRORS.push('general.outputFile is not a valid output file name, maybe you added a file extension?');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('general.outputFile is not defined or is not a string');
|
||||||
|
}
|
||||||
|
if (typeof taskConfig.general.gzip === 'boolean') {
|
||||||
|
if (taskConfig.general.gzip) {
|
||||||
|
if (!taskConfig.general.gzipLevel && typeof taskConfig.general.gzipLevel !== 'number') {
|
||||||
|
ERRORS.push('general.gzipLevel is not defined or is not a number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('general.gzip is not defined or is not a boolean');
|
||||||
|
ERRORS.push('general.gzipLevel is not defined or is not a number');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('general section is missing or not an object');
|
||||||
|
ERRORS.push('general.dateFormat is not defined or is not a string');
|
||||||
|
ERRORS.push('general.outputFile is not defined or is not a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskConfig.vacuum && typeof taskConfig.vacuum === 'object') {
|
||||||
|
if (typeof taskConfig.vacuum.enabled !== 'boolean') {
|
||||||
|
ERRORS.push('vacuum.enabled is not defined or is not a boolean');
|
||||||
|
} else if (taskConfig.vacuum.enabled) {
|
||||||
|
if (taskConfig.vacuum.unit && typeof taskConfig.vacuum.unit === 'string') {
|
||||||
|
const UNIT = taskConfig.vacuum.unit;
|
||||||
|
const UNIT_REGEX = /^(DAYS|HOURS|MINUTES)$/g;
|
||||||
|
if (!UNIT_REGEX.test(UNIT)) {
|
||||||
|
ERRORS.push('vacuum.unit is not a valid unit, please use DAYS, HOURS or MINUTES');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('vacuum.unit is not defined or is not a string');
|
||||||
|
}
|
||||||
|
if (taskConfig.vacuum.time && typeof taskConfig.vacuum.time === 'number') {
|
||||||
|
const TIME = taskConfig.vacuum.time;
|
||||||
|
if (TIME < 1) {
|
||||||
|
ERRORS.push('vacuum.time is not a valid time, please use a number greater than 0');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('vacuum.time is not defined or is not a number');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('vacuum section is missing or not an object');
|
||||||
|
ERRORS.push('vacuum.enabled is not defined or is not a boolean');
|
||||||
|
ERRORS.push('vacuum.unit is not defined or is not a string');
|
||||||
|
ERRORS.push('vacuum.time is not defined or is not a number');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskConfig.mysql && typeof taskConfig.mysql === 'object') {
|
||||||
|
if (typeof taskConfig.mysql.enabled !== 'boolean') {
|
||||||
|
ERRORS.push('mysql.enabled is not defined or is not a boolean');
|
||||||
|
} else if (taskConfig.mysql.enabled) {
|
||||||
|
if (!taskConfig.mysql.host || typeof taskConfig.mysql.host !== 'string') {
|
||||||
|
ERRORS.push('mysql.host is not defined or is not a string');
|
||||||
|
}
|
||||||
|
if (!taskConfig.mysql.port || typeof taskConfig.mysql.port !== 'number') {
|
||||||
|
ERRORS.push('mysql.port is not defined or is not a number');
|
||||||
|
}
|
||||||
|
if (!taskConfig.mysql.user || typeof taskConfig.mysql.user !== 'string') {
|
||||||
|
ERRORS.push('mysql.user is not defined or is not a string');
|
||||||
|
}
|
||||||
|
if (typeof taskConfig.mysql.password !== 'string') {
|
||||||
|
ERRORS.push('mysql.password is not defined or is not a string');
|
||||||
|
}
|
||||||
|
if (!taskConfig.mysql.database || typeof taskConfig.mysql.database !== 'string') {
|
||||||
|
ERRORS.push('mysql.database is not defined or is not a string');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('mysql section is missing or not an object');
|
||||||
|
ERRORS.push('mysql.host is not defined or is not a string');
|
||||||
|
ERRORS.push('mysql.port is not defined or is not a number');
|
||||||
|
ERRORS.push('mysql.user is not defined or is not a string');
|
||||||
|
ERRORS.push('mysql.password is not defined or is not a string');
|
||||||
|
ERRORS.push('mysql.database is not defined or is not a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (taskConfig.filesystem && typeof taskConfig.filesystem === 'object') {
|
||||||
|
if (typeof taskConfig.filesystem.enabled !== 'boolean') {
|
||||||
|
ERRORS.push('filesystem.enabled is not defined or is not a boolean');
|
||||||
|
} else if (taskConfig.filesystem.enabled) {
|
||||||
|
if (taskConfig.filesystem.targets && typeof taskConfig.filesystem.targets === 'object') {
|
||||||
|
const TARGETS = taskConfig.filesystem.targets;
|
||||||
|
if (TARGETS.length < 1) {
|
||||||
|
ERRORS.push('filesystem.targets is not defined or is not an array');
|
||||||
|
} else {
|
||||||
|
for (const target of TARGETS) {
|
||||||
|
if (!target || typeof target !== 'string') {
|
||||||
|
ERRORS.push('filesystem.targets[] is not defined or is not a valid path');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('filesystem.targets is not defined or is not an array');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ERRORS.push('filesystem section is missing or not an object');
|
||||||
|
ERRORS.push('filesystem.enabled is not defined or is not a boolean');
|
||||||
|
ERRORS.push('filesystem.targets is not defined or is not an array');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ERRORS;
|
||||||
|
};
|
||||||
|
|
||||||
|
const cli = async (forcePrompt) => {
|
||||||
|
const FIRST_RUN = !fs.existsSync(TASKS_DIRECTORY);
|
||||||
|
try {
|
||||||
|
if (!fs.existsSync(TASKS_DIRECTORY)) {
|
||||||
|
logger.info('tasks directory does not exist, creating it...');
|
||||||
|
await fs.mkdirSync(TASKS_DIRECTORY);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(BACKUPS_DIRECTORY)) {
|
||||||
|
logger.info('backups directory does not exist, creating it...');
|
||||||
|
await fs.mkdirSync(BACKUPS_DIRECTORY);
|
||||||
|
}
|
||||||
|
if (!fs.existsSync(TEMP_DIR)) {
|
||||||
|
await fs.mkdirSync(TEMP_DIR);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FIRST_RUN) {
|
||||||
|
const PROMPT = await prompts({
|
||||||
|
type: 'toggle',
|
||||||
|
name: 'createTask',
|
||||||
|
message: 'This is the first time you run this script, do you want to create a new task?',
|
||||||
|
active: 'yes',
|
||||||
|
inactive: 'no',
|
||||||
|
initial: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (PROMPT.createTask) {
|
||||||
|
run({action: 'generateTaskConf'});
|
||||||
|
} else cli(true);
|
||||||
|
} else {
|
||||||
|
const ARGS = process.argv.slice(2);
|
||||||
|
let action = ARGS[0];
|
||||||
|
const fileName = ARGS[1];
|
||||||
|
|
||||||
|
if (action === undefined || forcePrompt) {
|
||||||
|
const PROMPT = await prompts({
|
||||||
|
type: 'select',
|
||||||
|
name: 'action',
|
||||||
|
message: 'What would you like to do?',
|
||||||
|
choices: [
|
||||||
|
{title: 'Create a new task file', value: 'generateTaskConf'},
|
||||||
|
{title: 'Check a task file', value: 'checkTaskConf'},
|
||||||
|
{title: 'Perform a Backup', value: 'backup'},
|
||||||
|
{title: 'Create a Crontab for a task file', value: 'createCrontab'},
|
||||||
|
{title: 'Exit', value: 'exit'},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
action = PROMPT.action;
|
||||||
|
}
|
||||||
|
|
||||||
|
const programArgs = {
|
||||||
|
action,
|
||||||
|
filename: fileName,
|
||||||
|
};
|
||||||
|
|
||||||
|
run(programArgs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
cli(false);
|
cli(false);
|
||||||
|
|
||||||
|
|
26
logger.js
26
logger.js
|
@ -1,17 +1,17 @@
|
||||||
const ansiColors = require('ansi-colors');
|
const ansiColors = require('ansi-colors');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
info(message) {
|
info(message) {
|
||||||
console.log(ansiColors.bold.blue("🛈") + " " + ansiColors.bold(message));
|
console.log(ansiColors.bold.blue('🛈') + ' ' + ansiColors.bold(message));
|
||||||
},
|
},
|
||||||
warn(message) {
|
warn(message) {
|
||||||
console.log(ansiColors.bold.yellow("⚠") + " " + ansiColors.bold(message));
|
console.log(ansiColors.bold.yellow('⚠') + ' ' + ansiColors.bold(message));
|
||||||
},
|
},
|
||||||
error(message) {
|
error(message) {
|
||||||
console.log(ansiColors.red.red("✖") + " " + ansiColors.bold(message));
|
console.log(ansiColors.red.red('✖') + ' ' + ansiColors.bold(message));
|
||||||
},
|
},
|
||||||
success(message) {
|
success(message) {
|
||||||
console.log(ansiColors.green("✔") + " " + ansiColors.bold(message));
|
console.log(ansiColors.green('✔') + ' ' + ansiColors.bold(message));
|
||||||
}
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"eslint": ">=5.16.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
"pkg": "^5.8.0"
|
"pkg": "^5.8.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -21,6 +23,7 @@
|
||||||
"ansi-colors": "^4.1.3",
|
"ansi-colors": "^4.1.3",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
"count-files-dirs": "^0.2.3",
|
"count-files-dirs": "^0.2.3",
|
||||||
|
"crontab": "^1.4.2",
|
||||||
"moment": "^2.29.4",
|
"moment": "^2.29.4",
|
||||||
"mysqldump": "^3.2.0",
|
"mysqldump": "^3.2.0",
|
||||||
"ora": "5.4.1",
|
"ora": "5.4.1",
|
||||||
|
|
17
timer.js
17
timer.js
|
@ -1,13 +1,14 @@
|
||||||
const prettytime = require('prettytime')
|
/* eslint-disable require-jsdoc */
|
||||||
|
const prettytime = require('prettytime');
|
||||||
class Timer {
|
class Timer {
|
||||||
startTimer() {
|
startTimer() {
|
||||||
this.start = Date.now();
|
this.start = Date.now();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
endTimer() {
|
endTimer() {
|
||||||
return prettytime(Date.now() - this.start, { decimals: 2 });
|
return prettytime(Date.now() - this.start, {decimals: 2});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Timer;
|
module.exports = Timer;
|
Loading…
Reference in New Issue
Block a user