├── .env
├── .gitignore
├── logo.png
├── .github
└── workflows
│ └── deploy.yml
├── package.json
├── docker.js
├── fileServer.js
├── index.js
├── README.md
├── restore.js
├── utils.js
└── engines
└── mysql.js
/.env:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dumps
3 | .env
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dandanthedev/DockGuard/HEAD/logo.png
--------------------------------------------------------------------------------
/.github/workflows/deploy.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches: main
4 |
5 | jobs:
6 | publish:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v4
10 | - uses: actions/setup-node@v3
11 | with:
12 | node-version: "20"
13 | - run: npm ci
14 | - uses: JS-DevTools/npm-publish@v3
15 | with:
16 | token: ${{ secrets.NPM_TOKEN }}
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dockguard",
3 | "version": "1.1.3",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "keywords": [],
10 | "author": "",
11 | "license": "ISC",
12 | "dependencies": {
13 | "dotenv": "^16.4.4",
14 | "kleur": "^4.1.5",
15 | "node-docker-api": "^1.1.22"
16 | },
17 | "bin": {
18 | "dockguard": "./index.js"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/docker.js:
--------------------------------------------------------------------------------
1 | const { Docker } = require("node-docker-api");
2 | const fs = require("fs");
3 | //find socketpath
4 | let socketPath;
5 | if (fs.existsSync("//./pipe/docker_engine")) {
6 | socketPath = "//./pipe/docker_engine";
7 | } else if (fs.existsSync("/var/run/docker.sock")) {
8 | socketPath = "/var/run/docker.sock";
9 | } else {
10 | console.log(
11 | "🦆 I couldn't find a docker socket. Please make sure docker is installed and running."
12 | );
13 | process.exit(1);
14 | }
15 |
16 | const docker = new Docker({ socketPath });
17 |
18 | module.exports = docker;
19 |
--------------------------------------------------------------------------------
/fileServer.js:
--------------------------------------------------------------------------------
1 | const { findFreePort } = require("./utils");
2 | const http = require("http");
3 | const fs = require("fs");
4 |
5 | async function serveFile(file, port) {
6 | if (!port) port = await findFreePort();
7 |
8 | //serve file, shutdown server after first hit
9 | const server = http.createServer(async (req, res) => {
10 | res.writeHead(200, { "Content-Type": "application/octet-stream" });
11 | const readStream = fs.createReadStream(file);
12 | readStream.pipe(res);
13 | readStream.on("end", () => {
14 | server.close();
15 | });
16 | });
17 | server.listen(port);
18 | return port;
19 | }
20 |
21 | async function startRecieve(location) {
22 | const port = await findFreePort();
23 |
24 | //run http server that listens for POST requests. When a request is received, the server will save the file to the specified location
25 | const server = http.createServer(async (req, res) => {
26 | if (req.method === "POST") {
27 | const writeStream = fs.createWriteStream(location);
28 |
29 | //pipe req body to file
30 | req.pipe(writeStream);
31 |
32 | req.on("end", () => {
33 | res.writeHead(200);
34 | res.end("file received");
35 | });
36 |
37 | writeStream.on("finish", () => {
38 | server.close();
39 | });
40 |
41 | writeStream.on("error", (err) => {
42 | console.error(err);
43 | server.close();
44 | });
45 | } else {
46 | res.writeHead(405);
47 | res.end(
48 | "You have stumbled upon the DockGuard uploading server. This server is used to pass the export file to DockGuard. This server should shut down automatically after the file is received."
49 | );
50 | }
51 | });
52 |
53 | server.listen(port);
54 |
55 | return port;
56 | }
57 |
58 | module.exports = {
59 | startRecieve,
60 | serveFile,
61 | };
62 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | require("dotenv").config();
4 |
5 | const isUnattended = process.argv.includes("--unattended");
6 | const restore = process.argv.includes("--restore");
7 |
8 | if (restore) {
9 | console.log("🦆 Running restore script");
10 | require("./restore.js");
11 | return;
12 | }
13 |
14 | const docker = require("./docker.js");
15 |
16 | const fs = require("fs");
17 | const kleur = require("kleur");
18 |
19 | const label = "dockguard";
20 |
21 | const { yesOrNo, supportedContainers } = require("./utils.js");
22 |
23 | const mysql = require("./engines/mysql.js");
24 |
25 | let verbose = process.argv.includes("--verbose");
26 | if (verbose) console.log(kleur.gray("🦆 Verbose mode enabled."));
27 |
28 | async function main() {
29 | if (!fs.existsSync("./dumps")) {
30 | fs.mkdirSync("./dumps");
31 | }
32 |
33 | console.log(kleur.gray("🦆 Looking for supported containers"));
34 |
35 | let dbPorts = await supportedContainers(docker);
36 |
37 | console.log(kleur.green("\n🦆 Found the following running services:"));
38 | for (const container of dbPorts) {
39 | //convert all container data to regular objects
40 |
41 | console.log(` - ${container.data.data.Names[0]} (${container.type})`);
42 | }
43 |
44 | //check if .env file exists
45 | if (!fs.existsSync(".env")) {
46 | console.log(
47 | kleur.yellow(`🦆 I couldn't find a .env file. I will create one for you.`)
48 | );
49 | fs.writeFileSync(".env", "");
50 | }
51 |
52 | //ask which databases to backup
53 | if (!isUnattended) {
54 | for (const container of dbPorts) {
55 | if (
56 | !(await yesOrNo(
57 | `🦆 Do you want to backup ${container.data.data.Names[0]}? (y/N) `
58 | ))
59 | ) {
60 | dbPorts = dbPorts.filter((c) => c !== container);
61 | }
62 | }
63 | }
64 |
65 | for (const container of dbPorts) {
66 | console.log(`🦆 Backing up ${container.data.data.Names[0]}...`);
67 |
68 | if (container.type === "mysql")
69 | if (!(await mysql.runExport(container.data, isUnattended, verbose))) {
70 | console.log(
71 | kleur.red(`🦆 Failed to backup ${container.data.data.Names[0]}.`)
72 | );
73 | continue;
74 | }
75 | }
76 |
77 | console.log(kleur.green("\n🦆 Quack! All done!"));
78 | }
79 |
80 | main();
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > :warning: **This project is in very early development stage!** I'm still actively working on adding more services & backup storages!
2 |
3 |
4 |
5 | # DockGuard
6 |
7 | The easiest way to backup your Docker containers.
8 |
9 | ## ???
10 |
11 | Are you struggling with backing something up, a database, container, or whatever?
12 | DockGuard is here for you! Simply run the command, enter in some basic data and get a neat export file for any kind of service!
13 | Accidentially deleted the entire database? [:)](https://www.youtube.com/watch?v=tLdRBsuvVKc) Don't worry, DockGuard can get it back with just a few clicks, or, well, keyboard presses i guess... Uhh, moving on!
14 |
15 | ## Support
16 |
17 | This is the full list of containers DockGuard currently supports:
18 |
19 | - MySQL database
20 |
21 | > if you know anything at all about coding, please add new container types to the engines! I'll whip up some documentation soon, for now you can just check the existing files.
22 |
23 | ## Usage
24 |
25 | You can go the fully guided route by running `npx dockguard` and following the prompts, or you can fully automate the process by adding environment variables.
26 |
27 | ### Fully Guided
28 |
29 | ### Backing up
30 |
31 | ```bash
32 | npx dockguard
33 | ```
34 |
35 | ### Restoring
36 |
37 | ```bash
38 | npx dockguard --restore [containername]
39 | ```
40 |
41 | ### Environment Variables (Currently backing up only, restoring is not supported yet)
42 |
43 | ```bash
44 | export CONTAINERNAME_USER=yourusername
45 | export CONTAINERNAME_PASSWORD=yourpassword
46 |
47 | npx dockguard --unattended
48 | ```
49 |
50 | ### Supported Environment Variables & Flags
51 |
52 | #### Environment Variables
53 |
54 | - `CONTAINERNAME_USER` - The username of the database running in docker container 'containername'
55 |
56 | - `CONTAINERNAME_PASSWORD` - The password of the database running in docker container 'containername'
57 |
58 | - `DOCKGUARD_DISABLE_AUTH` - Set to `true` to automatically temporarily disable authentication for the database running in docker container 'containername' (DANGEROUS FOR PRODUCTION)
59 |
60 | #### Flags
61 |
62 | - `--unattended` - Run DockGuard without any prompts
63 | - `--verbose` - Show verbose output
64 | - `--restore [CONTAINERNAME]` Restore from backup
65 |
--------------------------------------------------------------------------------
/restore.js:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env node
2 |
3 | require("dotenv").config();
4 |
5 | const isUnattended = process.argv.includes("--unattended");
6 | const verbose = process.argv.includes("--verbose");
7 |
8 | let stillExists = false;
9 |
10 | //set last argument as the action
11 | const action = process.argv[process.argv.length - 1];
12 |
13 | const kleur = require("kleur");
14 |
15 | if (action === "--restore") {
16 | console.log(
17 | kleur.red(
18 | "🦆 You need to provide the name of the container to restore the backup from"
19 | )
20 | );
21 | return;
22 | }
23 |
24 | const docker = require("./docker.js");
25 |
26 | const { yesOrNo, supportedContainers, choose } = require("./utils.js");
27 |
28 | const mysql = require("./engines/mysql.js");
29 |
30 | const fs = require("fs");
31 |
32 | async function main() {
33 | if (!fs.existsSync("./dumps")) {
34 | console.log(
35 | kleur.red(
36 | "🦆 I couldn't find a dumps folder. Please run the backup script first."
37 | )
38 | );
39 | process.exit(1);
40 | }
41 |
42 | const dbPorts = await supportedContainers(docker, verbose);
43 |
44 | const container = dbPorts.find(
45 | (c) => c.data.data.Names[0].replace("/", "") === action
46 | );
47 |
48 | if (container) stillExists = true;
49 |
50 | //recursively search dumps folder for files
51 | const files = [];
52 | const walk = async (dir) => {
53 | const list = fs.readdirSync(dir);
54 | for (const file of list) {
55 | const path = `${dir}/${file}`;
56 | const stat = fs.statSync(path);
57 | if (stat && stat.isDirectory()) {
58 | await walk(path);
59 | } else {
60 | files.push(path);
61 | }
62 |
63 | if (file === list[list.length - 1]) {
64 | return;
65 | }
66 | }
67 | };
68 | await walk("./dumps");
69 |
70 | //find dumps for the container
71 | const containerFiles = files.filter((f) =>
72 | f.includes(container?.data?.data?.Names[0])
73 | );
74 |
75 | if (containerFiles.length === 0) {
76 | console.log(
77 | kleur.red(
78 | `🦆 I couldn't find any dumps for ${container?.data?.data?.Names[0]}. Try starting the container.`
79 | )
80 | );
81 | process.exit(1);
82 | }
83 |
84 | console.log(kleur.gray("🦆 What would you like to do?"));
85 |
86 | const options = ["Restore to a new container"];
87 |
88 | if (stillExists) options.push("Restore to the original container");
89 |
90 | let choice;
91 | if (options.length > 1) {
92 | choice = await choose(options);
93 | } else {
94 | choice = options[0];
95 | }
96 |
97 | const newContainer = choice === "Restore to a new container";
98 |
99 | if (!newContainer) {
100 | console.log(
101 | kleur.red(
102 | "Please ensure that the original container is empty, as no data will be deleted."
103 | )
104 | );
105 | }
106 |
107 | if (container.type === "mysql") {
108 | if (
109 | !(await mysql.runRestore(
110 | container.data,
111 | isUnattended,
112 | verbose,
113 | newContainer
114 | ))
115 | ) {
116 | process.exit(1);
117 | }
118 | }
119 |
120 | console.log(kleur.green(`🦆 Your database is ready to rock 🪨`));
121 | }
122 |
123 | main();
124 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 |
3 | async function prompt(question, password = false) {
4 | const readline = require("readline").createInterface({
5 | input: process.stdin,
6 | output: process.stdout,
7 | });
8 |
9 | const promise = new Promise((resolve) => {
10 | readline.question(question, (answer) => {
11 | if (password) readline.close();
12 | resolve(answer);
13 | });
14 | });
15 |
16 | if (password)
17 | readline._writeToOutput = function _writeToOutput(stringToWrite) {
18 | readline.output.write("*");
19 | };
20 |
21 | const result = await promise;
22 |
23 | readline.close();
24 |
25 | return result;
26 | }
27 | async function yesOrNo(question) {
28 | const readline = require("readline").createInterface({
29 | input: process.stdin,
30 | output: process.stdout,
31 | });
32 |
33 | const promise = new Promise((resolve) => {
34 | readline.question(question, (answer) => {
35 | readline.close();
36 | resolve(answer);
37 | });
38 | });
39 |
40 | const result = await promise;
41 |
42 | return result.toLowerCase().startsWith("y");
43 | }
44 | const promisifyStream = (stream, send = "") => {
45 | return new Promise((resolve) => {
46 | let data = "";
47 | stream.on("data", (chunk) => {
48 | data += chunk.toString();
49 | });
50 | stream.on("end", () => {
51 | resolve(data);
52 | });
53 | if (send) stream.write(send);
54 | });
55 | };
56 |
57 | async function supportedContainers(docker, verbose) {
58 | //load all engines
59 | const engines = fs.readdirSync("./engines").map((f) => f.split(".")[0]);
60 |
61 | const list = await docker.container.list();
62 |
63 | if (verbose) console.log(list);
64 |
65 | const dbPorts = [];
66 |
67 | //loop through all engines
68 | for (const engine of engines) {
69 | const engineModule = require(`./engines/${engine}`);
70 | const runningDatabases = await engineModule.detectRunning(list);
71 | dbPorts.push(...runningDatabases.map((c) => ({ type: engine, data: c })));
72 | }
73 |
74 | return dbPorts;
75 | }
76 | async function choose(options) {
77 | const readline = require("readline").createInterface({
78 | input: process.stdin,
79 | output: process.stdout,
80 | });
81 |
82 | const promise = new Promise((resolve) => {
83 | readline.question(
84 | options.map((o, i) => `${i + 1}. ${o}`).join("\n") + "\n",
85 | (answer) => {
86 | readline.close();
87 | resolve(answer);
88 | }
89 | );
90 | });
91 |
92 | const result = await promise;
93 |
94 | return options[result - 1];
95 | }
96 | function randomString(length) {
97 | const chars =
98 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
99 | let result = "";
100 | for (let i = 0; i < length; i++) {
101 | result += chars.charAt(Math.floor(Math.random() * chars.length));
102 | }
103 | return result;
104 | }
105 |
106 | async function findFreePort() {
107 | const net = require("net");
108 | return new Promise((resolve, reject) => {
109 | const server = net.createServer();
110 | server.unref();
111 | server.on("error", reject);
112 | server.listen(0, () => {
113 | const port = server.address().port;
114 | server.close(() => resolve(port));
115 | });
116 | });
117 | }
118 |
119 | module.exports = {
120 | prompt,
121 | yesOrNo,
122 | promisifyStream,
123 | supportedContainers,
124 | choose,
125 | randomString,
126 | findFreePort,
127 | };
128 |
--------------------------------------------------------------------------------
/engines/mysql.js:
--------------------------------------------------------------------------------
1 | const fs = require("fs");
2 | const {
3 | prompt,
4 | yesOrNo,
5 | promisifyStream,
6 | randomString,
7 | } = require("../utils.js");
8 | const { serveFile, closeServer, startRecieve } = require("../fileServer.js");
9 | const kleur = require("kleur");
10 |
11 | const docker = require("../docker.js");
12 |
13 | async function disableAuthChecks(container, verbose = false) {
14 | console.log(
15 | kleur.white(" 🦆 Restarting mysqld with authchecks disabled")
16 | );
17 |
18 | //stop container
19 | console.log(kleur.gray(" Stopping container..."));
20 | await container.stop();
21 |
22 | //run shell with auth disabled
23 | console.log(kleur.gray(" Doing the old switcheroo..."));
24 |
25 | await container.start();
26 | const authDisabled = await container.exec
27 | .create({
28 | AttachStdout: true,
29 | AttachStderr: true,
30 | Cmd: ["mysqld", "--skip-grant-tables"],
31 | })
32 | .then((exec) => {
33 | return exec.start({ Detach: false });
34 | });
35 |
36 | if (verbose) console.log(authDisabled);
37 |
38 | console.log(
39 | kleur.gray(
40 | " --skip-grant-tables injected. Waiting for database to start..."
41 | )
42 | );
43 |
44 | while (true) {
45 | await new Promise((resolve) => setTimeout(resolve, 1000));
46 | const exec = await container.exec
47 | .create({
48 | AttachStdout: true,
49 | AttachStderr: true,
50 | Cmd: ["mysql", "-e", "SELECT 1"],
51 | })
52 | .then((exec) => {
53 | return exec.start({ Detach: false });
54 | })
55 | .then((stream) => promisifyStream(stream));
56 |
57 | if (exec.includes("1")) {
58 | console.log(kleur.green(" 🎉 Database is up and running!"));
59 | break;
60 | }
61 | }
62 | }
63 |
64 | async function detectRunning(containers) {
65 | return containers.filter((c) =>
66 | c.data.Ports.some((p) => p.PrivatePort === 3306)
67 | );
68 | }
69 |
70 | async function runExport(container, isUnattended, verbose) {
71 | if (!fs.existsSync("./dumps/mysql"))
72 | fs.mkdirSync("./dumps/mysql", { recursive: true });
73 |
74 | let foundInEnv = [];
75 |
76 | let username =
77 | process.env[`${container.data.Names[0].replace("/", "")}_USER`];
78 | let password =
79 | process.env[`${container.data.Names[0].replace("/", "")}_PASSWORD`];
80 |
81 | if (username) foundInEnv.push("username");
82 |
83 | if (password) foundInEnv.push("password");
84 |
85 | if (!username || !password)
86 | console.log(
87 | kleur.yellow(
88 | ` ⚠️ I couldn't find credentials for ${container.data.Names[0]} in the environment variables. Please provide them manually.`
89 | )
90 | );
91 |
92 | if (!username && !isUnattended && !process.env.DOCKGUARD_DISABLE_AUTH)
93 | username = await prompt(" 😃 Please enter the database username: ");
94 |
95 | if (!username)
96 | if (
97 | process.env.DOCKGUARD_DISABLE_AUTH ||
98 | (!username &&
99 | !password &&
100 | !isUnattended &&
101 | (await yesOrNo(
102 | ` Do you want to temporarily disable authentication to try to backup the database? (y/N)\n${kleur.red(
103 | " THIS IS DANGEROUS! y/N "
104 | )}`
105 | )))
106 | ) {
107 | const res = await disableAuthChecks(container);
108 | // if (!res) return false; //TODO: error handling
109 | console.log(
110 | kleur.red(
111 | " ⚠️ Authentication has been temporarily disabled. This script will attempt to auto-restart the container after the backup. If it fails, you will need to manually restart the container to re-enable authentication."
112 | )
113 | );
114 | username = "root";
115 | password = false;
116 | }
117 |
118 | if (!password && password !== false && !isUnattended)
119 | password = await prompt(
120 | " 🔑 Please enter the database password: ",
121 | true
122 | );
123 |
124 | console.log(kleur.gray(" 🦆 Checking if authentication is valid..."));
125 | const exec = await container.exec
126 | .create({
127 | AttachStdout: true,
128 | AttachStderr: true,
129 | Cmd: ["mysql", "-u", username, "-p" + password, "-e", "SELECT 1"],
130 | })
131 | .then((exec) => {
132 | return exec.start({ Detach: false });
133 | })
134 | .then((stream) => promisifyStream(stream));
135 |
136 | if (exec.includes("ERROR")) {
137 | console.log(
138 | kleur.red(
139 | " ⚠️ Authentication failed. Please provide valid credentials."
140 | )
141 | );
142 | return false;
143 | }
144 |
145 | if (
146 | !foundInEnv.length > 0 &&
147 | !isUnattended &&
148 | !(username === "root" && password === false) &&
149 | (await yesOrNo(
150 | "\n Do you want to automagically ✨ append the missing credentials to the environment variables? (y/N) "
151 | ))
152 | ) {
153 | fs.appendFileSync(
154 | ".env",
155 | `\n${container.data.Names[0].replace(
156 | "/",
157 | ""
158 | )}_USER=${username}\n${container.data.Names[0].replace(
159 | "/",
160 | ""
161 | )}_PASSWORD=${password}\n`
162 | );
163 | console.log(
164 | kleur.green(
165 | ` 🎉 Credentials for ${container.data.Names[0]} have been added to the environment variables.`
166 | )
167 | );
168 | }
169 |
170 | //Webserver
171 | console.log(kleur.gray(" 🦆 Starting webserver..."));
172 |
173 | const dumpTime = new Date();
174 |
175 | const port = await startRecieve(
176 | `./dumps/mysql/${container.data.Names[0]}-${dumpTime.getTime()}.gz`
177 | );
178 |
179 | console.log(kleur.gray(" 🦆 Running mysqldump..."));
180 |
181 | //rmysqldump --all-databases
182 | let dumpExec = await container.exec
183 | .create({
184 | AttachStdout: true,
185 | AttachStderr: true,
186 | Cmd: [
187 | "sh",
188 | "-c",
189 | `mysqldump -u ${username} -p${password} --all-databases | gzip > /tmp/dockguard.gz`,
190 | ],
191 | })
192 | .then((exec) => {
193 | return exec.start({ Detach: false });
194 | })
195 | .then((stream) => promisifyStream(stream));
196 |
197 | if (verbose) console.log(dumpExec);
198 |
199 | console.log(kleur.gray(" 🦆 Sending dump to webserver..."));
200 |
201 | //send post request to webserver
202 | const sendExec = await container.exec
203 | .create({
204 | AttachStdout: true,
205 | AttachStderr: true,
206 | Cmd: [
207 | "sh",
208 | "-c",
209 | `curl -s -X POST --data-binary @/tmp/dockguard.gz http://host.docker.internal:${port}/`,
210 | ],
211 | })
212 | .then((exec) => {
213 | return exec.start({ Detach: false });
214 | })
215 | .then((stream) => promisifyStream(stream));
216 |
217 | if (verbose) console.log(sendExec);
218 |
219 | console.log(kleur.gray(" 🦆 Cleaning up..."));
220 | await container.exec
221 | .create({
222 | AttachStdout: true,
223 | AttachStderr: true,
224 | Cmd: ["sh", "-c", `rm /tmp/dockguard.sql /tmp/dockguard.gz`],
225 | })
226 | .then((exec) => {
227 | return exec.start({ Detach: false });
228 | });
229 |
230 | console.log(kleur.gray(" 🦆 Checking if dump has been created"));
231 |
232 | if (
233 | !fs.existsSync(
234 | `./dumps/mysql/${container.data.Names[0]}-${dumpTime.getTime()}.gz`
235 | ) ||
236 | fs.statSync(
237 | `./dumps/mysql/${container.data.Names[0]}-${dumpTime.getTime()}.gz`
238 | ).size < 1
239 | ) {
240 | console.log(
241 | kleur.red(
242 | " ⚠️ I couldn't find the dump file. Something went wrong during the process."
243 | )
244 | );
245 | return false;
246 | }
247 |
248 | //if auth was disabled, re-enable it
249 | if (username === "root" && password === false) {
250 | console.log(
251 | kleur.gray(" 🦆 Restarting container to re-enable authchecks...")
252 | );
253 | await container.restart();
254 | }
255 |
256 | console.log(
257 | kleur.green(
258 | ` 🎉 Backup of ${
259 | container.data.Names[0]
260 | } has been saved to ./dumps/mysql/${
261 | container.data.Names[0]
262 | }-${dumpTime.getTime()}.gz`
263 | )
264 | );
265 |
266 | return true;
267 | }
268 |
269 | async function runRestore(container, isUnattended, verbose, newContainer) {
270 | const backups = fs.readdirSync("./dumps/mysql");
271 | const backupsForContainer = backups.filter((b) =>
272 | b.includes(container.data.Names[0].replace("/", ""))
273 | );
274 |
275 | //hacky fix
276 | const trimmedBackups = backupsForContainer;
277 |
278 | if (trimmedBackups.length < 1) {
279 | console.log(
280 | kleur.red(
281 | `🦆 I couldn't find any trimmed backups for ${container.data.Names[0]}.`
282 | )
283 | );
284 | process.exit(1);
285 | }
286 |
287 | let selectedBackup = trimmedBackups[0];
288 |
289 | if (trimmedBackups.length > 1) {
290 | console.log(kleur.gray("🦆 Which backup would you like to restore?"));
291 |
292 | selectedBackup = await choose(trimmedBackups);
293 | }
294 |
295 | let password;
296 | let username;
297 |
298 | if (newContainer) {
299 | password = randomString(32);
300 | username = "root";
301 |
302 | console.log(kleur.gray("🦆 Creating new container..."));
303 | container = await docker.container.create({
304 | Image: "mysql:latest",
305 | name: "dockguard-restore-" + randomString(8),
306 | Env: [`MYSQL_ROOT_PASSWORD=${password}`],
307 | });
308 | console.log(
309 | kleur.green(
310 | `🦆 New container created: ${container.id}. Root password: ${password}`
311 | )
312 | );
313 | }
314 |
315 | if (!username && !isUnattended) {
316 | username = await prompt("🦆 Please enter the database username: ");
317 | if (
318 | !username &&
319 | !isUnattended &&
320 | (await yesOrNo(
321 | `🦆 Do you want to automagically & temporarily disable authentication? ${kleur.red(
322 | "WARNING, THIS IS DANGEROUS! "
323 | )}`
324 | ))
325 | ) {
326 | await disableAuthChecks(container, verbose);
327 | username = "root";
328 | password = false;
329 | }
330 | }
331 |
332 | if (!password && password !== false && !isUnattended)
333 | password = await prompt("🦆 Please enter the database password: ", true);
334 |
335 | if (!password && password !== false)
336 | console.log(
337 | kleur.red(
338 | "🦆 I couldn't find a password for the database. Please provide one."
339 | )
340 | );
341 |
342 | if (
343 | !(await yesOrNo(
344 | `🦆 Are you sure you want to restore the database? ${kleur.red(
345 | "THIS WILL DELETE EVERYTHING CURRENTLY IN THE DATABASE!"
346 | )}`
347 | ))
348 | )
349 | return;
350 |
351 | //ensure the container is running
352 | console.log(kleur.gray("\n🦆 Starting container..."));
353 | await container.start();
354 |
355 | //wait untill the database is up and running
356 | console.log(kleur.gray("🦆 Waiting for database to start..."));
357 | while (true) {
358 | await new Promise((resolve) => setTimeout(resolve, 1000));
359 | const exec = await container.exec
360 | .create({
361 | AttachStdout: true,
362 | AttachStderr: true,
363 | Cmd: ["mysql", "-e", "SELECT 1"],
364 | })
365 | .then((exec) => {
366 | return exec.start({ Detach: false });
367 | })
368 | .then((stream) => promisifyStream(stream));
369 |
370 | if (exec.includes("1")) {
371 | console.log(kleur.green("🎉 Database is up and running!"));
372 | break;
373 | }
374 | }
375 |
376 | //if database was created by us, wait untill the user has been created
377 | if (newContainer) {
378 | console.log(kleur.gray("🦆 Waiting for root user to be created..."));
379 | while (true) {
380 | await new Promise((resolve) => setTimeout(resolve, 1000));
381 | const exec = await container.exec
382 | .create({
383 | AttachStdout: true,
384 | AttachStderr: true,
385 | Cmd: ["mysql", "-u", username, "-p" + password, "-e", "SELECT 1"],
386 | })
387 | .then((exec) => {
388 | return exec.start({ Detach: false });
389 | })
390 | .then((stream) => promisifyStream(stream));
391 |
392 | if (!exec.includes("ERROR")) {
393 | console.log(kleur.green("🎉 Root user is created!"));
394 | break;
395 | }
396 | }
397 | }
398 |
399 | console.log(kleur.gray("🦆 Spinning up webserver"));
400 |
401 | const port = await serveFile(`./dumps/mysql/${selectedBackup}`);
402 |
403 | console.log(
404 | kleur.gray(`🦆 Backup is listening on ${port}/backup.gz. Restoring...`)
405 | );
406 |
407 | const internalIp = "host.docker.internal";
408 | //restore backup
409 | let exec = await container.exec
410 | .create({
411 | AttachStdout: true,
412 | AttachStderr: true,
413 | Cmd: [
414 | "sh",
415 | "-c",
416 | `curl -s http://${internalIp}:${port}/backup.gz > /tmp/dockguard.gz`,
417 | ],
418 | })
419 | .then((exec) => {
420 | return exec.start({ Detach: false });
421 | })
422 | .then((stream) => promisifyStream(stream));
423 |
424 | if (verbose) console.log(exec);
425 |
426 | console.log(kleur.gray("🦆 Extracting backup..."));
427 |
428 | const extractExec = await container.exec
429 | .create({
430 | AttachStdout: true,
431 | AttachStderr: true,
432 | Cmd: ["sh", "-c", `gunzip /tmp/dockguard.gz`],
433 | })
434 | .then((exec) => {
435 | return exec.start({ Detach: false });
436 | })
437 | .then((stream) => promisifyStream(stream));
438 |
439 | if (verbose) console.log(extractExec);
440 |
441 | console.log(kleur.gray("🦆 Restoring backup..."));
442 |
443 | const restoreExec = await container.exec
444 | .create({
445 | AttachStdout: true,
446 | AttachStderr: true,
447 | Cmd: ["sh", "-c", `mysql -u ${username} -p${password} < /tmp/dockguard`],
448 | })
449 | .then((exec) => {
450 | return exec.start({ Detach: false });
451 | })
452 | .then((stream) => promisifyStream(stream));
453 |
454 | if (exec.includes("ERROR")) {
455 | console.log(
456 | kleur.red(
457 | "🦆 Failed to restore database. The webserver might not have been reachable from within the docker container, or the backup file could have been malformed. Use --verbose for more info."
458 | )
459 | );
460 | return false;
461 | }
462 |
463 | return true;
464 | }
465 |
466 | module.exports = {
467 | runExport,
468 | detectRunning,
469 | runRestore,
470 | };
471 |
--------------------------------------------------------------------------------