Compare commits

...

No commits in common. "master" and "archived" have entirely different histories.

33 changed files with 1032 additions and 835 deletions

View File

@ -1,15 +0,0 @@
{
"env": {
"commonjs": true,
"es2021": true,
"node": true
},
"extends": [
"google"
],
"parserOptions": {
"ecmaVersion": "latest"
},
"rules": {
}
}

71
.gitignore vendored
View File

@ -1,6 +1,65 @@
node_modules/ # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
backups/ # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
tasks/ # User-specific stuff
.temp/ .idea/**/workspace.xml
bin/ .idea/**/tasks.xml
pnpm-lock.yaml .idea/**/usage.statistics.xml
.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 Normal file
View File

@ -0,0 +1,3 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -0,0 +1,22 @@
<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>

14
.idea/compiler.xml Normal file
View File

@ -0,0 +1,14 @@
<?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>

7
.idea/encodings.xml Normal file
View File

@ -0,0 +1,7 @@
<?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>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoogleJavaFormatSettings">
<option name="enabled" value="true" />
</component>
</project>

20
.idea/jarRepositories.xml Normal file
View File

@ -0,0 +1,20 @@
<?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>

14
.idea/misc.xml Normal file
View File

@ -0,0 +1,14 @@
<?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>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

View File

@ -1,6 +0,0 @@
{
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"eslint.validate": ["javascript"]
}

View File

@ -1,15 +0,0 @@
# 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!

View File

@ -1,64 +0,0 @@
# 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!

View File

@ -1,79 +0,0 @@
/* 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
View File

@ -1,572 +0,0 @@
/* 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);

View File

@ -1,17 +0,0 @@
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));
},
};

View File

@ -1,47 +0,0 @@
{
"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 Normal file
View File

@ -0,0 +1,102 @@
<?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>

View File

@ -0,0 +1,10 @@
import net.horizoncode.sysbackup.SysBackup;
import java.net.URISyntaxException;
public class Bootstrapper {
public static void main(String[] args) throws URISyntaxException {
new SysBackup().start(args);
}
}

View File

@ -0,0 +1,29 @@
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")));
}
}

View File

@ -0,0 +1,143 @@
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();
}
}
}

View File

@ -0,0 +1,77 @@
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);
}
}

View File

@ -0,0 +1,48 @@
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;
}
}
}

View File

@ -0,0 +1,7 @@
package net.horizoncode.sysbackup.tasks;
public class Task {
public void start() {}
public void onDone() {}
}

View File

@ -0,0 +1,146 @@
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);
}
}
}

View File

@ -0,0 +1,136 @@
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;
}
}

View File

@ -0,0 +1,68 @@
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();
}
}
}

View File

@ -0,0 +1,38 @@
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();
}
}

View File

@ -0,0 +1,19 @@
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;
}
}

View File

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: Bootstrapper

View File

@ -0,0 +1,20 @@
[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"]

View File

@ -0,0 +1,29 @@
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);
});
}
}
}

View File

@ -1,14 +0,0 @@
/* eslint-disable require-jsdoc */
const prettytime = require('prettytime');
class Timer {
startTimer() {
this.start = Date.now();
return this;
}
endTimer() {
return prettytime(Date.now() - this.start, {decimals: 2});
}
}
module.exports = Timer;