2022-06-30 18:31:18 +02:00
|
|
|
const Command = require("../structures/Command")
|
|
|
|
const { CommandWords } = require("../constants")
|
|
|
|
const dotenv = require("dotenv")
|
|
|
|
const fs = require("fs")
|
2022-06-30 20:20:50 +02:00
|
|
|
const { join } = require("path")
|
2022-06-30 18:31:18 +02:00
|
|
|
const { string } = require("../questions")
|
2022-06-30 20:20:50 +02:00
|
|
|
const { env } = require("@budibase/backend-core")
|
|
|
|
const { getPouch, getAllDbs } = require("@budibase/backend-core/db")
|
|
|
|
const tar = require("tar")
|
|
|
|
const { progressBar } = require("../utils")
|
|
|
|
|
|
|
|
const DEFAULT_COUCH = "http://budibase:budibase@localhost:10000/db/"
|
|
|
|
const DEFAULT_MINIO = "http://localhost:10000/"
|
|
|
|
const TEMP_DIR = ".temp"
|
2022-06-30 18:31:18 +02:00
|
|
|
|
|
|
|
const REQUIRED = [
|
2022-06-30 20:20:50 +02:00
|
|
|
{ value: "MAIN_PORT", default: "10000" },
|
|
|
|
{ value: "COUCH_DB_URL", default: DEFAULT_COUCH },
|
|
|
|
{ value: "MINIO_URL", default: DEFAULT_MINIO },
|
|
|
|
{ value: "MINIO_ACCESS_KEY" },
|
|
|
|
{ value: "MINIO_SECRET_KEY" },
|
2022-06-30 18:31:18 +02:00
|
|
|
]
|
|
|
|
|
2022-06-30 20:20:50 +02:00
|
|
|
function checkURLs(config) {
|
2022-06-30 18:31:18 +02:00
|
|
|
const mainPort = config["MAIN_PORT"],
|
2022-06-30 20:20:50 +02:00
|
|
|
username = config["COUCH_DB_USER"],
|
2022-06-30 18:31:18 +02:00
|
|
|
password = config["COUCH_DB_PASSWORD"]
|
2022-06-30 20:20:50 +02:00
|
|
|
if (!config["COUCH_DB_URL"] && mainPort && username && password) {
|
2022-06-30 18:31:18 +02:00
|
|
|
config[
|
|
|
|
"COUCH_DB_URL"
|
|
|
|
] = `http://${username}:${password}@localhost:${mainPort}/db/`
|
|
|
|
}
|
2022-06-30 20:20:50 +02:00
|
|
|
if (!config["MINIO_URL"]) {
|
|
|
|
config["MINIO_URL"] = DEFAULT_MINIO
|
|
|
|
}
|
2022-06-30 18:31:18 +02:00
|
|
|
return config
|
|
|
|
}
|
|
|
|
|
|
|
|
async function askQuestions() {
|
2022-06-30 20:20:50 +02:00
|
|
|
console.log(
|
|
|
|
"*** NOTE: use a .env file to load these parameters repeatedly ***"
|
|
|
|
)
|
2022-06-30 18:31:18 +02:00
|
|
|
let config = {}
|
|
|
|
for (let property of REQUIRED) {
|
2022-06-30 20:20:50 +02:00
|
|
|
config[property.value] = await string(property.value, property.default)
|
2022-06-30 18:31:18 +02:00
|
|
|
}
|
|
|
|
return config
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadEnvironment(path) {
|
|
|
|
if (!fs.existsSync(path)) {
|
|
|
|
throw "Unable to file specified .env file"
|
|
|
|
}
|
|
|
|
const env = fs.readFileSync(path, "utf8")
|
2022-06-30 20:20:50 +02:00
|
|
|
const config = checkURLs(dotenv.parse(env))
|
2022-06-30 18:31:18 +02:00
|
|
|
for (let required of REQUIRED) {
|
|
|
|
if (!config[required.value]) {
|
|
|
|
throw `Cannot find "${required.value}" property in .env file`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return config
|
|
|
|
}
|
|
|
|
|
|
|
|
// true is the default value passed by commander
|
|
|
|
async function getConfig(envFile = true) {
|
2022-06-30 20:20:50 +02:00
|
|
|
let config
|
2022-06-30 18:31:18 +02:00
|
|
|
if (envFile !== true) {
|
2022-06-30 20:20:50 +02:00
|
|
|
config = loadEnvironment(envFile)
|
2022-06-30 18:31:18 +02:00
|
|
|
} else {
|
2022-06-30 20:20:50 +02:00
|
|
|
config = askQuestions()
|
|
|
|
}
|
|
|
|
for (let required of REQUIRED) {
|
|
|
|
env._set(required.value, config[required.value])
|
|
|
|
}
|
|
|
|
return config
|
|
|
|
}
|
|
|
|
|
|
|
|
function replication(from, to) {
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
from.replicate
|
|
|
|
.to(to)
|
|
|
|
.on("complete", () => {
|
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
.on("error", err => {
|
|
|
|
reject(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
function getPouches() {
|
|
|
|
const Remote = getPouch({ replication: true })
|
|
|
|
const Local = getPouch({ onDisk: true, directory: TEMP_DIR })
|
|
|
|
return { Remote, Local }
|
|
|
|
}
|
|
|
|
|
|
|
|
async function exportBackup(opts) {
|
|
|
|
const envFile = opts.env || undefined
|
|
|
|
await getConfig(envFile)
|
|
|
|
let filename = opts["export"] || opts
|
|
|
|
if (typeof filename !== "string") {
|
|
|
|
filename = `backup-${new Date().toISOString()}.tar.gz`
|
2022-06-30 18:31:18 +02:00
|
|
|
}
|
2022-06-30 20:20:50 +02:00
|
|
|
await getConfig(envFile)
|
|
|
|
const dbList = await getAllDbs()
|
|
|
|
const { Remote, Local } = getPouches()
|
|
|
|
if (fs.existsSync(TEMP_DIR)) {
|
|
|
|
fs.rmSync(TEMP_DIR, { recursive: true })
|
|
|
|
}
|
|
|
|
const couchDir = join(TEMP_DIR, "couchdb")
|
|
|
|
fs.mkdirSync(TEMP_DIR)
|
|
|
|
fs.mkdirSync(couchDir)
|
|
|
|
const bar = progressBar(dbList.length)
|
|
|
|
let count = 0
|
|
|
|
for (let db of dbList) {
|
|
|
|
bar.update(++count)
|
|
|
|
const remote = new Remote(db)
|
|
|
|
const local = new Local(join(TEMP_DIR, "couchdb", db))
|
|
|
|
await replication(remote, local)
|
|
|
|
}
|
|
|
|
bar.stop()
|
|
|
|
tar.create(
|
|
|
|
{
|
|
|
|
sync: true,
|
|
|
|
gzip: true,
|
|
|
|
file: filename,
|
|
|
|
cwd: join(TEMP_DIR),
|
|
|
|
},
|
|
|
|
["couchdb"]
|
|
|
|
)
|
|
|
|
fs.rmSync(TEMP_DIR, { recursive: true })
|
|
|
|
console.log(`Generated export file - ${filename}`)
|
2022-06-30 18:31:18 +02:00
|
|
|
}
|
|
|
|
|
2022-06-30 20:20:50 +02:00
|
|
|
async function importBackup(opts) {
|
|
|
|
const envFile = opts.env || undefined
|
|
|
|
const filename = opts["import"] || opts
|
|
|
|
await getConfig(envFile)
|
|
|
|
if (!filename || !fs.existsSync(filename)) {
|
|
|
|
console.error("Cannot import without specifying a valid file to import")
|
|
|
|
process.exit(-1)
|
|
|
|
}
|
|
|
|
fs.mkdirSync(TEMP_DIR)
|
|
|
|
tar.extract({
|
|
|
|
sync: true,
|
|
|
|
cwd: join(TEMP_DIR),
|
|
|
|
file: filename,
|
|
|
|
})
|
|
|
|
const { Remote, Local } = getPouches()
|
|
|
|
const dbList = fs.readdirSync(join(TEMP_DIR, "couchdb"))
|
|
|
|
const bar = progressBar(dbList.length)
|
|
|
|
let count = 0
|
|
|
|
for (let db of dbList) {
|
|
|
|
bar.update(++count)
|
|
|
|
const remote = new Remote(db)
|
|
|
|
const local = new Local(join(TEMP_DIR, "couchdb", db))
|
|
|
|
await replication(local, remote)
|
|
|
|
}
|
|
|
|
bar.stop()
|
|
|
|
console.log("Import complete")
|
|
|
|
fs.rmSync(TEMP_DIR, { recursive: true })
|
2022-06-30 18:31:18 +02:00
|
|
|
}
|
|
|
|
|
2022-06-30 20:20:50 +02:00
|
|
|
async function pickOne(opts) {
|
|
|
|
if (opts["import"]) {
|
|
|
|
return importBackup(opts)
|
|
|
|
} else if (opts["export"]) {
|
|
|
|
return exportBackup(opts)
|
|
|
|
}
|
2022-06-30 18:31:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const command = new Command(`${CommandWords.BACKUPS}`)
|
|
|
|
.addHelp(
|
|
|
|
"Allows building backups of Budibase, as well as importing a backup to a new instance."
|
|
|
|
)
|
|
|
|
.addSubOption(
|
2022-06-30 20:20:50 +02:00
|
|
|
"--export [filename]",
|
2022-06-30 18:31:18 +02:00
|
|
|
"Export a backup from an existing Budibase installation.",
|
|
|
|
exportBackup
|
|
|
|
)
|
|
|
|
.addSubOption(
|
2022-06-30 20:20:50 +02:00
|
|
|
"--import [filename]",
|
2022-06-30 18:31:18 +02:00
|
|
|
"Import a backup to a new Budibase installation.",
|
|
|
|
importBackup
|
|
|
|
)
|
2022-06-30 20:20:50 +02:00
|
|
|
.addSubOption(
|
|
|
|
"--env [envFile]",
|
|
|
|
"Provide an environment variable file to configure the CLI.",
|
|
|
|
pickOne
|
|
|
|
)
|
2022-06-30 18:31:18 +02:00
|
|
|
|
|
|
|
exports.command = command
|