├── index.js ├── .prettierrc ├── .gitignore ├── .dockerignore ├── deploy.example.sh ├── package.json ├── .vscode └── launch.json ├── Dockerfile ├── docker-compose.yml ├── src ├── http.js ├── google-drive.js ├── config.js ├── main.js ├── system_notify.js └── backup.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | require('./src/main'); 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | node_modules/ 4 | .env 5 | dist/ 6 | deploy/ 7 | backup/ 8 | /deploy.sh 9 | build.sh 10 | build_lastest.sh 11 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | /configs 3 | node_modules/ 4 | /resources 5 | .dockerignore 6 | .gitignore 7 | docker_*.sh 8 | build.sh 9 | *compose.yml 10 | Dockerfile 11 | *.md 12 | **/test/ 13 | backup/ 14 | .env 15 | backup/ 16 | deploy.sh 17 | build.sh 18 | build_lastest 19 | example 20 | -------------------------------------------------------------------------------- /deploy.example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | GOOGLE_CLIENT_MAIL="cliemt-mail@gmail.com" \ 3 | GOOGLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----1234-----END PRIVATE KEY-----\n" \ 4 | GOOGLE_FOLDER_ID="1234" \ 5 | IS_FORCE_BACKUP=0 \ 6 | MONGO_BACKUP_USER="user" \ 7 | MONGO_BACKUP_PASSWORD="pass" \ 8 | MONGO_HOST="mongodb" \ 9 | HTTP_FORCE_BACKUP_TOKEN="1234" \ 10 | docker-compose up -d 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongodb-backup", 3 | "version": "2.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "node index.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "Nguyen Van Tuan", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^0.21.0", 14 | "cron": "^1.8.2", 15 | "dotenv": "^8.2.0", 16 | "express": "^4.17.1", 17 | "googleapis": "^65.0.0", 18 | "lodash": "^4.17.20", 19 | "zip-a-folder": "0.0.12" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}/index.js" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build from node 2 | FROM node 3 | 4 | # Install mongo 5 | RUN apt-get update -y 6 | RUN apt-get install gnupg apt-transport-https -y 7 | RUN wget -qO - https://www.mongodb.org/static/pgp/server-4.4.asc | apt-key add - 8 | RUN echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu bionic/mongodb-org/4.4 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-4.4.list 9 | RUN apt-get update -y 10 | RUN apt-get install -y mongodb-org-tools=4.4.2 11 | 12 | # Handle nodeapp 13 | WORKDIR /usr/src/app 14 | COPY ["package.json", "./"] 15 | RUN npm install --production 16 | COPY . . 17 | EXPOSE 5050 18 | CMD npm start 19 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | services: 3 | mongodb_backup: 4 | image: vtuanjs/mongodb_backup:lastest 5 | networks: 6 | - net 7 | ports: 8 | - "5050:5050" 9 | environment: 10 | NODE_ENV: 'production' 11 | GOOGLE_CLIENT_MAIL: ${GOOGLE_CLIENT_MAIL} 12 | GOOGLE_PRIVATE_KEY: ${GOOGLE_PRIVATE_KEY} 13 | GOOGLE_FOLDER_ID: ${GOOGLE_FOLDER_ID} 14 | MONGO_BACKUP_USER: ${MONGO_BACKUP_USER} 15 | MONGO_BACKUP_PASSWORD: ${MONGO_BACKUP_PASSWORD} 16 | MONGO_URI: ${MONGO_URI} 17 | # MONGO_HOST: ${MONGO_HOST} 18 | # MONGO_PORT: ${MONGO_PORT} 19 | IS_FORCE_BACKUP: ${IS_FORCE_BACKUP} 20 | HTTP_FORCE_BACKUP_TOKEN: ${HTTP_FORCE_BACKUP_TOKEN} 21 | networks: 22 | net: 23 | -------------------------------------------------------------------------------- /src/http.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const app = express(); 3 | const config = require('./config'); 4 | const backupDB = require('./backup'); 5 | 6 | app.use(express.urlencoded({ extended: true })); 7 | app.use(express.json()); 8 | 9 | function handleBackup(token, res) { 10 | if (!config.token) { 11 | return res.status(500).json({ 12 | message: "Method is disabled, please set your system' token", 13 | }); 14 | } 15 | 16 | try { 17 | if (typeof token == 'string' && token === config.token) { 18 | backupDB(); 19 | 20 | return res.status(200).json({ 21 | message: 'Process starting...', 22 | }); 23 | } 24 | 25 | return res.status(403).json({ 26 | message: 'Invalid token', 27 | }); 28 | } catch (error) { 29 | return res.status(500).json({ 30 | message: 'Unknown error', 31 | details: JSON.stringify(error), 32 | }); 33 | } 34 | } 35 | 36 | app.get('/', (req, res) => { 37 | const token = req.query.token; 38 | return handleBackup(token, res); 39 | }); 40 | 41 | app.post('/', (req, res) => { 42 | const token = req.body.token; 43 | 44 | return handleBackup(token, res); 45 | }); 46 | 47 | module.exports = app; 48 | -------------------------------------------------------------------------------- /src/google-drive.js: -------------------------------------------------------------------------------- 1 | const { google } = require('googleapis'); 2 | const fs = require('fs'); 3 | const config = require('./config'); 4 | const _ = require('lodash'); 5 | 6 | /** 7 | * 8 | * @param {*} auth 9 | * @param {string} id 10 | */ 11 | async function deleteFile(auth, id) { 12 | const drive = google.drive({ version: 'v3', auth }); 13 | const result = await drive.files.delete({ 14 | fileId: id, 15 | }); 16 | 17 | return result.data; 18 | } 19 | 20 | /** 21 | * 22 | * @param {*} auth 23 | */ 24 | async function listFile(auth) { 25 | const drive = google.drive({ version: 'v3', auth }); 26 | const files = await drive.files.list({ 27 | q: "'" + config.googleFolderId + "' in parents" 28 | }); 29 | 30 | return files.data.files; 31 | } 32 | 33 | async function authorize() { 34 | const jwt = new google.auth.JWT( 35 | config.googleClientMail, 36 | null, 37 | _.replace(config.googlePrivateKey, new RegExp("\\\\n", "\g"), "\n"), 38 | config.googleScopes 39 | ); 40 | 41 | await jwt.authorize(); 42 | return jwt; 43 | } 44 | 45 | /** 46 | * @param {object} params 47 | * @param {string} params.auth 48 | * @param {string} params.filePath 49 | * @param {string} params.fileName 50 | */ 51 | async function uploadFile({auth, filePath, fileName}) { 52 | const drive = google.drive({ version: 'v3', auth }); 53 | const fileMetadata = { 54 | name: fileName, 55 | parents: [config.googleFolderId], 56 | }; 57 | const media = { 58 | mimeType: 'application/zip', 59 | body: fs.createReadStream(filePath), 60 | }; 61 | 62 | const file = await drive.files.create({ 63 | resource: fileMetadata, 64 | media: media, 65 | fields: 'name', 66 | }); 67 | 68 | if (file && file.data) return file.data; 69 | throw new Error('Upload file error'); 70 | } 71 | 72 | module.exports = { 73 | authorize, 74 | uploadFile, 75 | listFile, 76 | deleteFile, 77 | }; 78 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | require('dotenv').config(); 3 | 4 | module.exports = { 5 | user: process.env.MONGO_ROOT_USER || process.env.MONGO_BACKUP_USER || '', 6 | pass: process.env.MONGO_ROOT_PASSWORD || process.env.MONGO_BACKUP_PASSWORD || '', 7 | host: process.env.MONGO_HOST || 'localhost', 8 | port: process.env.MONGO_PORT || 27017, 9 | uri: process.env.MONGO_URI, 10 | 11 | isAutoBackup: process.env.IS_AUTO_BACKUP || 1, 12 | cronJobTime: process.env.CRON_JOB_TIME || '00 00 * * *', 13 | cronJobTimes: process.env.CRON_JOB_TIMES, 14 | autoBackupPath: path.join(__dirname, 'backup'), 15 | isForceBackup: process.env.IS_FORCE_BACKUP || 0, 16 | 17 | isRemoveOldLocalBackup: process.env.IS_REMOVE_OLD_LOCAL_BACKUP || 1, 18 | keepLastDaysOfLocalBackup: process.env.KEEP_LAST_DAYS_OF_LOCAL_BACKUP || 2, 19 | 20 | isRemoveOldDriveBackup: process.env.IS_REMOVE_OLD_DRIVE_BACKUP || 1, 21 | keepLastDaysOfDriveBackup: process.env.KEEP_LAST_DAYS_OF_DRIVE_BACKUP || 7, 22 | 23 | googleClientMail: process.env.GOOGLE_CLIENT_MAIL || '', 24 | googlePrivateKey: process.env.GOOGLE_PRIVATE_KEY || '', 25 | googleScopes: [ 26 | 'https://www.googleapis.com/auth/drive', 27 | 'https://www.googleapis.com/auth/drive.appdata', 28 | 'https://www.googleapis.com/auth/drive.file', 29 | 'https://www.googleapis.com/auth/drive.metadata', 30 | 'https://www.googleapis.com/auth/drive.metadata.readonly', 31 | 'https://www.googleapis.com/auth/drive.photos.readonly', 32 | 'https://www.googleapis.com/auth/drive.readonly', 33 | ], 34 | googleFolderId: process.env.GOOGLE_FOLDER_ID || '', // Do not forget share your folder to client email 35 | 36 | isAllowSendTelegramMessage: process.env.IS_ALLOW_SEND_TELEGRAM_MESSAGE || 1, 37 | telegramChanelId: process.env.TELEGRAM_CHANEL_ID || '', 38 | telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || '', 39 | telegramMessageLevels: process.env.TELEGRAM_MESSAGE_LEVELS || 'info error', 40 | telegramPrefix: process.env.TELEGRAM_PREFIX || 'MongoDB Backup', 41 | 42 | httpPort: process.env.HTTP_PORT || 5050, 43 | token: process.env.HTTP_FORCE_BACKUP_TOKEN || "" 44 | }; 45 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | const app = require('./http'); 2 | const backupDB = require('./backup'); 3 | const CronJob = require('cron').CronJob; 4 | const config = require('./config'); 5 | const { 6 | sendErrorToTelegram, 7 | sendSuccessMessageToTelegram, 8 | } = require('./system_notify'); 9 | 10 | app.listen(config.httpPort, () => { 11 | console.info(`App listening at port: ${config.httpPort}`); 12 | run() 13 | }); 14 | 15 | // Backup data immediately when start server, Default: 0 16 | if (config.isForceBackup == 1) { 17 | backupDB(); 18 | } 19 | 20 | async function startAutoBackup(cronTime) { 21 | const job = new CronJob( 22 | cronTime, 23 | () => { 24 | backupDB(); 25 | }, 26 | null, 27 | true, 28 | 'Asia/Vientiane' 29 | ); 30 | 31 | job.start(); 32 | console.info('Backup at cron time: ', cronTime) 33 | } 34 | 35 | // Default cron time: 0:00AM everyday, GMT +7 36 | async function run(){ 37 | try { 38 | if (config.isAutoBackup == 1) { 39 | if (config.cronJobTimes) { 40 | const array = JSON.parse(config.cronJobTimes) 41 | for (const time of array) { 42 | await startAutoBackup(time) 43 | } 44 | } else { 45 | await startAutoBackup(config.cronJobTime) 46 | } 47 | 48 | await sendSuccessMessageToTelegram('Auto backup MongoDB starting with cron: '); 49 | console.info('Auto backup MongoDB starting...'); 50 | } 51 | } catch (error) { 52 | console.log(error) 53 | process.exit(1) 54 | } 55 | } 56 | 57 | process.on('beforeExit', async (code) => { 58 | await sendErrorToTelegram('🟥 Process beforeExit event', { code }); 59 | console.error('Process beforeExit event with code: ', code); 60 | process.exit(1); 61 | }); 62 | 63 | process.on('SIGTERM', async (signal) => { 64 | await sendErrorToTelegram( 65 | `🟥 Process ${process.pid} received a SIGTERM signal`, 66 | '' 67 | ); 68 | console.error(`Process ${process.pid} received a SIGTERM signal`); 69 | process.exit(0); 70 | }); 71 | 72 | process.on('SIGINT', async (signal) => { 73 | await sendErrorToTelegram( 74 | `🟥 Process ${process.pid} has been interrupted`, 75 | '' 76 | ); 77 | console.error(`Process ${process.pid} has been interrupted`); 78 | process.exit(0); 79 | }); 80 | -------------------------------------------------------------------------------- /src/system_notify.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const config = require('./config'); 3 | 4 | /** 5 | * 6 | * @param {Error} error 7 | */ 8 | function changeErrorObjectToMessage(error) { 9 | let text = ''; 10 | 11 | if (typeof error === 'string' && error.trim()) { 12 | text += `\n - Message: ${error}`; 13 | } 14 | 15 | if (typeof error.code === 'string' || typeof error.code === 'number') { 16 | text += `\n - Code: ${error.code}`; 17 | } 18 | 19 | if (typeof error.message === 'string') { 20 | text += `\n - Message: ${error.message}`; 21 | } 22 | 23 | if (typeof error.details === 'object') { 24 | if (typeof error.details.code === 'string') { 25 | text += `\n - Code Detail: ${error.details.code}`; 26 | } 27 | 28 | if (typeof error.details.message === 'string') { 29 | text += `\n - Message Detail: ${error.details.message}`; 30 | } 31 | 32 | if (typeof error.details.localMessage === 'string') { 33 | text += `\n - Local Message Detail: ${error.details.localMessage}`; 34 | } 35 | } 36 | 37 | return text; 38 | } 39 | 40 | /** 41 | * 42 | * @param {string} message 43 | */ 44 | async function sendMessageToTelegram(message) { 45 | if (!config.telegramBotToken || !config.telegramChanelId || config.isAllowSendTelegramMessage != 1) { 46 | return; 47 | } 48 | 49 | let isSend = 50 | process.env.NODE_ENV === 'development' || 51 | process.env.NODE_ENV === 'staging' || 52 | process.env.NODE_ENV === 'production'; 53 | if (!isSend) return; 54 | 55 | const splitDate = new Date() 56 | .toLocaleString('en-US', { timeZone: 'Asia/Ho_Chi_Minh' }) 57 | .split('/'); 58 | const formatDate = `${splitDate[1]}/${splitDate[0]}/${splitDate[2]}, VietNam`; 59 | 60 | try { 61 | await axios({ 62 | url: `https://api.telegram.org/bot${config.telegramBotToken}/sendMessage`, 63 | method: 'POST', 64 | data: { 65 | chat_id: config.telegramChanelId, 66 | text: `${formatDate}\n\n${message}`, 67 | }, 68 | headers: { 69 | 'Content-Type': 'application/json', 70 | }, 71 | }); 72 | 73 | return; 74 | } catch (error) { 75 | console.log('Send telegram message error', error.response.data); 76 | return; 77 | } 78 | } 79 | 80 | /** 81 | * @param {string} title 82 | * @param {object} error 83 | */ 84 | async function sendErrorToTelegram(title, error) { 85 | return sendMessageToTelegram( 86 | `❌ ${config.telegramPrefix}\n\n${title} \n${changeErrorObjectToMessage( 87 | error 88 | )}` 89 | ); 90 | } 91 | 92 | /** 93 | * 94 | * @param {string} title 95 | */ 96 | async function sendSuccessMessageToTelegram(title) { 97 | return sendMessageToTelegram(`✅ ${config.telegramPrefix}\n\n${title}`); 98 | } 99 | 100 | /** 101 | * @typedef {Object} Error 102 | * @property {String} code 103 | * @property {String} message 104 | * @property {ErrorDetails} details 105 | */ 106 | 107 | /** 108 | * @typedef {Object} ErrorDetails 109 | * @property {String} code 110 | * @property {String} message 111 | * @property {String} localMessage 112 | */ 113 | 114 | module.exports = { sendErrorToTelegram, sendSuccessMessageToTelegram }; 115 | -------------------------------------------------------------------------------- /src/backup.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const _ = require('lodash'); 3 | const exec = require('child_process').exec; 4 | const zipFolder = require('zip-a-folder'); 5 | const config = require('./config'); 6 | const { 7 | authorize, 8 | uploadFile, 9 | deleteFile, 10 | listFile, 11 | } = require('./google-drive'); 12 | const { 13 | sendErrorToTelegram, 14 | sendSuccessMessageToTelegram, 15 | } = require('./system_notify'); 16 | 17 | // Backup script 18 | async function backup() { 19 | console.info(`[${getVNDate()}] Backup database starting...`); 20 | 21 | try { 22 | await createFolderIfNotExists(config.autoBackupPath); 23 | let currentDate = getVNDate(); 24 | 25 | let newBackupPath = 26 | config.autoBackupPath + '/' + formatYYYYMMDD(currentDate); 27 | 28 | // create backup file 29 | const cmd = getMongodumpCMD(newBackupPath); 30 | try { 31 | await runCommand(cmd); 32 | } catch (err) { 33 | console.log('Run command error'); 34 | if (typeof err.message === 'string') { 35 | err.message = removeSensitive(err.message); 36 | } 37 | if (typeof err.cmd === 'string') { 38 | err.cmd = removeSensitive(err.cmd); 39 | } 40 | throw err; 41 | } 42 | 43 | // create zip file and remove old file 44 | const zipPath = await zipFolderPromise(newBackupPath); 45 | await runCommand(`rm -rf ${newBackupPath}`); 46 | 47 | // handle google drive 48 | const auth = await authorize(); 49 | const fileName = zipPath.split('/').slice(-1)[0]; 50 | 51 | try { 52 | const file = await uploadFile({ 53 | auth, 54 | filePath: zipPath, 55 | fileName, 56 | }); 57 | 58 | console.info( 59 | `[${getVNDate()}] Backup database to GG Drive with file name: ${ 60 | file.name 61 | } successfully!` 62 | ); 63 | } catch (error) { 64 | console.error('Push file to GG Drive error'); 65 | throw error; 66 | } 67 | 68 | // if (config.telegramMessageLevels.includes('info')) { 69 | // await sendSuccessMessageToTelegram( 70 | // `Backup database to GG Drive with file name: ${file.name} successfully!` 71 | // ); 72 | // } 73 | 74 | try { 75 | // check for remove old local backup after keeping # of days given in configuration 76 | if (config.isRemoveOldLocalBackup == 1) { 77 | let beforeDate = _.clone(currentDate); 78 | beforeDate.setDate( 79 | beforeDate.getDate() - config.keepLastDaysOfLocalBackup 80 | ); // Substract number of days to keep backup and remove old backup 81 | 82 | const oldBackupPath = 83 | config.autoBackupPath + '/' + formatYYYYMMDD(beforeDate); // old backup(after keeping # of days) 84 | if (fs.existsSync(`${oldBackupPath}.zip`)) { 85 | await runCommand(`rm -rf ${oldBackupPath}.zip`); 86 | } 87 | } 88 | 89 | // check for remove old drive backup after keeping # of days given in configuration 90 | if (config.isRemoveOldDriveBackup == 1) { 91 | let beforeDate = _.clone(currentDate); 92 | beforeDate.setDate( 93 | beforeDate.getDate() - config.keepLastDaysOfDriveBackup 94 | ); // Substract number of days to keep backup and remove old backup 95 | 96 | const oldBackupName = formatYYYYMMDD(beforeDate); // old backup(after keeping # of days) 97 | const files = await listFile(auth); 98 | for (const _file of files) { 99 | if (_file.name === `${oldBackupName}.zip`) { 100 | await deleteFile(auth, _file.id); 101 | // Do not break the loop because some files have the same name 102 | } 103 | } 104 | } 105 | } catch (err) { 106 | console.error('Delete file error: ', err); 107 | if (config.telegramMessageLevels.includes('error')) { 108 | await sendErrorToTelegram(`Delete backup file failed`, err); 109 | } 110 | } 111 | 112 | return; 113 | } catch (error) { 114 | console.error(error); 115 | if (config.telegramMessageLevels.includes('error')) { 116 | await sendErrorToTelegram(`Backup database to GG Drive failed`, error); 117 | } 118 | } 119 | } 120 | 121 | /** 122 | * 123 | * @param {string} output output folder 124 | */ 125 | function getMongodumpCMD(output) { 126 | let cmd = `mongodump`; 127 | if (config.uri) { 128 | cmd += ` --uri ${config.uri}`; 129 | } 130 | 131 | if (config.host) { 132 | cmd += ` --host ${config.host}`; 133 | } 134 | 135 | if (config.port) { 136 | cmd += ` --port ${config.port}`; 137 | } 138 | 139 | if (config.host.includes('rs')) { 140 | cmd += ` --readPreference secondaryPreferred` 141 | } 142 | 143 | if (config.user) cmd += ` --username ${config.user}`; 144 | if (config.pass) cmd += ` --password ${config.pass}`; 145 | cmd += ` --out ${output}`; 146 | 147 | return cmd; 148 | } 149 | 150 | /** 151 | * 152 | * @param {Date} date 153 | */ 154 | function formatYYYYMMDD(date) { 155 | return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`; 156 | } 157 | 158 | function getVNDate() { 159 | return new Date( 160 | new Date().toLocaleString('en-US', { timeZone: 'Asia/Ho_Chi_Minh' }) 161 | ); 162 | } 163 | 164 | /** 165 | * 166 | * @param {string} _path 167 | * @returns {Promise} 168 | */ 169 | function createFolderIfNotExists(_path) { 170 | return new Promise((resolve, reject) => 171 | fs.mkdir(_path, { recursive: true }, (err) => { 172 | // if (err) { 173 | // return reject(err); 174 | // } 175 | 176 | resolve(true); 177 | }) 178 | ); 179 | } 180 | 181 | const empty = function (mixedVar) { 182 | let undef, key, i, len; 183 | let emptyValues = [undef, null, false, 0, '', '0']; 184 | for (i = 0, len = emptyValues.length; i < len; i++) { 185 | if (mixedVar === emptyValues[i]) { 186 | return true; 187 | } 188 | } 189 | if (typeof mixedVar === 'object') { 190 | for (key in mixedVar) { 191 | return false; 192 | } 193 | return true; 194 | } 195 | return false; 196 | }; 197 | 198 | /** 199 | * Run shell script 200 | * @param {string} cmd 201 | * @returns {Promise} 202 | */ 203 | function runCommand(cmd) { 204 | return new Promise((resolve, reject) => { 205 | return exec(cmd, (error, stdout, stderr) => { 206 | if (empty(error)) return resolve('Success'); 207 | 208 | return reject(error); 209 | }); 210 | }); 211 | } 212 | 213 | /** 214 | * Zip file 215 | * @param {string} _path 216 | * @returns {Promise} 217 | */ 218 | function zipFolderPromise(_path) { 219 | return new Promise((resolve, reject) => { 220 | const out = `${_path}.zip`; 221 | return zipFolder.zipFolder(_path, out, (error) => { 222 | if (error) return reject(error); 223 | 224 | resolve(out); 225 | }); 226 | }); 227 | } 228 | 229 | function removeSensitive(text) { 230 | if (config.user) { 231 | text = text.replace(config.user, ''); 232 | } 233 | 234 | if (config.pass) { 235 | text = text.replace(config.pass, ''); 236 | } 237 | 238 | return text; 239 | } 240 | 241 | module.exports = backup; 242 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MONGODB BACKUP SERVICE 2 | 3 | ## 1. Vấn đề 4 | Trong khi vận hành hệ thống, việc mất mát dữ liệu là điều không thể tránh khỏi. 5 | Có những nguyên nhân chính như sau: 6 | - Xung đột khi Migration data 7 | - Hệ thống bị hack 8 | - Dev mớ ngủ xoá nhầm data 9 | 10 | Khi đó, việc khôi phục data rất tốn thời gian => Cần phải backup data hàng ngày và mỗi khi lên version mới. 11 | 12 | Có nhiều giải pháp để giải quyết vấn đề này, như sử dụng dịch vụ backup data của bên cung cấp Server. Tuy nhiên, chúng thường tốn phí. Vậy làm sao để vừa free (do tận dụng tài nguyên hiện có), vừa đảm bảo tính an toàn dữ liệu? 13 | 14 | ## 2. Giải pháp 15 | Trước khi đưa ra giải pháp thì cùng nhìn lại bài toán của công ty mình gặp phải: 16 | 17 | - Lượng data của công ty mình không lớn, chủ yếu là lưu thông tin về user, app, không lưu các thông tin về transaction, nếu dùng dịch vụ bên ngoài thì lãng phí 18 | - Hệ thống được maintain thường xuyên, việc di dời server khi có big update là điều không thể tránh khỏi 19 | - Từng sử dụng shell script để backup data, tuy nhiên mỗi lần di dời hệ thống (trên 3 môi trường dev, staging, production) thì việc settup lại shell script khá cực. 20 | - Không sử dụng các dịch vụ cloud database mà dùng giải pháp cây nhà lá vườn - deploy dưới dạng services 21 | 22 | => Mình sẽ cần dev một service vừa có khả năng truy cập vào mongodb service để chạy backup toàn bộ các database, vừa có khả năng đẩy file backup lên kho lưu trữ (mình chọn google drive vì nó free) 23 | 24 | ## 3. Các tính năng 25 | - Tự động backup database theo thời gian chỉ định và đẩy lên Google Drive 26 | - Cho phép Force Backup 27 | - Cho phép tuỳ chỉnh tự động xoá các file backup trên Local và Drive sau thời gian chỉ định 28 | - Cho phép gửi thông báo tình trạng backup qua Telegram 29 | 30 | ## 4. Cách tích hợp 31 | ### 4.1. Lấy thông tin cần thiết từ Google 32 | - Google Client Mail 33 | - Google Private Key 34 | - Google Folder ID 35 | 36 | ### 4.2. Local test 37 | Tạo 1 file .env ngang hàng với thư mục src và khởi tạo các biến môi trường: 38 | ``` 39 | GOOGLE_CLIENT_MAIL="" 40 | GOOGLE_PRIVATE_KEY="" 41 | GOOGLE_FOLDER_ID="" 42 | IS_FORCE_BACKUP=1 43 | ``` 44 | Lưu ý: File .env chỉ hoạt động với Local, khi lên Production sẽ bị loại bỏ. 45 | 46 | ### 4.2. Docker test 47 | Chỉnh sửa file deploy.example.sh với các thông số như phần `4.2` 48 | 49 | Chạy 50 | ``` 51 | bash ./deploy.example.sh 52 | ``` 53 | 54 | ### 4.3. Production 55 | Tạo file docker-compose.yml như sau 56 | ``` 57 | version: '3.8' 58 | services: 59 | mongodb_backup: 60 | image: vtuanjs/mongodb_backup:lastest 61 | networks: 62 | - net 63 | environment: 64 | NODE_ENV: 'production' 65 | GOOGLE_CLIENT_MAIL: ${GOOGLE_CLIENT_MAIL} 66 | GOOGLE_PRIVATE_KEY: ${GOOGLE_PRIVATE_KEY} 67 | GOOGLE_FOLDER_ID: ${GOOGLE_FOLDER_ID} 68 | MONGO_BACKUP_USER: ${MONGO_BACKUP_USER} 69 | MONGO_BACKUP_PASSWORD: ${MONGO_BACKUP_PASSWORD} 70 | MONGO_URI: ${MONGO_URI} 71 | IS_FORCE_BACKUP: ${IS_FORCE_BACKUP} 72 | networks: 73 | net: 74 | driver: overlay 75 | attachable: true 76 | ``` 77 | 78 | Cấu hình bên trên là cấu hình tối thiểu để app có thể hoạt động. Bạn có thể tuỳ chỉnh thêm các tính năng nhờ Biến môi trường trong phần 5. 79 | 80 | Lưu ý: 81 | ``` 82 | Biến MONGO_BACKUP_USER: Là user có quyền "root" hoặc quyền "backup" database. Xem "phần 7" để được hướng dẫn cách tạo user có quyền Backup. 83 | Folder lưu trữ file backup cần được chia sẽ với Client Mail 84 | ``` 85 | 86 | ## 5. Các biến môi trường 87 | 88 | `MONGO_ROOT_USER`: Root User (Biến cũ ở Version 1.0.0 và sẽ bị ***loại bỏ*** trong tương lai) 89 | 90 | `MONGO_ROOT_PASSWORD`: Root Password (Biến cũ ở Version 1.0.0 và sẽ bị ***loại bỏ*** trong tương lai) 91 | 92 | `MONGO_BACKUP_USER`: Root Username hoặc Backup Username 93 | 94 | `MONGO_BACKUP_PASSWORD`: Root Password hặc Backup User Password 95 | 96 | `MONGO_URI`: Chuỗi connection string. Ex: "mongodb://localhost:27017" 97 | 98 | `MONGO_HOST`: Địa chỉ IP/ Tên container/ Tên service của MongoDB. Default: localhost 99 | 100 | `MONGO_PORT`: Port của MongoDB. Default: 27017 101 | 102 | --- 103 | 104 | `IS_AUTO_BACKUP`: Cho phép tự động Backup hay không, Default: 1 105 | 106 | `CRON_JOB_TIME`: Cấu hình thời gian Backup theo khung giờ GMT +7. Default '00 00 * * *' tương ứng 0:00 AM 107 | 108 | `IS_FORCE_BACKUP`: Backup ngay khi khởi động app. Default 0 109 | 110 | `IS_REMOVE_OLD_LOCAL_BACKUP`: Có cho phép xoá bản backup trên server khi hết hạn không? Default 1 111 | 112 | `KEEP_LAST_DAYS_OF_LOCAL_BACKUP`: Thời gian lưu trữ bản Local Backup. Default 2 113 | 114 | `IS_REMOVE_OLD_DRIVE_BACKUP`: Có cho phép xoá bản backup trên drive khi hết hạn không? Default 1 115 | 116 | `KEEP_LAST_DAYS_OF_DRIVE_BACKUP`: Thời hạn lưu trữ bản Drive Backup: Default 7 117 | 118 | --- 119 | 120 | `GOOGLE_CLIENT_MAIL`: Mail được cấp quyền sử dụng API (Do google phát sinh khi tạo Google User Service) 121 | 122 | `GOOGLE_PRIVATE_KEY`: Key được phát sinh khi tạo Google User Service 123 | 124 | `GOOGLE_FOLDER_ID`: Thư mục để lưu trữ file backup. Lưu ý: Thư mục này cần được chia sẽ với Google Client Email 125 | 126 | --- 127 | 128 | `IS_ALLOW_SEND_TELEGRAM_MESSAGE`: Có cho phép gửi tin nhắn qua Telegram không. Default 1 129 | 130 | `TELEGRAM_CHANEL_ID`: Cấu hình gửi thông báo backup qua telegram. Cấu trúc: "-chanelID" (Có dấu "-" đằng trước chanelID) 131 | 132 | `TELEGRAM_BOT_TOKEN` 133 | 134 | `TELEGRAM_MESSAGE_LEVELS`: Cấu hình loại tin nhắn sẽ gửi qua telegram. Mặc định: "info error" 135 | 136 | `TELEGRAM_PREFIX`: Prefix tuỳ chỉnh khi gửi tin nhắn qua Telegram. Default: MongoDB Backup 137 | 138 | --- 139 | 140 | `HTTP_FORCE_BACKUP_TOKEN`: Mã token dùng để user force backup qua api. Mặc định là tắt, chỉ hoạt động khi được truyền giá trị. Lưu ý: Bạn cần mapping Port 5050 ra host để chạy `curl` nhé! 141 | 142 | Cấu trúc: 143 | ``` 144 | GET: http://localhost:5050?token= 145 | 146 | POST: http://localhost:5050 147 | Body: { token: } 148 | ``` 149 | 150 | --- 151 | 152 | - Message mẫu Telegram được gửi: 153 | 154 | ``` 155 | 21/11/2020, 6:43:34 PM, VietNam 156 | 157 | ✅ MongoDB Backup 158 | 159 | Backup database to GG Drive with file name: 2020-11-21.zip successfully! 160 | ``` 161 | 162 | - Lưu ý: Telegram message chỉ hoạt động trên 3 môi trường: Development, Staging, Production. 163 | - Bạn cần truyền đủ TELEGRAM_CHANEL_ID và TELEGRAM_BOT_TOKEN thì hệ thống mới gửi tin nhắn qua Telegram được. 164 | 165 | ## 6. Demo và hướng dẫn cách lấy các tham số cần thiết 166 | 167 | Youtube: 168 | 169 | [![Demo](https://img.youtube.com/vi/NvYQqbnKP8g/0.jpg)](https://www.youtube.com/watch?v=NvYQqbnKP8g) 170 | 171 | ## 7. Thông tin khác 172 | 173 | ### 7.1. Cách tạo Backup User 174 | 175 | - Bước 1: Truy cập mongodb bằng shell 176 | ``` 177 | mongo --port 27017 -u "" --authenticationDatabase "admin" 178 | ``` 179 | Sau đó nhập Password để truy cập 180 | 181 | - Bước 2: Truy cập vào admin database 182 | ``` 183 | use admin 184 | ``` 185 | 186 | - Bước 3: Chạy lệnh sau để tạo user 187 | ``` 188 | db.createUser({ 189 | user: "", 190 | pwd: "", 191 | roles: [{ 192 | role: "backup", 193 | db: "admin" 194 | }] 195 | }) 196 | ``` 197 | 198 | ### 7.2. Cách tạo Telegram Bot để nhận thông báo 199 | Đang cập nhập... 200 | 201 | ## 8. Lời cảm ơn 202 | Cám ơn sự đông góp nhiệt tình của mọi người: 203 | 204 | * https://github.com/nnthuanegany 205 | * https://github.com/nhayhoc --------------------------------------------------------------------------------