Compare commits
No commits in common. "archived" and "master" have entirely different histories.
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": {
|
||||||
|
}
|
||||||
|
}
|
71
.gitignore
vendored
71
.gitignore
vendored
|
@ -1,65 +1,6 @@
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
node_modules/
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
backups/
|
||||||
# User-specific stuff
|
tasks/
|
||||||
.idea/**/workspace.xml
|
.temp/
|
||||||
.idea/**/tasks.xml
|
bin/
|
||||||
.idea/**/usage.statistics.xml
|
pnpm-lock.yaml
|
||||||
.idea/**/dictionaries
|
|
||||||
.idea/**/shelf
|
|
||||||
# AWS User-specific
|
|
||||||
.idea/**/aws.xml
|
|
||||||
# Generated files
|
|
||||||
.idea/**/contentModel.xml
|
|
||||||
# Sensitive or high-churn files
|
|
||||||
.idea/**/dataSources/
|
|
||||||
.idea/**/dataSources.ids
|
|
||||||
.idea/**/dataSources.local.xml
|
|
||||||
.idea/**/sqlDataSources.xml
|
|
||||||
.idea/**/dynamic.xml
|
|
||||||
.idea/**/uiDesigner.xml
|
|
||||||
.idea/**/dbnavigator.xml
|
|
||||||
# Gradle
|
|
||||||
.idea/**/gradle.xml
|
|
||||||
.idea/**/libraries
|
|
||||||
# Gradle and Maven with auto-import
|
|
||||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
|
||||||
# since they will be recreated, and may cause churn. Uncomment if using
|
|
||||||
# auto-import.
|
|
||||||
# .idea/artifacts
|
|
||||||
# .idea/compiler.xml
|
|
||||||
# .idea/jarRepositories.xml
|
|
||||||
# .idea/modules.xml
|
|
||||||
# .idea/*.iml
|
|
||||||
# .idea/modules
|
|
||||||
# *.iml
|
|
||||||
# *.ipr
|
|
||||||
# CMake
|
|
||||||
cmake-build-*/
|
|
||||||
# Mongo Explorer plugin
|
|
||||||
.idea/**/mongoSettings.xml
|
|
||||||
# File-based project format
|
|
||||||
*.iws
|
|
||||||
# IntelliJ
|
|
||||||
out/
|
|
||||||
# mpeltonen/sbt-idea plugin
|
|
||||||
.idea_modules/
|
|
||||||
# JIRA plugin
|
|
||||||
atlassian-ide-plugin.xml
|
|
||||||
# Cursive Clojure plugin
|
|
||||||
.idea/replstate.xml
|
|
||||||
# SonarLint plugin
|
|
||||||
.idea/sonarlint/
|
|
||||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
|
||||||
com_crashlytics_export_strings.xml
|
|
||||||
crashlytics.properties
|
|
||||||
crashlytics-build.properties
|
|
||||||
fabric.properties
|
|
||||||
# Editor-based Rest Client
|
|
||||||
.idea/httpRequests
|
|
||||||
# Android studio 3.1+ serialized cache file
|
|
||||||
.idea/caches/build_file_checksums.ser
|
|
||||||
target/
|
|
||||||
run/
|
|
||||||
/target/
|
|
||||||
/run/
|
|
||||||
/dependency-reduced-pom.xml
|
|
3
.idea/.gitignore
vendored
3
.idea/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
|
@ -1,22 +0,0 @@
|
||||||
<component name="ArtifactManager">
|
|
||||||
<artifact type="jar" name="SysBackup:jar">
|
|
||||||
<output-path>$PROJECT_DIR$/out/artifacts/SysBackup_jar</output-path>
|
|
||||||
<root id="archive" name="SysBackup.jar">
|
|
||||||
<element id="module-output" name="SysBackup" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/antlr/antlr4-runtime/4.7.2/antlr4-runtime-4.7.2.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/tomlj/tomlj/1.0.0/tomlj-1.0.0.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/guava/guava/31.1-jre/guava-31.1-jre.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/net/lingala/zip4j/zip4j/2.11.1/zip4j-2.11.1.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/commons-io/commons-io/2.11.0/commons-io-2.11.0.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/me/tongfei/progressbar/0.9.3/progressbar-0.9.3.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/jline/jline/3.21.0/jline-3.21.0.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/errorprone/error_prone_annotations/2.11.0/error_prone_annotations-2.11.0.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/checkerframework/checker-qual/3.12.0/checker-qual-3.12.0.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar" path-in-jar="/" />
|
|
||||||
<element id="extracted-dir" path="$MAVEN_REPOSITORY$/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" path-in-jar="/" />
|
|
||||||
</root>
|
|
||||||
</artifact>
|
|
||||||
</component>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="CompilerConfiguration">
|
|
||||||
<annotationProcessing>
|
|
||||||
<profile default="true" name="Default" enabled="true" />
|
|
||||||
<profile name="Maven default annotation processors profile" enabled="true">
|
|
||||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
|
||||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
|
||||||
<outputRelativeToContentRoot value="true" />
|
|
||||||
<module name="SysBackup" />
|
|
||||||
</profile>
|
|
||||||
</annotationProcessing>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Encoding">
|
|
||||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
|
||||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GoogleJavaFormatSettings">
|
|
||||||
<option name="enabled" value="true" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,20 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="RemoteRepositoriesConfiguration">
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="central" />
|
|
||||||
<option name="name" value="Central Repository" />
|
|
||||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="central" />
|
|
||||||
<option name="name" value="Maven Central repository" />
|
|
||||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
|
||||||
</remote-repository>
|
|
||||||
<remote-repository>
|
|
||||||
<option name="id" value="jboss.community" />
|
|
||||||
<option name="name" value="JBoss Community repository" />
|
|
||||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
|
||||||
</remote-repository>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
|
||||||
<component name="MavenProjectsManager">
|
|
||||||
<option name="originalFiles">
|
|
||||||
<list>
|
|
||||||
<option value="$PROJECT_DIR$/pom.xml" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="11" project-jdk-type="JavaSDK">
|
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": true
|
||||||
|
},
|
||||||
|
"eslint.validate": ["javascript"]
|
||||||
|
}
|
15
CONTRIBUTING.md
Normal file
15
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
# Contributing
|
||||||
|
Thank you for wanting to contribute to the development of SysBackup! Please keep the following in mind when contributing to this repository please keep all of the following in mind as it will prevent headaches in the future
|
||||||
|
|
||||||
|
## Code style
|
||||||
|
- Constants are to be written using SCREAMING_SNAKE_CASE
|
||||||
|
- Classes are to be written using PascalCase
|
||||||
|
- Functions/Variables are written in camelCase
|
||||||
|
- Define variables using `let` and not `var`
|
||||||
|
- Use undefined over null
|
||||||
|
|
||||||
|
## Creating a pull request
|
||||||
|
- When making changes please fork and submit a pull request
|
||||||
|
- Please outline every change made in this pull request, to make the maintainers' job easier
|
||||||
|
- Discussion within pull requests regarding implementation details is always healthy
|
||||||
|
- Once a maintainer agrees your pull request shall be squashed and merged!
|
64
README.md
Normal file
64
README.md
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# SysBackup
|
||||||
|
|
||||||
|
This branch is a rewrite of the [previous in Java Developed branch](https://git.ez-pp.farm/HorizonCode/SysBackup/src/branch/archived), rewritten in NodeJS.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- NodeJS >= 14
|
||||||
|
|
||||||
|
You can either: [Build a executeable](#build-a-executeable) or [Running from Source](#running-from-source)
|
||||||
|
|
||||||
|
### Build a executeable
|
||||||
|
|
||||||
|
#### Extra Requirements
|
||||||
|
- pkg
|
||||||
|
|
||||||
|
#### Aquire SysBackup
|
||||||
|
```bash
|
||||||
|
git clone https://git.ez-pp.farm/HorizonCode/SysBackup
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Installing Dependencies
|
||||||
|
```bash
|
||||||
|
cd /path/to/sysbackupclone/
|
||||||
|
# with NPM
|
||||||
|
npm i
|
||||||
|
# or with PNPM
|
||||||
|
pnpm i
|
||||||
|
# or with Yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# Install pkg global
|
||||||
|
npm i pkg -g
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Building the executeable
|
||||||
|
```bash
|
||||||
|
# with NPM
|
||||||
|
npm run pkg
|
||||||
|
# or with PNPM
|
||||||
|
pnpm run pkg
|
||||||
|
# or with Yarn
|
||||||
|
yarn run pkg
|
||||||
|
```
|
||||||
|
|
||||||
|
Your builded binary executeable should be located in `bin/`
|
||||||
|
|
||||||
|
### Running from Source
|
||||||
|
|
||||||
|
#### Aquire SysBackup
|
||||||
|
```bash
|
||||||
|
git clone https://git.ez-pp.farm/HorizonCode/SysBackup
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Running
|
||||||
|
```bash
|
||||||
|
cd /path/to/sysbackupclone/
|
||||||
|
|
||||||
|
node index.js
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
Please read our [CONTRIBUTING.md](https://git.ez-pp.farm/HorizonCode/SysBackup/src/branch/master/CONTRIBUTING.md) to read how to contribute!
|
79
archiver.js
Normal file
79
archiver.js
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
/* eslint-disable require-jsdoc */
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const {EventEmitter} = require('events');
|
||||||
|
|
||||||
|
class Archiver {
|
||||||
|
constructor(backupPath, backupPaths, gzip, gzipLevel) {
|
||||||
|
this.backupPath = backupPath;
|
||||||
|
this.backupPaths = backupPaths;
|
||||||
|
this.eventEmitter = new EventEmitter();
|
||||||
|
this.archive = require('archiver')('tar', {
|
||||||
|
gzip: gzip,
|
||||||
|
gzipOptions: {level: gzipLevel},
|
||||||
|
});
|
||||||
|
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++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(item) {
|
||||||
|
if (!fs.existsSync(item)) return;
|
||||||
|
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;
|
572
index.js
Normal file
572
index.js
Normal file
|
@ -0,0 +1,572 @@
|
||||||
|
/* eslint-disable max-len */
|
||||||
|
const prompts = require('prompts');
|
||||||
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
|
const glob = require('tiny-glob');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const ora = require('ora');
|
||||||
|
const BackupArchive = require('./archiver');
|
||||||
|
const mysqldump = require('mysqldump');
|
||||||
|
|
||||||
|
const CURRENT_FOLDER = __dirname.startsWith('/snapshot') ?
|
||||||
|
path.dirname(process.env._) :
|
||||||
|
__dirname;
|
||||||
|
const TASKS_DIRECTORY = path.join(CURRENT_FOLDER, 'tasks');
|
||||||
|
const BACKUPS_DIRECTORY = path.join(CURRENT_FOLDER, 'backups');
|
||||||
|
const TEMP_DIR = path.join(CURRENT_FOLDER, '.temp');
|
||||||
|
|
||||||
|
const run = async (programArgs) => {
|
||||||
|
if (programArgs === undefined || programArgs.action === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ACTION = programArgs.action.toLowerCase();
|
||||||
|
switch (ACTION) {
|
||||||
|
case 'exit':
|
||||||
|
logger.info('exiting...');
|
||||||
|
break;
|
||||||
|
case 'createcrontab':
|
||||||
|
{
|
||||||
|
logger.info('not added yet.');
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
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('preparing backup... please wait.').start();
|
||||||
|
|
||||||
|
const FILE_PATHS = [];
|
||||||
|
const fsExtra = require('fs-extra');
|
||||||
|
|
||||||
|
await Promise.all(TASK_FILE_CONTENTS.filesystem.targets.map(async (file) => {
|
||||||
|
if (fs.existsSync(file)) {
|
||||||
|
const newPath = path.join(TEMP_DIR, path.parse(file).name);
|
||||||
|
FILE_PATHS.push(newPath);
|
||||||
|
await fsExtra.copySync(file, newPath);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
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 BackupArchive(
|
||||||
|
path.join(
|
||||||
|
BACKUPS_DIRECTORY,
|
||||||
|
path.parse(taskName).name,
|
||||||
|
REPLACED_FILENAME + '.tar.gz',
|
||||||
|
),
|
||||||
|
FILE_PATHS,
|
||||||
|
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 () => {
|
||||||
|
SPINNER.text = 'Cleaning up temporary files...';
|
||||||
|
if (fs.existsSync(DB_FILE)) {
|
||||||
|
await fs.unlinkSync(DB_FILE);
|
||||||
|
}
|
||||||
|
for (const FILE of FILE_PATHS) {
|
||||||
|
await fsExtra.removeSync(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 = [];
|
||||||
|
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);
|
17
logger.js
Normal file
17
logger.js
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
const ansiColors = require('ansi-colors');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
info(message) {
|
||||||
|
console.log(ansiColors.bold.blue('🛈') + ' ' + ansiColors.bold(message));
|
||||||
|
},
|
||||||
|
warn(message) {
|
||||||
|
console.log(ansiColors.bold.yellow('⚠') + ' ' + ansiColors.bold(message));
|
||||||
|
},
|
||||||
|
error(message) {
|
||||||
|
console.log(ansiColors.red.red('✖') + ' ' + ansiColors.bold(message));
|
||||||
|
},
|
||||||
|
success(message) {
|
||||||
|
console.log(ansiColors.green('✔') + ' ' + ansiColors.bold(message));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
47
package.json
Normal file
47
package.json
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
{
|
||||||
|
"name": "backup",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"run": "node index.js",
|
||||||
|
"run genBackupFile": "node index.js generateTaskConf test",
|
||||||
|
"run checkFile": "node index.js checkTaskConf test",
|
||||||
|
"run backup": "node index.js backup test",
|
||||||
|
"pkg": "pkg index.js -o bin/backup -t node16-linux"
|
||||||
|
},
|
||||||
|
"keywords": [],
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"eslint": ">=5.16.0",
|
||||||
|
"eslint-config-google": "^0.14.0",
|
||||||
|
"pkg": "^5.8.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@horizoncode/eazylog": "^1.0.9",
|
||||||
|
"ansi-colors": "^4.1.3",
|
||||||
|
"archiver": "^5.3.1",
|
||||||
|
"count-files-dirs": "^0.2.3",
|
||||||
|
"crontab": "^1.4.2",
|
||||||
|
"fs-extra": "^10.1.0",
|
||||||
|
"moment": "^2.29.4",
|
||||||
|
"mysqldump": "^3.2.0",
|
||||||
|
"ora": "5.4.1",
|
||||||
|
"prettytime": "^1.0.0",
|
||||||
|
"prompts": "^2.4.2",
|
||||||
|
"tiny-glob": "^0.2.9"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"overrides": {
|
||||||
|
"minimist@<1.2.6": ">=1.2.6",
|
||||||
|
"braces@<2.3.1": ">=2.3.1",
|
||||||
|
"diff@<3.5.0": ">=3.5.0",
|
||||||
|
"minimatch@<3.0.2": ">=3.0.2",
|
||||||
|
"debug@<2.6.9": ">=2.6.9",
|
||||||
|
"minimist@<0.2.1": ">=0.2.1",
|
||||||
|
"glob-parent@<5.1.2": ">=5.1.2",
|
||||||
|
"growl@<1.10.0": ">=1.10.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
102
pom.xml
102
pom.xml
|
@ -1,102 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
|
|
||||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|
||||||
<modelVersion>4.0.0</modelVersion>
|
|
||||||
|
|
||||||
<groupId>net.horizoncode</groupId>
|
|
||||||
<artifactId>SysBackup</artifactId>
|
|
||||||
<version>1.0-SNAPSHOT</version>
|
|
||||||
<packaging>jar</packaging>
|
|
||||||
|
|
||||||
<name>SysBackup</name>
|
|
||||||
|
|
||||||
<properties>
|
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
|
||||||
<maven.compiler.source>11</maven.compiler.source>
|
|
||||||
<maven.compiler.target>11</maven.compiler.target>
|
|
||||||
</properties>
|
|
||||||
|
|
||||||
<dependencies>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jline</groupId>
|
|
||||||
<artifactId>jline</artifactId>
|
|
||||||
<version>3.21.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.tomlj</groupId>
|
|
||||||
<artifactId>tomlj</artifactId>
|
|
||||||
<version>1.0.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.projectlombok</groupId>
|
|
||||||
<artifactId>lombok</artifactId>
|
|
||||||
<version>1.18.24</version>
|
|
||||||
<scope>provided</scope>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>commons-io</groupId>
|
|
||||||
<artifactId>commons-io</artifactId>
|
|
||||||
<version>2.11.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.google.guava</groupId>
|
|
||||||
<artifactId>guava</artifactId>
|
|
||||||
<version>31.1-jre</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>net.lingala.zip4j</groupId>
|
|
||||||
<artifactId>zip4j</artifactId>
|
|
||||||
<version>2.11.1</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>me.tongfei</groupId>
|
|
||||||
<artifactId>progressbar</artifactId>
|
|
||||||
<version>0.9.3</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.apache.commons</groupId>
|
|
||||||
<artifactId>commons-lang3</artifactId>
|
|
||||||
<version>3.12.0</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.diogonunes</groupId>
|
|
||||||
<artifactId>JColor</artifactId>
|
|
||||||
<version>5.5.1</version>
|
|
||||||
</dependency>
|
|
||||||
<!-- testing dependencies -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>junit</groupId>
|
|
||||||
<artifactId>junit</artifactId>
|
|
||||||
<version>4.13.2</version>
|
|
||||||
<scope>test</scope>
|
|
||||||
</dependency>
|
|
||||||
</dependencies>
|
|
||||||
|
|
||||||
<build>
|
|
||||||
<pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
|
|
||||||
<plugins>
|
|
||||||
<plugin>
|
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
|
||||||
<artifactId>maven-shade-plugin</artifactId>
|
|
||||||
<version>3.3.0</version>
|
|
||||||
<executions>
|
|
||||||
<execution>
|
|
||||||
<goals>
|
|
||||||
<goal>shade</goal>
|
|
||||||
</goals>
|
|
||||||
<configuration>
|
|
||||||
<shadedArtifactAttached>true</shadedArtifactAttached>
|
|
||||||
<transformers>
|
|
||||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
|
||||||
<mainClass>Bootstrapper</mainClass>
|
|
||||||
</transformer>
|
|
||||||
</transformers>
|
|
||||||
</configuration>
|
|
||||||
</execution>
|
|
||||||
</executions>
|
|
||||||
</plugin>
|
|
||||||
</plugins>
|
|
||||||
</pluginManagement>
|
|
||||||
</build>
|
|
||||||
</project>
|
|
|
@ -1,10 +0,0 @@
|
||||||
import net.horizoncode.sysbackup.SysBackup;
|
|
||||||
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
public class Bootstrapper {
|
|
||||||
|
|
||||||
public static void main(String[] args) throws URISyntaxException {
|
|
||||||
new SysBackup().start(args);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
package net.horizoncode.sysbackup;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import net.horizoncode.sysbackup.cli.CLIProcessor;
|
|
||||||
import net.horizoncode.sysbackup.logging.Logger;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
public class SysBackup {
|
|
||||||
|
|
||||||
@Getter private static final Logger logger = Logger.builder().logFile(new File("log")).build();
|
|
||||||
|
|
||||||
public void start(String[] args) throws URISyntaxException {
|
|
||||||
File jarFile =
|
|
||||||
new File(SysBackup.class.getProtectionDomain().getCodeSource().getLocation().toURI());
|
|
||||||
|
|
||||||
File executionPath =
|
|
||||||
new File(
|
|
||||||
new File(SysBackup.class.getProtectionDomain().getCodeSource().getLocation().toURI())
|
|
||||||
.getParent());
|
|
||||||
|
|
||||||
if (!jarFile.isFile()) getLogger().log(Logger.LogLevel.INFO, "Dev environment detected!");
|
|
||||||
|
|
||||||
CLIProcessor cliProcessor = new CLIProcessor();
|
|
||||||
cliProcessor.startCLI(
|
|
||||||
args, jarFile.isFile() ? executionPath : new File(System.getProperty("user.dir")));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.cli;
|
|
||||||
|
|
||||||
import net.horizoncode.sysbackup.SysBackup;
|
|
||||||
import net.horizoncode.sysbackup.config.Config;
|
|
||||||
import net.horizoncode.sysbackup.logging.Logger;
|
|
||||||
import net.horizoncode.sysbackup.tasks.TaskBuilder;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.apache.commons.io.FilenameUtils;
|
|
||||||
import org.tomlj.Toml;
|
|
||||||
import org.tomlj.TomlParseResult;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class CLIProcessor {
|
|
||||||
|
|
||||||
public static void usage() {
|
|
||||||
String[] usage = {
|
|
||||||
"Usage: java -jar sysbackup.jar [action] <args>",
|
|
||||||
" Backup:",
|
|
||||||
" backup <backup task> create a backup based on a backup task configuration file",
|
|
||||||
" Miscellaneous:",
|
|
||||||
" checkTaskConf <file name> checks a backup task configuration file",
|
|
||||||
" generateTaskConf <file name> generate a example backup task configuration file",
|
|
||||||
" Examples:",
|
|
||||||
" java -jar sysbackup.jar generateTaskConf magento",
|
|
||||||
" java -jar sysbackup.jar backup magento"
|
|
||||||
};
|
|
||||||
Arrays.stream(usage).forEach(System.out::println);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startCLI(String[] args, File executionPath) {
|
|
||||||
try {
|
|
||||||
if ((args == null) || (args.length == 0)) {
|
|
||||||
usage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger logger = SysBackup.getLogger();
|
|
||||||
|
|
||||||
for (int index = 0; index < args.length; index++) {
|
|
||||||
switch (args[index].toLowerCase(Locale.ROOT)) {
|
|
||||||
case "backup":
|
|
||||||
{
|
|
||||||
if (args.length <= 1) {
|
|
||||||
logger.log(Logger.LogLevel.WARN, "Please specify a output task config name!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String fileName = args[1];
|
|
||||||
File tasksFolder = new File(executionPath, "tasks");
|
|
||||||
if (!tasksFolder.exists())
|
|
||||||
if (!tasksFolder.mkdir())
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Failed to create tasks folder!");
|
|
||||||
File taskFile = new File(tasksFolder, fileName + ".toml");
|
|
||||||
if (!taskFile.exists()) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "TaskFile %s.toml does not exist!", fileName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.log(Logger.LogLevel.INFO, "setupping TaskBuilder...");
|
|
||||||
Config taskConfig = new Config(taskFile);
|
|
||||||
TaskBuilder taskBuilder =
|
|
||||||
TaskBuilder.builder()
|
|
||||||
.executionPath(executionPath)
|
|
||||||
.taskName(FilenameUtils.removeExtension(taskFile.getName()))
|
|
||||||
.taskConfig(taskConfig)
|
|
||||||
.build();
|
|
||||||
taskBuilder.start();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "generatetaskconf":
|
|
||||||
{
|
|
||||||
if (args.length <= 1) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Please specify a output task config name!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String fileName = args[1];
|
|
||||||
File tasksFolder = new File(executionPath, "tasks");
|
|
||||||
if (!tasksFolder.exists())
|
|
||||||
if (!tasksFolder.mkdir())
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Failed to create tasks folder!");
|
|
||||||
logger.log(Logger.LogLevel.INFO, "Saving task config %s.toml...", fileName);
|
|
||||||
try {
|
|
||||||
FileUtils.copyInputStreamToFile(
|
|
||||||
Objects.requireNonNull(
|
|
||||||
getClass().getResourceAsStream("/" + "exampletask.toml")),
|
|
||||||
new File(tasksFolder, fileName + ".toml"));
|
|
||||||
} catch (IOException exception) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Failed to save task config.");
|
|
||||||
}
|
|
||||||
logger.log(Logger.LogLevel.INFO, "%s.toml saved!", fileName);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "checktaskconf":
|
|
||||||
{
|
|
||||||
if (args.length <= 1) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Please specify a output task config name!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
String fileName = args[1];
|
|
||||||
File tasksFolder = new File(executionPath, "tasks");
|
|
||||||
if (!tasksFolder.exists())
|
|
||||||
if (!tasksFolder.mkdir())
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Failed to create tasks folder!");
|
|
||||||
File taskFile = new File(tasksFolder, fileName + ".toml");
|
|
||||||
if (!taskFile.exists()) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "TaskFile %s.toml does not exist!", fileName);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
TomlParseResult toml;
|
|
||||||
try {
|
|
||||||
toml = Toml.parse(taskFile.toPath());
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "failed to read TaskFile.");
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
if (toml.hasErrors()) {
|
|
||||||
logger.log(
|
|
||||||
Logger.LogLevel.ERROR,
|
|
||||||
"TaskFile checked: found %d issues!:\n",
|
|
||||||
(long) toml.errors().size());
|
|
||||||
toml.errors().forEach(error -> logger.log(Logger.LogLevel.ERROR, error.toString()));
|
|
||||||
} else {
|
|
||||||
logger.log(Logger.LogLevel.INFO, "TaskFile checked successfully: no issues found!");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
if (index == 0) {
|
|
||||||
usage();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Throwable t) {
|
|
||||||
SysBackup.getLogger().log(Logger.LogLevel.ERROR, t.getMessage());
|
|
||||||
t.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.config;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
import org.tomlj.Toml;
|
|
||||||
import org.tomlj.TomlArray;
|
|
||||||
import org.tomlj.TomlParseResult;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class Config {
|
|
||||||
|
|
||||||
private final File configFile;
|
|
||||||
|
|
||||||
private TomlParseResult toml;
|
|
||||||
|
|
||||||
private boolean justCreated;
|
|
||||||
|
|
||||||
public Config(File configFile) {
|
|
||||||
this.configFile = configFile;
|
|
||||||
if (!configFile.exists()) {
|
|
||||||
try {
|
|
||||||
FileUtils.copyInputStreamToFile(
|
|
||||||
Objects.requireNonNull(getClass().getResourceAsStream("/" + configFile.getName())),
|
|
||||||
configFile);
|
|
||||||
justCreated = true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (configFile.isDirectory()) {
|
|
||||||
try {
|
|
||||||
FileUtils.copyInputStreamToFile(
|
|
||||||
Objects.requireNonNull(getClass().getResourceAsStream("/" + configFile.getName())),
|
|
||||||
configFile);
|
|
||||||
justCreated = true;
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (justCreated) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
toml = Toml.parse(configFile.toPath());
|
|
||||||
if (toml.hasErrors()) {
|
|
||||||
toml.errors().forEach(error -> System.err.println(error.toString()));
|
|
||||||
System.exit(-1);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getIntOrDefault(String key, int defaultValue) {
|
|
||||||
return getToml().contains(key) && getToml().get(key) instanceof Number
|
|
||||||
? Math.toIntExact((long) getToml().get(key))
|
|
||||||
: defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getBooleanOrDefault(String key, boolean defaultValue) {
|
|
||||||
return getToml().contains(key) && getToml().get(key) instanceof Boolean
|
|
||||||
? getToml().getBoolean(key)
|
|
||||||
: defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStringOrDefault(String key, String defaultValue) {
|
|
||||||
return getToml().contains(key) ? getToml().getString(key) : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TomlArray getArray(String key) {
|
|
||||||
return getToml().getArrayOrEmpty(key);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,48 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.logging;
|
|
||||||
|
|
||||||
import com.diogonunes.jcolor.Ansi;
|
|
||||||
import com.diogonunes.jcolor.Attribute;
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import org.apache.commons.io.FileUtils;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public class Logger {
|
|
||||||
private File logFile;
|
|
||||||
|
|
||||||
public void log(LogLevel logLevel, String message, Object... args) {
|
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
|
|
||||||
String prefix =
|
|
||||||
String.format(
|
|
||||||
"[%s - %s] ",
|
|
||||||
sdf.format(new Date()), Ansi.colorize(logLevel.name(), logLevel.getColor()));
|
|
||||||
String line = prefix + message;
|
|
||||||
System.out.printf(line + "\r\n", args);
|
|
||||||
|
|
||||||
// append to logfile
|
|
||||||
try {
|
|
||||||
FileUtils.writeStringToFile(
|
|
||||||
logFile, line.replaceAll("\u001B\\[[;\\d]*m", "") + "\r\n", StandardCharsets.UTF_8, true);
|
|
||||||
} catch (IOException ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum LogLevel {
|
|
||||||
INFO(Attribute.CYAN_TEXT()),
|
|
||||||
WARN(Attribute.YELLOW_TEXT()),
|
|
||||||
ERROR(Attribute.RED_TEXT());
|
|
||||||
|
|
||||||
@Getter private final Attribute color;
|
|
||||||
|
|
||||||
LogLevel(Attribute color) {
|
|
||||||
this.color = color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.tasks;
|
|
||||||
|
|
||||||
public class Task {
|
|
||||||
public void start() {}
|
|
||||||
|
|
||||||
public void onDone() {}
|
|
||||||
}
|
|
|
@ -1,146 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.tasks;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import net.horizoncode.sysbackup.SysBackup;
|
|
||||||
import net.horizoncode.sysbackup.config.Config;
|
|
||||||
import net.horizoncode.sysbackup.logging.Logger;
|
|
||||||
import net.horizoncode.sysbackup.tasks.impl.DatabaseTask;
|
|
||||||
import net.horizoncode.sysbackup.tasks.impl.FileSystemTask;
|
|
||||||
import net.horizoncode.sysbackup.tasks.impl.VacuumTask;
|
|
||||||
import net.horizoncode.sysbackup.threading.ThreadPool;
|
|
||||||
import org.apache.commons.io.FilenameUtils;
|
|
||||||
import org.tomlj.TomlArray;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.concurrent.LinkedBlockingQueue;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
@Getter
|
|
||||||
public class TaskBuilder {
|
|
||||||
|
|
||||||
private final Config taskConfig;
|
|
||||||
|
|
||||||
private final String taskName;
|
|
||||||
|
|
||||||
@Builder.Default private final LinkedBlockingQueue<Task> taskList = new LinkedBlockingQueue<>();
|
|
||||||
@Builder.Default @Getter private final ThreadPool threadPool = new ThreadPool(3, 10);
|
|
||||||
|
|
||||||
private final File executionPath;
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
|
|
||||||
Logger logger = SysBackup.getLogger();
|
|
||||||
|
|
||||||
File rootBackupDir = new File(executionPath, "backups");
|
|
||||||
if (!rootBackupDir.exists())
|
|
||||||
if (!rootBackupDir.mkdir()) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Failed to create root backup directory!");
|
|
||||||
System.exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
File backupDir = new File(rootBackupDir, getTaskName());
|
|
||||||
if (!backupDir.exists())
|
|
||||||
if (!backupDir.mkdir()) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "Failed to create backup directory!");
|
|
||||||
System.exit(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
SimpleDateFormat sdf =
|
|
||||||
new SimpleDateFormat(
|
|
||||||
getTaskConfig().getStringOrDefault("general.dateFormat", "yyyy-MM-dd HH-mm-ss"));
|
|
||||||
String fileName =
|
|
||||||
getTaskConfig().getStringOrDefault("general.outputFile", "{date} - {taskName}") + ".zip";
|
|
||||||
boolean doVAC = getTaskConfig().getBooleanOrDefault("vacuum.enabled", false);
|
|
||||||
boolean doFS = getTaskConfig().getBooleanOrDefault("filesystem.enabled", false);
|
|
||||||
boolean doDB = getTaskConfig().getBooleanOrDefault("mysql.enabled", false);
|
|
||||||
|
|
||||||
fileName =
|
|
||||||
fileName
|
|
||||||
.replace(
|
|
||||||
"{taskName}",
|
|
||||||
FilenameUtils.removeExtension(getTaskConfig().getConfigFile().getName()))
|
|
||||||
.replace("{date}", sdf.format(new Date()));
|
|
||||||
|
|
||||||
File outputFile = new File(backupDir, fileName);
|
|
||||||
|
|
||||||
if (doVAC) {
|
|
||||||
ChronoUnit unit =
|
|
||||||
ChronoUnit.valueOf(
|
|
||||||
getTaskConfig().getStringOrDefault("vacuum.unit", ChronoUnit.DAYS.name()));
|
|
||||||
|
|
||||||
int value = getTaskConfig().getIntOrDefault("vacuum.time", 5);
|
|
||||||
|
|
||||||
logger.log(
|
|
||||||
Logger.LogLevel.INFO, "Adding VacuumTask with lifetime of %d %s%n", value, unit.name());
|
|
||||||
taskList.add(
|
|
||||||
new VacuumTask(backupDir, unit, value) {
|
|
||||||
@Override
|
|
||||||
public void onDone() {
|
|
||||||
getThreadPool().getPool().submit(() -> executeNextTask());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doFS && getTaskConfig().getToml().contains("filesystem.targets")) {
|
|
||||||
logger.log(Logger.LogLevel.INFO, "Adding FileSystemTask...");
|
|
||||||
TomlArray filesArray = getTaskConfig().getArray("filesystem.targets");
|
|
||||||
|
|
||||||
IntStream.range(0, filesArray.size())
|
|
||||||
.forEach(
|
|
||||||
value -> {
|
|
||||||
String target = filesArray.getString(value);
|
|
||||||
logger.log(Logger.LogLevel.INFO, "Adding \"%s\"", target);
|
|
||||||
taskList.add(
|
|
||||||
new FileSystemTask(target, outputFile) {
|
|
||||||
@Override
|
|
||||||
public void onDone() {
|
|
||||||
getThreadPool().getPool().submit(() -> executeNextTask());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (doDB) {
|
|
||||||
String database = getTaskConfig().getStringOrDefault("mysql.database", "");
|
|
||||||
String user = getTaskConfig().getStringOrDefault("mysql.user", "");
|
|
||||||
String password = getTaskConfig().getStringOrDefault("mysql.password", "");
|
|
||||||
|
|
||||||
if (!database.isEmpty() && !user.isEmpty() && !password.isEmpty()) {
|
|
||||||
DatabaseTask.DatabaseCredentials databaseCredentials =
|
|
||||||
DatabaseTask.DatabaseCredentials.builder()
|
|
||||||
.database(database)
|
|
||||||
.username(user)
|
|
||||||
.password(password.toCharArray())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
logger.log(Logger.LogLevel.INFO, "Adding DatabaseTask for database \"%s\"", database);
|
|
||||||
|
|
||||||
taskList.add(
|
|
||||||
new DatabaseTask(databaseCredentials, outputFile) {
|
|
||||||
@Override
|
|
||||||
public void onDone() {
|
|
||||||
getThreadPool().getPool().submit(() -> executeNextTask());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "username, password or database is empty.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getThreadPool().getPool().submit(this::executeNextTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void executeNextTask() {
|
|
||||||
Task nextTask = taskList.poll();
|
|
||||||
if (nextTask != null) nextTask.start();
|
|
||||||
else {
|
|
||||||
SysBackup.getLogger().log(Logger.LogLevel.INFO, "Backup completed!");
|
|
||||||
System.exit(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,136 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.tasks.impl;
|
|
||||||
|
|
||||||
import lombok.Builder;
|
|
||||||
import lombok.Getter;
|
|
||||||
import me.tongfei.progressbar.ProgressBar;
|
|
||||||
import me.tongfei.progressbar.ProgressBarBuilder;
|
|
||||||
import me.tongfei.progressbar.ProgressBarStyle;
|
|
||||||
import net.horizoncode.sysbackup.SysBackup;
|
|
||||||
import net.horizoncode.sysbackup.logging.Logger;
|
|
||||||
import net.horizoncode.sysbackup.tasks.Task;
|
|
||||||
import net.lingala.zip4j.ZipFile;
|
|
||||||
import net.lingala.zip4j.progress.ProgressMonitor;
|
|
||||||
import org.apache.commons.lang3.RandomStringUtils;
|
|
||||||
|
|
||||||
import java.io.*;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class DatabaseTask extends Task {
|
|
||||||
|
|
||||||
private final DatabaseCredentials databaseCredentials;
|
|
||||||
|
|
||||||
private final File outputFile;
|
|
||||||
|
|
||||||
public DatabaseTask(DatabaseCredentials credentials, File outputFile) {
|
|
||||||
this.databaseCredentials = credentials;
|
|
||||||
this.outputFile = outputFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
try {
|
|
||||||
Logger logger = SysBackup.getLogger();
|
|
||||||
String commandArgs =
|
|
||||||
"mysqldump -u "
|
|
||||||
+ getDatabaseCredentials().username
|
|
||||||
+ " -p"
|
|
||||||
+ new String(getDatabaseCredentials().password)
|
|
||||||
+ " "
|
|
||||||
+ getDatabaseCredentials().database;
|
|
||||||
|
|
||||||
Runtime runtime = Runtime.getRuntime();
|
|
||||||
Process process = runtime.exec(commandArgs);
|
|
||||||
|
|
||||||
String databaseContent = "";
|
|
||||||
|
|
||||||
while (process.isAlive()) {
|
|
||||||
try {
|
|
||||||
Thread.sleep(1000);
|
|
||||||
databaseContent =
|
|
||||||
new BufferedReader(
|
|
||||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))
|
|
||||||
.lines()
|
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, e.getMessage());
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int exitValue = process.exitValue();
|
|
||||||
|
|
||||||
if (exitValue != 0) {
|
|
||||||
String text =
|
|
||||||
new BufferedReader(
|
|
||||||
new InputStreamReader(process.getErrorStream(), StandardCharsets.UTF_8))
|
|
||||||
.lines()
|
|
||||||
.collect(Collectors.joining("\n"));
|
|
||||||
logger.log(Logger.LogLevel.ERROR, text);
|
|
||||||
onDone();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (databaseContent.isEmpty()) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, "database content is empty");
|
|
||||||
onDone();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
File outputSQLFile =
|
|
||||||
new File(
|
|
||||||
outputFile.getParent(),
|
|
||||||
String.format(
|
|
||||||
"%s-%s.sql",
|
|
||||||
getDatabaseCredentials().database, RandomStringUtils.random(16, true, true)));
|
|
||||||
|
|
||||||
BufferedWriter writer = new BufferedWriter(new FileWriter(outputSQLFile));
|
|
||||||
writer.write(databaseContent);
|
|
||||||
writer.close();
|
|
||||||
logger.log(Logger.LogLevel.INFO, "Adding database to backup zip...");
|
|
||||||
try (ZipFile zipFile = new ZipFile(getOutputFile())) {
|
|
||||||
ProgressMonitor progressMonitor = zipFile.getProgressMonitor();
|
|
||||||
zipFile.setRunInThread(true);
|
|
||||||
zipFile.addFile(outputSQLFile);
|
|
||||||
ProgressBarBuilder pbb =
|
|
||||||
new ProgressBarBuilder()
|
|
||||||
.setStyle(ProgressBarStyle.ASCII)
|
|
||||||
.setInitialMax(progressMonitor.getTotalWork())
|
|
||||||
.setTaskName("Adding DB File...")
|
|
||||||
.setUnit("MiB", 1048576);
|
|
||||||
|
|
||||||
try (ProgressBar pb = pbb.build()) {
|
|
||||||
while (!progressMonitor.getState().equals(ProgressMonitor.State.READY)) {
|
|
||||||
pb.stepTo(progressMonitor.getWorkCompleted());
|
|
||||||
Thread.sleep(100);
|
|
||||||
}
|
|
||||||
pb.stepTo(progressMonitor.getTotalWork());
|
|
||||||
} catch (Exception exception) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, exception.getMessage());
|
|
||||||
exception.printStackTrace();
|
|
||||||
outputSQLFile.deleteOnExit();
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
progressMonitor.endProgressMonitor();
|
|
||||||
outputSQLFile.deleteOnExit();
|
|
||||||
onDone();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, ex.getMessage());
|
|
||||||
ex.printStackTrace();
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (IOException e) {
|
|
||||||
SysBackup.getLogger().log(Logger.LogLevel.ERROR, e.getMessage());
|
|
||||||
throw new RuntimeException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Builder
|
|
||||||
public static class DatabaseCredentials {
|
|
||||||
private final String username;
|
|
||||||
private final char[] password;
|
|
||||||
private final String database;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,68 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.tasks.impl;
|
|
||||||
|
|
||||||
import me.tongfei.progressbar.ProgressBar;
|
|
||||||
import me.tongfei.progressbar.ProgressBarBuilder;
|
|
||||||
import me.tongfei.progressbar.ProgressBarStyle;
|
|
||||||
import net.horizoncode.sysbackup.SysBackup;
|
|
||||||
import net.horizoncode.sysbackup.logging.Logger;
|
|
||||||
import net.horizoncode.sysbackup.tasks.Task;
|
|
||||||
import net.lingala.zip4j.ZipFile;
|
|
||||||
import net.lingala.zip4j.progress.ProgressMonitor;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
|
|
||||||
public class FileSystemTask extends Task {
|
|
||||||
|
|
||||||
private final File target;
|
|
||||||
private File outputZipFile;
|
|
||||||
|
|
||||||
public FileSystemTask(String folderOrFilePath, File outputZipFile) {
|
|
||||||
this.target = Paths.get(folderOrFilePath).toFile();
|
|
||||||
Logger logger = SysBackup.getLogger();
|
|
||||||
if (!target.exists()) {
|
|
||||||
onDone();
|
|
||||||
logger.log(
|
|
||||||
Logger.LogLevel.ERROR, "File or folder named \"%s\" does not exist.", folderOrFilePath);
|
|
||||||
System.exit(2);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.outputZipFile = outputZipFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
Logger logger = SysBackup.getLogger();
|
|
||||||
try (ZipFile zipFile = new ZipFile(outputZipFile)) {
|
|
||||||
logger.log(Logger.LogLevel.INFO, "Indexing files...");
|
|
||||||
ProgressMonitor progressMonitor = zipFile.getProgressMonitor();
|
|
||||||
zipFile.setRunInThread(true);
|
|
||||||
zipFile.addFolder(target);
|
|
||||||
ProgressBarBuilder pbb =
|
|
||||||
new ProgressBarBuilder()
|
|
||||||
.setStyle(ProgressBarStyle.ASCII)
|
|
||||||
.setInitialMax(progressMonitor.getTotalWork())
|
|
||||||
.setTaskName("Adding Files...")
|
|
||||||
.setUnit("MiB", 1048576);
|
|
||||||
|
|
||||||
try (ProgressBar pb = pbb.build()) {
|
|
||||||
while (!progressMonitor.getState().equals(ProgressMonitor.State.READY)) {
|
|
||||||
pb.stepTo(progressMonitor.getWorkCompleted());
|
|
||||||
Thread.sleep(100);
|
|
||||||
}
|
|
||||||
pb.stepTo(progressMonitor.getTotalWork());
|
|
||||||
} catch (Exception exception) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, exception.getMessage());
|
|
||||||
exception.printStackTrace();
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
progressMonitor.endProgressMonitor();
|
|
||||||
onDone();
|
|
||||||
} catch (Exception ex) {
|
|
||||||
logger.log(Logger.LogLevel.ERROR, ex.getMessage());
|
|
||||||
ex.printStackTrace();
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.tasks.impl;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
import net.horizoncode.sysbackup.tasks.Task;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
@Getter
|
|
||||||
public class VacuumTask extends Task {
|
|
||||||
|
|
||||||
private final File backupDir;
|
|
||||||
private final ChronoUnit unit;
|
|
||||||
private final int value;
|
|
||||||
|
|
||||||
public VacuumTask(File backupDir, ChronoUnit unit, int value) {
|
|
||||||
this.backupDir = backupDir;
|
|
||||||
this.unit = unit;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void start() {
|
|
||||||
if (backupDir.listFiles() != null) {
|
|
||||||
Arrays.stream(Objects.requireNonNull(backupDir.listFiles()))
|
|
||||||
.filter(
|
|
||||||
file ->
|
|
||||||
file.lastModified() + Duration.of(value, unit).toMillis()
|
|
||||||
<= System.currentTimeMillis())
|
|
||||||
.forEachOrdered(File::deleteOnExit);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDone();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
package net.horizoncode.sysbackup.threading;
|
|
||||||
|
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
|
||||||
|
|
||||||
public class ThreadPool {
|
|
||||||
|
|
||||||
@Getter private final ExecutorService pool;
|
|
||||||
|
|
||||||
public ThreadPool(int corePoolSize, int maxPoolSize) {
|
|
||||||
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor =
|
|
||||||
new ScheduledThreadPoolExecutor(corePoolSize);
|
|
||||||
scheduledThreadPoolExecutor.setMaximumPoolSize(maxPoolSize);
|
|
||||||
|
|
||||||
this.pool = scheduledThreadPoolExecutor;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
Manifest-Version: 1.0
|
|
||||||
Main-Class: Bootstrapper
|
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
[general]
|
|
||||||
dateFormat = "yyyy-MM-dd HH-mm-ss"
|
|
||||||
outputFile = "{date} - {taskName}"
|
|
||||||
|
|
||||||
[vacuum]
|
|
||||||
enabled = true
|
|
||||||
time = 5
|
|
||||||
unit = "DAYS" # See java.time.temporal.ChronoUnit
|
|
||||||
|
|
||||||
[mysql]
|
|
||||||
enabled = true
|
|
||||||
database = "magento"
|
|
||||||
user = ""
|
|
||||||
password = ""
|
|
||||||
|
|
||||||
[filesystem]
|
|
||||||
enabled = true
|
|
||||||
targets = ["/home/magento/", "/home/test/test.ini"]
|
|
||||||
|
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
package net.horizoncode.sysbackup;
|
|
||||||
|
|
||||||
import net.horizoncode.sysbackup.config.Config;
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.tomlj.TomlArray;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
public class TOMLArrayIterationTest {
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void testTOMLIteration() {
|
|
||||||
File tasksFolder = new File("tasks");
|
|
||||||
if (!tasksFolder.exists())
|
|
||||||
if (!tasksFolder.mkdir()) System.err.println("Failed to create tasks folder!");
|
|
||||||
Config config = new Config(new File(tasksFolder, "magento.toml"));
|
|
||||||
if (config.getToml().contains("filesystem.targets")) {
|
|
||||||
TomlArray filesArray = config.getArray("filesystem.targets");
|
|
||||||
|
|
||||||
IntStream.range(0, filesArray.size())
|
|
||||||
.forEach(
|
|
||||||
value -> {
|
|
||||||
String target = filesArray.getString(value);
|
|
||||||
System.out.println(target);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user