budibase/packages/cli/src/backups/index.js

193 lines
5.0 KiB
JavaScript
Raw Normal View History

const Command = require("../structures/Command")
const { CommandWords } = require("../constants")
const dotenv = require("dotenv")
const fs = require("fs")
const { join } = require("path")
const { string } = require("../questions")
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"
const REQUIRED = [
{ 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" },
]
function checkURLs(config) {
const mainPort = config["MAIN_PORT"],
username = config["COUCH_DB_USER"],
password = config["COUCH_DB_PASSWORD"]
if (!config["COUCH_DB_URL"] && mainPort && username && password) {
config[
"COUCH_DB_URL"
] = `http://${username}:${password}@localhost:${mainPort}/db/`
}
if (!config["MINIO_URL"]) {
config["MINIO_URL"] = DEFAULT_MINIO
}
return config
}
async function askQuestions() {
console.log(
"*** NOTE: use a .env file to load these parameters repeatedly ***"
)
let config = {}
for (let property of REQUIRED) {
config[property.value] = await string(property.value, property.default)
}
return config
}
function loadEnvironment(path) {
if (!fs.existsSync(path)) {
throw "Unable to file specified .env file"
}
const env = fs.readFileSync(path, "utf8")
const config = checkURLs(dotenv.parse(env))
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) {
let config
if (envFile !== true) {
config = loadEnvironment(envFile)
} else {
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`
}
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}`)
}
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 })
}
async function pickOne(opts) {
if (opts["import"]) {
return importBackup(opts)
} else if (opts["export"]) {
return exportBackup(opts)
}
}
const command = new Command(`${CommandWords.BACKUPS}`)
.addHelp(
"Allows building backups of Budibase, as well as importing a backup to a new instance."
)
.addSubOption(
"--export [filename]",
"Export a backup from an existing Budibase installation.",
exportBackup
)
.addSubOption(
"--import [filename]",
"Import a backup to a new Budibase installation.",
importBackup
)
.addSubOption(
"--env [envFile]",
"Provide an environment variable file to configure the CLI.",
pickOne
)
exports.command = command