├── src ├── utils │ ├── constants.js │ ├── isGzip.js │ ├── appendInitVector.js │ ├── date.js │ ├── await-exec.js │ ├── utils.js │ ├── isEncrypted.js │ ├── files.js │ └── strings.js ├── inquirer.js ├── remoteSync │ ├── s3 │ │ ├── validator.js │ │ ├── inquirer.js │ │ └── s3.js │ ├── gDrive │ │ ├── validator.js │ │ ├── inquirer.js │ │ └── gDrive.js │ ├── inquirer.js │ ├── validator.js │ ├── sftp │ │ ├── validator.js │ │ ├── sftp.js │ │ └── inquirer.js │ └── remoteSync.js ├── backupScheduler.js ├── cipher │ ├── inquirer.js │ └── cipher.js ├── database │ ├── mongoDb │ │ ├── mongoUriBuilder.js │ │ └── mongoDb.js │ ├── postgresql │ │ └── postgresql.js │ ├── mysql │ │ └── mysql.js │ ├── database.js │ ├── validator.js │ └── inquirer.js ├── smtp │ ├── inquirer.js │ ├── validator.js │ └── smtp.js ├── backup.js └── cli.js ├── scripts ├── postinstall.js └── postuninstall.js ├── bin ├── synchly.conf ├── synchly.service └── synchly ├── .gitignore ├── docs ├── contributing.md ├── configuration-using-file.md └── examples.md ├── package.json ├── LICENSE └── README.md /src/utils/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PACKAGE_NAME: 'synchly', 3 | DB_BACKUP_DIR_PREFIX: '_backup_on_', 4 | DB_MANUAL_BACKUP_DIR_PREFIX: '_manual_backup_on_', 5 | }; 6 | -------------------------------------------------------------------------------- /scripts/postinstall.js: -------------------------------------------------------------------------------- 1 | const omelette = require('omelette'); 2 | 3 | const completion = omelette('synchly'); 4 | 5 | try { 6 | completion.setupShellInitFile(); 7 | } catch (err) { 8 | console.log(err); 9 | } 10 | -------------------------------------------------------------------------------- /bin/synchly.conf: -------------------------------------------------------------------------------- 1 | # Upstart script 2 | # /etc/init/synchly.conf 3 | 4 | description "Synchly Backups" 5 | author "Synchly" 6 | 7 | start on started mountall 8 | stop on shutdown 9 | 10 | respawn 11 | respawn limit 20 5 12 | 13 | exec sudo -u root synchly --start 14 | -------------------------------------------------------------------------------- /src/utils/isGzip.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const isGzip = function (path) { 4 | let buffer = fs.readFileSync(path); 5 | if (!buffer || buffer.length < 3) { 6 | return false; 7 | } 8 | 9 | return buffer[0] === 0x1f && buffer[1] === 0x8b && buffer[2] === 0x08; 10 | }; 11 | 12 | module.exports = isGzip; 13 | -------------------------------------------------------------------------------- /src/inquirer.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | let askResetConfirmation = async (jobName) => { 4 | let questions = []; 5 | 6 | questions.push({ 7 | type: 'confirm', 8 | name: 'resetConfirmation', 9 | message: `Are you sure you want to clear all the saved configurations for the job '${jobName}'?`, 10 | }); 11 | 12 | return await inquirer.prompt(questions); 13 | }; 14 | 15 | module.exports = { 16 | askResetConfirmation, 17 | }; 18 | -------------------------------------------------------------------------------- /src/utils/appendInitVector.js: -------------------------------------------------------------------------------- 1 | const {Transform} = require('stream'); 2 | 3 | class AppendInitVect extends Transform { 4 | constructor(initVect, opts) { 5 | super(opts); 6 | this.initVect = initVect; 7 | this.appended = false; 8 | } 9 | 10 | _transform(chunk, encoding, cb) { 11 | if (!this.appended) { 12 | this.push(this.initVect); 13 | this.appended = true; 14 | } 15 | this.push(chunk); 16 | cb(); 17 | } 18 | } 19 | 20 | module.exports = AppendInitVect; 21 | -------------------------------------------------------------------------------- /src/utils/date.js: -------------------------------------------------------------------------------- 1 | const getMinutes = (date) => { 2 | const hours = date.getHours(); 3 | const minutes = date.getMinutes(); 4 | 5 | return minutes + hours * 60; 6 | }; 7 | 8 | const isBetween = (date, intialDate, finalDate) => { 9 | const dateMinutes = getMinutes(date); 10 | const initialDateMinutes = getMinutes(intialDate); 11 | const finalDateMinutes = getMinutes(finalDate); 12 | 13 | return dateMinutes >= initialDateMinutes && dateMinutes <= finalDateMinutes; 14 | }; 15 | 16 | module.exports = { 17 | getMinutes, 18 | isBetween, 19 | }; 20 | -------------------------------------------------------------------------------- /src/utils/await-exec.js: -------------------------------------------------------------------------------- 1 | const exec = require('child_process').exec; 2 | 3 | const Exec = async (command, options = {log: false, cwd: process.cwd()}) => { 4 | if (options.log) console.log(command); 5 | let promise = new Promise((resolve, reject) => { 6 | exec(command, {...options}, (err, stdout, stderr) => { 7 | if (err) { 8 | err.stdout = stdout; 9 | err.stderr = stderr; 10 | reject(err); 11 | return; 12 | } 13 | 14 | resolve({stdout, stderr}); 15 | }); 16 | }); 17 | return await promise; 18 | }; 19 | 20 | module.exports = Exec; 21 | -------------------------------------------------------------------------------- /bin/synchly.service: -------------------------------------------------------------------------------- 1 | # 2 | #systemd unit file 3 | # 4 | # If placed in /etc/systemd/system 5 | # systemctl enable synchly.service 6 | # systemctl start synchly.service 7 | # 8 | # If placed in /etc/systemd/user or $HOME/.config/systemd/user 9 | # systemctl --user enable synchly.service 10 | # systemctl --user start synchly.service 11 | 12 | [Unit] 13 | Description=Synchly Backups 14 | Documentation=https://github.com/hariprasanths/synchly 15 | After=network.target 16 | 17 | [Service] 18 | WorkingDirectory=/usr/local/lib/node_modules/synchly/bin/ 19 | ExecStart=/usr/bin/node synchly --start 20 | Type=simple 21 | Restart=always 22 | 23 | [Install] 24 | WantedBy=default.target 25 | -------------------------------------------------------------------------------- /scripts/postuninstall.js: -------------------------------------------------------------------------------- 1 | const omelette = require('omelette'); 2 | const configstore = require('conf'); 3 | const files = require('./../src/utils/files'); 4 | 5 | const confStore = new configstore(); 6 | 7 | const jobNamesConfig = confStore.store; 8 | let jobNames = []; 9 | for (let j in jobNamesConfig) { 10 | if (jobNamesConfig[j].enabled) jobNames.push(j); 11 | } 12 | 13 | for (let i in jobNames) { 14 | const jobConfStore = new configstore({configName: jobNames[i]}); 15 | files.deleteFile(jobConfStore.path); 16 | } 17 | 18 | confStore.clear(); 19 | 20 | const completion = omelette('synchly'); 21 | 22 | try { 23 | completion.cleanupShellInitFile(); 24 | } catch (err) { 25 | console.log(err); 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/utils.js: -------------------------------------------------------------------------------- 1 | const replaceAll = (str, find, replace) => { 2 | return str.replace(new RegExp(find, 'g'), replace); 3 | }; 4 | 5 | let validateEmail = (email) => { 6 | const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; 7 | return re.test(String(email).toLowerCase()); 8 | }; 9 | 10 | let createJobsListTable = () => { 11 | const Table = require('cli-table3'); 12 | 13 | const jobInfoTable = new Table({ 14 | head: ['JOB', 'JOB STATUS', 'DB TYPE', 'DB NAME', 'BACKUP TIME', 'REMOTE SYNC', 'SMTP'], 15 | style: { 16 | head: ['bold'], 17 | border: [], 18 | }, 19 | colWidths: [16, 14, 12, 16, 16, 14, 12], 20 | }); 21 | 22 | return jobInfoTable; 23 | }; 24 | 25 | module.exports = { 26 | replaceAll, 27 | validateEmail, 28 | createJobsListTable, 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/isEncrypted.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const strings = require('./strings'); 3 | const string = require('./strings'); 4 | let readData = async (fileName) => { 5 | const chunks = []; 6 | const readStream = fs.createReadStream(fileName, {start: 0, end: 3}); 7 | let promise = new Promise((resolve, reject) => { 8 | readStream.on('error', () => { 9 | reject(); 10 | }); 11 | readStream.on('data', (chunk) => { 12 | chunks.push(chunk); 13 | }); 14 | readStream.on('end', () => { 15 | resolve(Buffer.concat(chunks).toString('utf-8')); 16 | }); 17 | }); 18 | return promise; 19 | }; 20 | 21 | let isEncrypted = async (fileName) => { 22 | let data = await readData(fileName); 23 | if (data === strings.encryptionTag) { 24 | return true; 25 | } else { 26 | return false; 27 | } 28 | }; 29 | 30 | module.exports = { 31 | isEncrypted, 32 | }; 33 | -------------------------------------------------------------------------------- /bin/synchly: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const omelette = require('omelette'); 4 | const completion = omelette('synchly').tree({ 5 | '--config': { 6 | 'db': ['--file', '--debug', '--stacktrace'], 7 | 'remote-sync': ['--file', '--debug', '--stacktrace'], 8 | 'smtp': ['--file', '--debug', '--stacktrace'] 9 | }, 10 | '--enable': { 11 | 'cipher': ['--debug', '--stacktrace'], 12 | 'remote-sync': ['--debug', '--stacktrace'], 13 | 'smtp': ['--debug', '--stacktrace'] 14 | }, 15 | '--disable': { 16 | 'cipher': ['--debug', '--stacktrace'], 17 | 'remote-sync': ['--debug', '--stacktrace'], 18 | 'smtp': ['--debug', '--stacktrace'] 19 | }, 20 | '--enablejob': [], 21 | '--disablejob': [], 22 | '--version':[], 23 | '--help':[], 24 | '--start': [], 25 | '--restore': [], 26 | '--reset': ['--job'] 27 | }); 28 | completion.init(); 29 | 30 | require('../src/cli').cli(process.argv); 31 | -------------------------------------------------------------------------------- /src/remoteSync/s3/validator.js: -------------------------------------------------------------------------------- 1 | const files = require('./../../utils/files'); 2 | 3 | const s3ConfigKeys = { 4 | s3AccKeyLoc: 's3CredentialsFilePath', 5 | }; 6 | 7 | const validateInitConfig = async (config) => { 8 | let validatedConfig = {}; 9 | 10 | if (!config[s3ConfigKeys.s3AccKeyLoc]) { 11 | throw new Error(`Invalid config: Missing required field - '${s3ConfigKeys.s3AccKeyLoc}'`); 12 | } 13 | if (!files.directoryExists(config[s3ConfigKeys.s3AccKeyLoc])) { 14 | throw new Error(`Invalid config: No such file, '${config[s3ConfigKeys.s3AccKeyLoc]}'`); 15 | } 16 | let isFile = files.isFile(config[s3ConfigKeys.s3AccKeyLoc]); 17 | if (!isFile) { 18 | throw new Error(`Invalid config: '${config[s3ConfigKeys.s3AccKeyLoc]}' is a directory.`); 19 | } 20 | validatedConfig.s3AccKeyLoc = config[s3ConfigKeys.s3AccKeyLoc]; 21 | 22 | return validatedConfig; 23 | }; 24 | 25 | module.exports = { 26 | validateInitConfig, 27 | }; 28 | -------------------------------------------------------------------------------- /src/remoteSync/gDrive/validator.js: -------------------------------------------------------------------------------- 1 | const files = require('./../../utils/files'); 2 | 3 | const gDriveConfigKeys = { 4 | gDriveServiceAccKeyLoc: 'serviceAccountKeyPath', 5 | }; 6 | 7 | const validateInitConfig = async (config) => { 8 | let validatedConfig = {}; 9 | 10 | if (!config[gDriveConfigKeys.gDriveServiceAccKeyLoc]) { 11 | throw new Error(`Invalid config: Missing required field - '${gDriveConfigKeys.gDriveServiceAccKeyLoc}'`); 12 | } 13 | if (!files.directoryExists(config[gDriveConfigKeys.gDriveServiceAccKeyLoc])) { 14 | throw new Error(`Invalid config: No such file, '${config[gDriveConfigKeys.gDriveServiceAccKeyLoc]}'`); 15 | } 16 | let isFile = files.isFile(config[gDriveConfigKeys.gDriveServiceAccKeyLoc]); 17 | if (!isFile) { 18 | throw new Error(`Invalid config: '${config[gDriveConfigKeys.gDriveServiceAccKeyLoc]}' is a directory.`); 19 | } 20 | validatedConfig.gDriveServiceAccKeyLoc = config[gDriveConfigKeys.gDriveServiceAccKeyLoc]; 21 | 22 | return validatedConfig; 23 | }; 24 | 25 | module.exports = { 26 | validateInitConfig, 27 | }; 28 | -------------------------------------------------------------------------------- /src/backupScheduler.js: -------------------------------------------------------------------------------- 1 | const backupDb = require('./backup'); 2 | const cron = require('node-cron'); 3 | const strings = require('./utils/strings'); 4 | const configstore = require('conf'); 5 | 6 | let cronScheduler = (jobNames, key, isDebug) => { 7 | console.log(strings.synchlyStartedDesc); 8 | 9 | for (let i in jobNames) { 10 | const currentJob = jobNames[i]; 11 | const jobConfStore = new configstore({configName: currentJob, encryptionKey: key}); 12 | const jobConfObj = jobConfStore.store; 13 | 14 | console.log(strings.jobConfigsLog(currentJob, jobConfObj)); 15 | const backupTime = new Date(jobConfObj.dbBackupTime); 16 | const backupHours = backupTime.getHours(); 17 | const backupMinutes = backupTime.getMinutes(); 18 | const cronExp = `${backupMinutes} ${backupHours} * * *`; 19 | cron.schedule(cronExp, () => { 20 | backupDb.backupCheck(currentJob, key, isDebug); 21 | }); 22 | } 23 | 24 | console.log(`Started ${jobNames.length} job(s)`); 25 | if (jobNames.length == 0) console.log(strings.enableJobsWarning); 26 | }; 27 | 28 | module.exports = cronScheduler; 29 | -------------------------------------------------------------------------------- /src/cipher/inquirer.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | 3 | let askConfig = async () => { 4 | let questions = []; 5 | questions.push({ 6 | name: 'encryptionKey', 7 | type: 'password', 8 | message: 'Enter the key for encryption', 9 | mask: true, 10 | validate: function (value) { 11 | if (value.length) { 12 | return true; 13 | } else { 14 | return 'Please enter the key for encryption'; 15 | } 16 | }, 17 | }); 18 | let secureConfig; 19 | secureConfig = await inquirer.prompt(questions); 20 | return secureConfig; 21 | }; 22 | 23 | let askConfirmation = async () => { 24 | let questions = []; 25 | questions.push({ 26 | name: 'encryptionKey', 27 | type: 'password', 28 | message: 'Enter the encryption key', 29 | mask: true, 30 | }); 31 | questions.push({ 32 | type: 'confirm', 33 | name: 'disableConfirmation', 34 | message: 'Are you sure you want to disable the encryption globally ?', 35 | }); 36 | let disableConfirmation; 37 | disableConfirmation = await inquirer.prompt(questions); 38 | return disableConfirmation; 39 | }; 40 | 41 | module.exports = { 42 | askConfig, 43 | askConfirmation, 44 | }; 45 | -------------------------------------------------------------------------------- /src/remoteSync/inquirer.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const files = require('../utils/files'); 3 | const configstore = require('conf'); 4 | const gDriveInquirer = require('./gDrive/inquirer'); 5 | const sftpInquirer = require('./sftp/inquirer'); 6 | const s3Inquirer = require('./s3/inquirer'); 7 | 8 | let askConfig = async (jobName, key) => { 9 | const jobConfStore = new configstore({configName: jobName, encryptionKey: key}); 10 | const jobConfigObj = jobConfStore.store; 11 | 12 | let questions = []; 13 | questions.push({ 14 | type: 'list', 15 | name: 'remoteType', 16 | message: 'Choose the remote service:', 17 | choices: ['Google Drive', 'SFTP', 'S3'], 18 | default: jobConfigObj.remoteType || 'Google Drive', 19 | }); 20 | 21 | let retObj = await inquirer.prompt(questions); 22 | if (retObj.remoteType == 'Google Drive') { 23 | let gdConfig = await gDriveInquirer.askConfig(jobName, key); 24 | retObj = Object.assign(retObj, gdConfig); 25 | } else if (retObj.remoteType == 'SFTP') { 26 | let sftpConfig = await sftpInquirer.askConfig(jobName, key); 27 | retObj = Object.assign(retObj, sftpConfig); 28 | } else if (retObj.remoteType == 'S3') { 29 | let s3Config = await s3Inquirer.askConfig(jobName, key); 30 | retObj = Object.assign(retObj, s3Config); 31 | } 32 | return retObj; 33 | }; 34 | 35 | module.exports = { 36 | askConfig, 37 | }; 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (https://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | .env.test 64 | 65 | # parcel-bundler cache (https://parceljs.org/) 66 | .cache 67 | 68 | # next.js build output 69 | .next 70 | 71 | # nuxt.js build output 72 | .nuxt 73 | 74 | # vuepress build output 75 | .vuepress/dist 76 | 77 | # Serverless directories 78 | .serverless/ 79 | 80 | # FuseBox cache 81 | .fusebox/ 82 | 83 | # DynamoDB Local files 84 | .dynamodb/ 85 | 86 | -------------------------------------------------------------------------------- /src/remoteSync/validator.js: -------------------------------------------------------------------------------- 1 | const files = require('./../utils/files'); 2 | const gDriveValidator = require('./gDrive/validator'); 3 | const sftpValidator = require('./sftp/validator'); 4 | const s3Validator = require('./s3/validator'); 5 | 6 | const remoteConfigKeys = { 7 | remoteType: 'remoteType', 8 | }; 9 | 10 | const validateInitConfig = async (config) => { 11 | let validatedConfig = {}; 12 | 13 | if (!config[remoteConfigKeys.remoteType]) { 14 | throw new Error(`Invalid config: Missing required field - '${remoteConfigKeys.remoteType}'`); 15 | } 16 | if (!['Google Drive', 'SFTP', 'S3'].includes(config[remoteConfigKeys.remoteType])) { 17 | throw new Error( 18 | `Invalid config: Unrecognised '${remoteConfigKeys.remoteType}' - ${config[remoteConfigKeys.remoteType]}` 19 | ); 20 | } 21 | validatedConfig.remoteType = config[remoteConfigKeys.remoteType]; 22 | 23 | if (validatedConfig.remoteType == 'Google Drive') { 24 | let gDriveValidatedConfig = await gDriveValidator.validateInitConfig(config); 25 | validatedConfig = Object.assign(validatedConfig, gDriveValidatedConfig); 26 | } else if (validatedConfig.remoteType == 'SFTP') { 27 | let sftpValidatedConfig = await sftpValidator.validateInitConfig(config); 28 | validatedConfig = Object.assign(validatedConfig, sftpValidatedConfig); 29 | } else if (validatedConfig.remoteType == 'S3') { 30 | let s3ValidatedConfig = await s3Validator.validateInitConfig(config); 31 | validatedConfig = Object.assign(validatedConfig, s3ValidatedConfig); 32 | } 33 | 34 | return validatedConfig; 35 | }; 36 | 37 | module.exports = { 38 | validateInitConfig, 39 | }; 40 | -------------------------------------------------------------------------------- /src/database/mongoDb/mongoUriBuilder.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (config) { 4 | const defaults = { 5 | host: 'localhost', 6 | port: '27017', 7 | }; 8 | 9 | config = Object.assign({}, defaults, config); 10 | 11 | // Schema 12 | let uri = 'mongodb://'; 13 | 14 | if (config.username || config.user) { 15 | uri += config.username || config.user; 16 | } 17 | 18 | if (config.password) { 19 | uri += ':' + config.password; 20 | } 21 | 22 | if (config.username || config.user) { 23 | uri += '@'; 24 | } 25 | 26 | // Host 27 | uri += config.host; 28 | 29 | // Port 30 | if (config.port) { 31 | uri += ':' + config.port; 32 | } 33 | 34 | // Replicas 35 | if (config.replicas) { 36 | config.replicas.forEach((replica) => { 37 | uri += ',' + replica.host; 38 | if (replica.port) { 39 | uri += ':' + replica.port; 40 | } 41 | }); 42 | } 43 | 44 | // Database & options 45 | if (config.database || config.options) { 46 | uri += '/'; 47 | } 48 | 49 | if (config.database) { 50 | uri += config.database; 51 | } 52 | 53 | if (config.options) { 54 | const pairs = []; 55 | 56 | for (const prop in config.options) { 57 | if (Object.prototype.hasOwnProperty.call(config.options, prop)) { 58 | const k = encodeURIComponent(prop); 59 | const v = encodeURIComponent(config.options[prop]); 60 | pairs.push(k + '=' + v); 61 | } 62 | } 63 | 64 | if (pairs) { 65 | uri += '?' + pairs.join('&'); 66 | } 67 | } 68 | 69 | return uri; 70 | }; 71 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Want to contribute to *Synchly*? Here are some guidelines for how we accept help. 4 | 5 | ## Getting in Touch 6 | 7 | Our [slack](https://join.slack.com/t/synchly/shared_invite/zt-iab7nopx-3hkGAgQh_VmXcFXkdEcCRg) channel is the best place to ask questions or get advice. 8 | 9 | ## Submitting a Pull Request 10 | 11 | 1. [Fork](https://github.com/hariprasanths/synchly/fork) and clone the repository. 12 | 1. Comment on the issue that you are gonna take up, indicating the approximate time you would take to finish it. 13 | 1. After finishing, push to your fork and [submit a pull request](https://github.com/hariprasanths/synchly/compare). 14 | 1. Wait for your `pull request` to be reviewed and merged. 15 | 1. In the meantime you can take up other issues as well. 16 | 17 | ## Reporting Bugs and Issues 18 | 19 | We use [GitHub Issues](https://github.com/hariprasanths/synchly/issues) to track bugs, so please do a search before submitting to ensure your problem isn't already tracked. 20 | 21 | ### New Issues 22 | 23 | Please provide the expected and observed behaviours in your issue. 24 | 25 | ## Proposing a Change 26 | 27 | If you intend to implement a feature, or make a non-trivial change to the current implementation, we recommend [first filing an issue](https://github.com/hariprasanths/synchly/issues/new) marked with the `proposal` tag, so that the engineering team can provide guidance and feedback on the direction of an implementation. This also help ensure that other people aren't also working on the same thing. 28 | 29 | Bug fixes are welcome and should come with appropriate test coverage. 30 | 31 | ### License 32 | 33 | By contributing to Synchly, you agree that your contributions will be licensed under its Apache License, Version 2.0.. -------------------------------------------------------------------------------- /src/remoteSync/gDrive/inquirer.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const files = require('../../utils/files'); 3 | const gDrive = require('./gDrive'); 4 | const ora = require('ora'); 5 | const configstore = require('conf'); 6 | 7 | let askConfig = async (jobName, key) => { 8 | const jobConfStore = new configstore({configName: jobName, encryptionKey: key}); 9 | const jobConfigObj = jobConfStore.store; 10 | let questions = []; 11 | questions.push({ 12 | name: 'gDriveServiceAccKeyLoc', 13 | type: 'input', 14 | message: 'Enter the absolute path of the service account key file:', 15 | default: jobConfigObj.gDriveServiceAccKeyLoc, 16 | validate: function (value) { 17 | if (value.length) { 18 | if (!files.directoryExists(value)) { 19 | return `No Such file, '${value}'`; 20 | } 21 | let isFile = files.isFile(value); 22 | if (!isFile) { 23 | return `'${value}' is a directory.`; 24 | } 25 | return true; 26 | } else { 27 | return 'Please enter the absolute path of the service account key file.'; 28 | } 29 | }, 30 | }); 31 | 32 | let gdConfig = await inquirer.prompt(questions); 33 | return gdConfig; 34 | }; 35 | 36 | const askRemoteLoc = async (folders) => { 37 | let retObj = await inquirer.prompt({ 38 | type: 'list', 39 | name: 'gDriveParentFolderId', 40 | message: 'Choose the remote folder in which backups will be stored:', 41 | choices: folders, 42 | default: 0, 43 | pageSize: 4, 44 | }); 45 | 46 | return retObj; 47 | }; 48 | 49 | module.exports = { 50 | askConfig, 51 | askRemoteLoc, 52 | }; 53 | -------------------------------------------------------------------------------- /src/remoteSync/sftp/validator.js: -------------------------------------------------------------------------------- 1 | const files = require('./../../utils/files'); 2 | 3 | const sftpConfigKeys = { 4 | sftpHost: 'host', 5 | sftpPort: 'port', 6 | sftpAuthUser: 'username', 7 | sftpAuthPwd: 'password', 8 | sftpBackupPath: 'backupPath', 9 | }; 10 | 11 | const validateInitConfig = async (config) => { 12 | let validatedConfig = {}; 13 | 14 | if (!config[sftpConfigKeys.sftpHost]) { 15 | throw new Error(`Invalid config: Missing required field - '${sftpConfigKeys.sftpHost}'`); 16 | } 17 | validatedConfig.sftpHost = config[sftpConfigKeys.sftpHost]; 18 | 19 | if (!config[sftpConfigKeys.sftpPort]) { 20 | throw new Error(`Invalid config: Missing required field - '${sftpConfigKeys.sftpPort}'`); 21 | } 22 | if (isNaN(config[sftpConfigKeys.sftpPort]) || Number(config[sftpConfigKeys.sftpPort]) == 0) { 23 | throw new Error( 24 | `Invalid config: Not a valid '${sftpConfigKeys.sftpPort}' - ${config[sftpConfigKeys.sftpPort]}` 25 | ); 26 | } 27 | validatedConfig.sftpPort = config[sftpConfigKeys.sftpPort].toString(); 28 | 29 | if (!config[sftpConfigKeys.sftpAuthUser]) { 30 | throw new Error(`Invalid config: Missing required field - '${sftpConfigKeys.sftpAuthUser}'`); 31 | } 32 | validatedConfig.sftpAuthUser = config[sftpConfigKeys.sftpAuthUser]; 33 | 34 | if (!config[sftpConfigKeys.sftpAuthPwd]) { 35 | throw new Error(`Invalid config: Missing required field - '${sftpConfigKeys.sftpAuthPwd}'`); 36 | } 37 | validatedConfig.sftpAuthPwd = config[sftpConfigKeys.sftpAuthPwd]; 38 | 39 | if (!config[sftpConfigKeys.sftpBackupPath]) { 40 | throw new Error(`Invalid config: Missing required field - '${sftpConfigKeys.sftpBackupPath}'`); 41 | } 42 | validatedConfig.sftpBackupPath = config[sftpConfigKeys.sftpBackupPath]; 43 | 44 | return validatedConfig; 45 | }; 46 | 47 | module.exports = { 48 | validateInitConfig, 49 | }; 50 | -------------------------------------------------------------------------------- /src/remoteSync/s3/inquirer.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer'); 2 | const files = require('../../utils/files'); 3 | const s3 = require('./s3'); 4 | const ora = require('ora'); 5 | const configstore = require('conf'); 6 | 7 | let askConfig = async (jobName, key) => { 8 | const jobConfStore = new configstore({configName: jobName, encryptionKey: key}); 9 | const jobConfigObj = jobConfStore.store; 10 | 11 | let questions = []; 12 | 13 | questions.push({ 14 | name: 's3AccKeyLoc', 15 | type: 'input', 16 | message: 'Enter the absolute path of the aws sdk credentials file:', 17 | default: jobConfigObj.s3AccKeyLoc, 18 | validate: function (value) { 19 | if (value.length) { 20 | if (!files.directoryExists(value)) { 21 | return `No Such file, '${value}'`; 22 | } 23 | let isFile = files.isFile(value); 24 | if (!isFile) { 25 | return `'${value}' is a directory.`; 26 | } 27 | return true; 28 | } else { 29 | return 'Please enter the absolute path of the aws sdk credentials file.'; 30 | } 31 | }, 32 | }); 33 | 34 | let s3Config = await inquirer.prompt(questions); 35 | return s3Config; 36 | }; 37 | 38 | const askRemoteBuck = async (buckets) => { 39 | let retObj = await inquirer.prompt({ 40 | type: 'list', 41 | name: 's3ParentBucket', 42 | message: 'Choose the remote bucket in which backups will be stored:', 43 | choices: buckets, 44 | default: 0, 45 | pageSize: 4, 46 | }); 47 | 48 | return retObj; 49 | }; 50 | 51 | const askRemoteLoc = async (bucket, folders) => { 52 | let retObj = await inquirer.prompt({ 53 | type: 'list', 54 | name: 's3ParentFolder', 55 | message: 'Choose the remote folder in ' + '"' + bucket + '"' + ' in which backups will be stored:', 56 | choices: folders, 57 | default: 0, 58 | pageSize: 4, 59 | }); 60 | 61 | return retObj; 62 | }; 63 | 64 | module.exports = { 65 | askConfig, 66 | askRemoteBuck, 67 | askRemoteLoc, 68 | }; 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "synchly", 3 | "version": "1.1.0", 4 | "description": "A CLI to automate database backups", 5 | "homepage": "https://github.com/hariprasanths/synchly", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/hariprasanths/synchly.git" 9 | }, 10 | "author": { 11 | "name": "Hariprasanth S", 12 | "email": "shhariprasanth@gmail.com", 13 | "url": "https://hariprasanths.github.io" 14 | }, 15 | "license": "Apache-2.0", 16 | "bugs": { 17 | "url": "https://github.com/hariprasanths/synchly/issues" 18 | }, 19 | "main": "src/backup.js", 20 | "scripts": { 21 | "postinstall": "node scripts/postinstall.js", 22 | "postuninstall": "node scripts/postuninstall.js", 23 | "fmt": "prettier --print-width 120 --no-bracket-spacing --single-quote --tab-width 4 --write scripts src", 24 | "test": "echo \"Error: no test specified\" && exit 1" 25 | }, 26 | "pre-commit": [ 27 | "fmt" 28 | ], 29 | "bin": { 30 | "synchly": "bin/synchly" 31 | }, 32 | "engines": { 33 | "node": ">=8" 34 | }, 35 | "files": [ 36 | "/bin", 37 | "/scripts", 38 | "/src" 39 | ], 40 | "preferGlobal": true, 41 | "keywords": [ 42 | "cli", 43 | "automatic", 44 | "backup", 45 | "database", 46 | "sync", 47 | "synchronise", 48 | "recurring", 49 | "scheduler", 50 | "remote-sync", 51 | "notifications", 52 | "mongodb", 53 | "mysql", 54 | "postgresql", 55 | "google-drive", 56 | "sftp" 57 | ], 58 | "dependencies": { 59 | "arg": "^4.1.3", 60 | "aws-sdk": "^2.877.0", 61 | "cli-table3": "^0.6.0", 62 | "conf": "^6.2.4", 63 | "googleapis": "^49.0.0", 64 | "inquirer": "^7.1.0", 65 | "inquirer-datepicker-prompt": "^0.4.2", 66 | "keytar": "^6.0.1", 67 | "mongoose": "^5.9.11", 68 | "mysql": "^2.18.1", 69 | "node-cron": "^2.0.3", 70 | "nodemailer": "^6.5.0", 71 | "omelette": "^0.4.12", 72 | "ora": "^4.0.4", 73 | "pg": "^8.5.1", 74 | "ssh2-sftp-client": "^5.1.2", 75 | "uuid": "^8.3.2" 76 | }, 77 | "devDependencies": { 78 | "pre-commit": "^1.2.2", 79 | "prettier": "^2.0.5" 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/remoteSync/gDrive/gDrive.js: -------------------------------------------------------------------------------- 1 | const {google} = require('googleapis'); 2 | const configstore = require('conf'); 3 | const fs = require('fs'); 4 | 5 | let init = (jobName, key, googleCreds = undefined) => { 6 | const jobConfStore = new configstore({configName: jobName, encryptionKey: key}); 7 | if (!googleCreds) googleCreds = jobConfStore.store; 8 | else { 9 | googleCreds = require(googleCreds.gDriveServiceAccKeyLoc); 10 | } 11 | 12 | const scopes = ['https://www.googleapis.com/auth/drive']; 13 | const gDriveAuth = new google.auth.JWT(googleCreds.client_email, null, googleCreds.private_key, scopes); 14 | return (drive = google.drive({version: 'v3', auth: gDriveAuth})); 15 | }; 16 | 17 | let cloneServiceAccKey = async (jobName, key, serviceKeyLoc) => { 18 | const jobConfStore = new configstore({configName: jobName, encryptionKey: key}); 19 | const googleCreds = require(serviceKeyLoc); 20 | return await jobConfStore.set(googleCreds); 21 | }; 22 | 23 | let listFolders = async (jobName, key, gdConfig) => { 24 | let drive = init(jobName, key, gdConfig); 25 | let res = await drive.files.list({ 26 | q: "mimeType = 'application/vnd.google-apps.folder'", 27 | }); 28 | const files = res.data.files; 29 | return files; 30 | }; 31 | 32 | let uploadFile = async (jobName, key, fileName, filePath) => { 33 | const jobConfStore = new configstore({configName: jobName, encryptionKey: key}); 34 | let drive = init(jobName, key); 35 | let res = await drive.files.create({ 36 | requestBody: { 37 | name: fileName, 38 | mimeType: 'application/x-gzip', 39 | parents: [jobConfStore.get('gDriveParentFolderId')], 40 | }, 41 | media: { 42 | mimeType: 'application/x-gzip', 43 | body: fs.createReadStream(filePath), 44 | }, 45 | }); 46 | 47 | jobConfStore.set({[res.data.name]: res.data.id}); 48 | return res; 49 | }; 50 | 51 | let deleteFile = async (jobName, key, fileName) => { 52 | const jobConfStore = new configstore({configName: jobName, encryptionKey: key}); 53 | let drive = init(jobName, key); 54 | let fileId = jobConfStore.get(fileName); 55 | let res = await drive.files.delete({fileId: fileId}); 56 | jobConfStore.delete(fileName); 57 | return res; 58 | }; 59 | 60 | module.exports = { 61 | init, 62 | cloneServiceAccKey, 63 | listFolders, 64 | uploadFile, 65 | deleteFile, 66 | }; 67 | -------------------------------------------------------------------------------- /docs/configuration-using-file.md: -------------------------------------------------------------------------------- 1 | ## Configuration using file 2 | 3 | For initializing a module configuration using a file, you'll need a JSON file of the following structure: 4 | 5 | ### Database Configuration 6 | 7 | **/home/foo/dbConfig.json:** 8 | ``` 9 | { 10 | "databaseType": , 11 | "username": , 12 | "password": , 13 | "host": , 14 | "port": , 15 | "databaseName": , 16 | "authSource": , 17 | "backupPath": , 18 | "enableCompression": , 19 | "backupTime":