Merge pull request #2815 from Budibase/feature/cloud-export

Cloud export -> self host import
This commit is contained in:
Michael Drury 2021-09-30 14:55:37 +01:00 committed by GitHub
commit 29b177208c
30 changed files with 2159 additions and 92 deletions

View File

@ -12,7 +12,7 @@ const populateFromDB = async (userId, tenantId) => {
const user = await getGlobalDB(tenantId).get(userId) const user = await getGlobalDB(tenantId).get(userId)
user.budibaseAccess = true user.budibaseAccess = true
if (!env.SELF_HOSTED) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(user.email) const account = await accounts.getAccount(user.email)
if (account) { if (account) {
user.account = account user.account = account

View File

@ -21,6 +21,7 @@ module.exports = {
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL, ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED), SELF_HOSTED: !!parseInt(process.env.SELF_HOSTED),
COOKIE_DOMAIN: process.env.COOKIE_DOMAIN, COOKIE_DOMAIN: process.env.COOKIE_DOMAIN,
isTest, isTest,

View File

@ -73,7 +73,7 @@ exports.tryAddTenant = async (tenantId, userId, email) => {
await Promise.all(promises) await Promise.all(promises)
} }
exports.getGlobalDB = (tenantId = null) => { exports.getGlobalDBName = (tenantId = null) => {
// tenant ID can be set externally, for example user API where // tenant ID can be set externally, for example user API where
// new tenants are being created, this may be the case // new tenants are being created, this may be the case
if (!tenantId) { if (!tenantId) {
@ -81,13 +81,16 @@ exports.getGlobalDB = (tenantId = null) => {
} }
let dbName let dbName
if (tenantId === DEFAULT_TENANT_ID) { if (tenantId === DEFAULT_TENANT_ID) {
dbName = StaticDatabases.GLOBAL.name dbName = StaticDatabases.GLOBAL.name
} else { } else {
dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}` dbName = `${tenantId}${SEPARATOR}${StaticDatabases.GLOBAL.name}`
} }
return dbName
}
exports.getGlobalDB = (tenantId = null) => {
const dbName = exports.getGlobalDBName(tenantId)
return getDB(dbName) return getDB(dbName)
} }

View File

@ -4,6 +4,9 @@
import { onMount } from "svelte" import { onMount } from "svelte"
let loaded = false let loaded = false
// don't react to these
let cloud = $admin.cloud
let shouldRedirect = !cloud || $admin.disableAccountPortal
$: multiTenancyEnabled = $admin.multiTenancy $: multiTenancyEnabled = $admin.multiTenancy
$: hasAdminUser = $admin?.checklist?.adminUser?.checked $: hasAdminUser = $admin?.checklist?.adminUser?.checked
@ -39,30 +42,35 @@
$: { $: {
// We should never see the org or admin user creation screens in the cloud // We should never see the org or admin user creation screens in the cloud
if (!cloud) {
const apiReady = $admin.loaded && $auth.loaded const apiReady = $admin.loaded && $auth.loaded
// if tenant is not set go to it // if tenant is not set go to it
if (loaded && apiReady && multiTenancyEnabled && !tenantSet) { if (
loaded &&
shouldRedirect &&
apiReady &&
multiTenancyEnabled &&
!tenantSet
) {
$redirect("./auth/org") $redirect("./auth/org")
} }
// Force creation of an admin user if one doesn't exist // Force creation of an admin user if one doesn't exist
else if (loaded && apiReady && !hasAdminUser) { else if (loaded && shouldRedirect && apiReady && !hasAdminUser) {
$redirect("./admin") $redirect("./admin")
} }
}
}
// Redirect to log in at any time if the user isn't authenticated // Redirect to log in at any time if the user isn't authenticated
$: { else if (
if (
loaded && loaded &&
(hasAdminUser || cloud) && (hasAdminUser || cloud) &&
!$auth.user && !$auth.user &&
!$isActive("./auth") && !$isActive("./auth") &&
!$isActive("./invite") !$isActive("./invite") &&
!$isActive("./admin")
) { ) {
const returnUrl = encodeURIComponent(window.location.pathname) const returnUrl = encodeURIComponent(window.location.pathname)
$redirect("./auth?", { returnUrl }) $redirect("./auth?", { returnUrl })
} else if ($auth?.user?.forceResetPassword) { }
// check if password reset required for user
else if ($auth.user?.forceResetPassword) {
$redirect("./auth/reset") $redirect("./auth/reset")
} }
} }

View File

@ -0,0 +1,50 @@
<script>
import { notifications, ModalContent, Dropzone, Body } from "@budibase/bbui"
import { post } from "builderStore/api"
let submitting = false
$: value = { file: null }
async function importApps() {
submitting = true
try {
// Create form data to create app
let data = new FormData()
data.append("importFile", value.file)
// Create App
const importResp = await post("/api/cloud/import", data, {})
const importJson = await importResp.json()
if (!importResp.ok) {
throw new Error(importJson.message)
}
// now reload to get to login
window.location.reload()
} catch (error) {
notifications.error(error)
submitting = false
}
}
</script>
<ModalContent
title="Import apps"
confirmText="Import apps"
onConfirm={importApps}
disabled={!value.file}
>
<Body
>Please upload the file that was exported from your Cloud environment to get
started</Body
>
<Dropzone
gallery={false}
label="File to import"
value={[value.file]}
on:change={e => {
value.file = e.detail?.[0]
}}
/>
</ModalContent>

View File

@ -7,18 +7,22 @@
Input, Input,
Body, Body,
ActionButton, ActionButton,
Modal,
} from "@budibase/bbui" } from "@budibase/bbui"
import { goto } from "@roxi/routify" import { goto } from "@roxi/routify"
import api from "builderStore/api" import api from "builderStore/api"
import { admin, auth } from "stores/portal" import { admin, auth } from "stores/portal"
import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte" import PasswordRepeatInput from "components/common/users/PasswordRepeatInput.svelte"
import ImportAppsModal from "./_components/ImportAppsModal.svelte"
import Logo from "assets/bb-emblem.svg" import Logo from "assets/bb-emblem.svg"
let adminUser = {} let adminUser = {}
let error let error
let modal
$: tenantId = $auth.tenantId $: tenantId = $auth.tenantId
$: multiTenancyEnabled = $admin.multiTenancy $: multiTenancyEnabled = $admin.multiTenancy
$: cloud = $admin.cloud
async function save() { async function save() {
try { try {
@ -38,6 +42,9 @@
} }
</script> </script>
<Modal bind:this={modal} padding={false} width="600px">
<ImportAppsModal />
</Modal>
<section> <section>
<div class="container"> <div class="container">
<Layout> <Layout>
@ -66,6 +73,15 @@
> >
Change organisation Change organisation
</ActionButton> </ActionButton>
{:else if !cloud}
<ActionButton
quiet
on:click={() => {
modal.show()
}}
>
Import from cloud
</ActionButton>
{/if} {/if}
</Layout> </Layout>
</Layout> </Layout>

View File

@ -9,10 +9,10 @@
$redirect("../") $redirect("../")
} }
// redirect to account portal for authentication in the cloud
if ( if (
!$auth.user && !$auth.user &&
$admin.cloud && $admin.cloud &&
!$admin.disableAccountPortal &&
$admin.accountPortalUrl && $admin.accountPortalUrl &&
!$admin?.checklist?.sso?.checked !$admin?.checklist?.sso?.checked
) { ) {

View File

@ -9,6 +9,7 @@
let tenantId = get(auth).tenantSet ? get(auth).tenantId : "" let tenantId = get(auth).tenantSet ? get(auth).tenantId : ""
$: multiTenancyEnabled = $admin.multiTenancy $: multiTenancyEnabled = $admin.multiTenancy
$: cloud = $admin.cloud $: cloud = $admin.cloud
$: disableAccountPortal = $admin.disableAccountPortal
async function setOrg() { async function setOrg() {
if (tenantId == null || tenantId === "") { if (tenantId == null || tenantId === "") {
@ -26,7 +27,7 @@
onMount(async () => { onMount(async () => {
await auth.checkQueryString() await auth.checkQueryString()
if (!multiTenancyEnabled || cloud) { if (!multiTenancyEnabled || (cloud && !disableAccountPortal)) {
$goto("../") $goto("../")
} else { } else {
admin.unload() admin.unload()

View File

@ -5,11 +5,9 @@
auth.checkQueryString() auth.checkQueryString()
$: { $: {
if (!$auth.user) { if ($auth.user?.builder?.global) {
$redirect(`./auth`)
} else if ($auth.user.builder?.global) {
$redirect(`./portal`) $redirect(`./portal`)
} else { } else if ($auth.user) {
$redirect(`./apps`) $redirect(`./apps`)
} }
} }

View File

@ -35,6 +35,7 @@
let unpublishModal let unpublishModal
let creatingApp = false let creatingApp = false
let loaded = false let loaded = false
let cloud = $admin.cloud
$: enrichedApps = enrichApps($apps, $auth.user, sortBy) $: enrichedApps = enrichApps($apps, $auth.user, sortBy)
@ -70,6 +71,15 @@
creatingApp = true creatingApp = true
} }
const initiateAppsExport = () => {
try {
download(`/api/cloud/export`)
notifications.success("Apps exported successfully")
} catch (err) {
notifications.error(`Error exporting apps: ${err}`)
}
}
const initiateAppImport = () => { const initiateAppImport = () => {
template = { fromFile: true } template = { fromFile: true }
creationModal.show() creationModal.show()
@ -190,6 +200,9 @@
<div class="title"> <div class="title">
<Heading>Apps</Heading> <Heading>Apps</Heading>
<ButtonGroup> <ButtonGroup>
{#if cloud}
<Button secondary on:click={initiateAppsExport}>Export apps</Button>
{/if}
<Button secondary on:click={initiateAppImport}>Import app</Button> <Button secondary on:click={initiateAppImport}>Import app</Button>
<Button cta on:click={initiateAppCreation}>Create app</Button> <Button cta on:click={initiateAppCreation}>Create app</Button>
</ButtonGroup> </ButtonGroup>

View File

@ -7,6 +7,7 @@ export function createAdminStore() {
loaded: false, loaded: false,
multiTenancy: false, multiTenancy: false,
cloud: false, cloud: false,
disableAccountPortal: false,
accountPortalUrl: "", accountPortalUrl: "",
onboardingProgress: 0, onboardingProgress: 0,
checklist: { checklist: {
@ -47,12 +48,14 @@ export function createAdminStore() {
async function getEnvironment() { async function getEnvironment() {
let multiTenancyEnabled = false let multiTenancyEnabled = false
let cloud = false let cloud = false
let disableAccountPortal = false
let accountPortalUrl = "" let accountPortalUrl = ""
try { try {
const response = await api.get(`/api/system/environment`) const response = await api.get(`/api/system/environment`)
const json = await response.json() const json = await response.json()
multiTenancyEnabled = json.multiTenancy multiTenancyEnabled = json.multiTenancy
cloud = json.cloud cloud = json.cloud
disableAccountPortal = json.disableAccountPortal
accountPortalUrl = json.accountPortalUrl accountPortalUrl = json.accountPortalUrl
} catch (err) { } catch (err) {
// just let it stay disabled // just let it stay disabled
@ -60,6 +63,7 @@ export function createAdminStore() {
admin.update(store => { admin.update(store => {
store.multiTenancy = multiTenancyEnabled store.multiTenancy = multiTenancyEnabled
store.cloud = cloud store.cloud = cloud
store.disableAccountPortal = disableAccountPortal
store.accountPortalUrl = accountPortalUrl store.accountPortalUrl = accountPortalUrl
return store return store
}) })

File diff suppressed because it is too large Load Diff

View File

@ -96,6 +96,7 @@
"koa-session": "5.12.0", "koa-session": "5.12.0",
"koa-static": "5.0.0", "koa-static": "5.0.0",
"lodash": "4.17.21", "lodash": "4.17.21",
"memorystream": "^0.3.1",
"mongodb": "3.6.3", "mongodb": "3.6.3",
"mssql": "6.2.3", "mssql": "6.2.3",
"mysql": "2.18.1", "mysql": "2.18.1",

View File

@ -37,7 +37,7 @@ async function init() {
const envFileJson = { const envFileJson = {
PORT: 4001, PORT: 4001,
MINIO_URL: "http://localhost:10000/", MINIO_URL: "http://localhost:10000/",
COUCH_DB_URL: "http://@localhost:10000/db/", COUCH_DB_URL: "http://budibase:budibase@localhost:10000/db/",
REDIS_URL: "localhost:6379", REDIS_URL: "localhost:6379",
WORKER_URL: "http://localhost:4002", WORKER_URL: "http://localhost:4002",
INTERNAL_API_KEY: "budibase", INTERNAL_API_KEY: "budibase",
@ -48,6 +48,7 @@ async function init() {
COUCH_DB_PASSWORD: "budibase", COUCH_DB_PASSWORD: "budibase",
COUCH_DB_USER: "budibase", COUCH_DB_USER: "budibase",
SELF_HOSTED: 1, SELF_HOSTED: 1,
DISABLE_ACCOUNT_PORTAL: "",
MULTI_TENANCY: "", MULTI_TENANCY: "",
} }
let envFile = "" let envFile = ""

View File

@ -31,7 +31,7 @@ const {
getDeployedApps, getDeployedApps,
removeAppFromUserRoles, removeAppFromUserRoles,
} = require("../../utilities/workerRequests") } = require("../../utilities/workerRequests")
const { clientLibraryPath } = require("../../utilities") const { clientLibraryPath, stringToReadStream } = require("../../utilities")
const { getAllLocks } = require("../../utilities/redis") const { getAllLocks } = require("../../utilities/redis")
const { const {
updateClientLibrary, updateClientLibrary,
@ -114,8 +114,13 @@ async function createInstance(template) {
// replicate the template data to the instance DB // replicate the template data to the instance DB
// this is currently very hard to test, downloading and importing template files // this is currently very hard to test, downloading and importing template files
if (template && template.templateString) {
const { ok } = await db.load(stringToReadStream(template.templateString))
if (!ok) {
throw "Error loading database dump from memory."
}
} else if (template && template.useTemplate === "true") {
/* istanbul ignore next */ /* istanbul ignore next */
if (template && template.useTemplate === "true") {
const { ok } = await db.load(await getTemplateStream(template)) const { ok } = await db.load(await getTemplateStream(template))
if (!ok) { if (!ok) {
throw "Error loading database dump from template." throw "Error loading database dump from template."
@ -191,10 +196,11 @@ exports.fetchAppPackage = async function (ctx) {
} }
exports.create = async function (ctx) { exports.create = async function (ctx) {
const { useTemplate, templateKey } = ctx.request.body const { useTemplate, templateKey, templateString } = ctx.request.body
const instanceConfig = { const instanceConfig = {
useTemplate, useTemplate,
key: templateKey, key: templateKey,
templateString,
} }
if (ctx.request.files && ctx.request.files.templateFile) { if (ctx.request.files && ctx.request.files.templateFile) {
instanceConfig.file = ctx.request.files.templateFile instanceConfig.file = ctx.request.files.templateFile

View File

@ -0,0 +1,92 @@
const env = require("../../environment")
const { getAllApps } = require("@budibase/auth/db")
const CouchDB = require("../../db")
const {
exportDB,
sendTempFile,
readFileSync,
} = require("../../utilities/fileSystem")
const { stringToReadStream } = require("../../utilities")
const { getGlobalDBName, getGlobalDB } = require("@budibase/auth/tenancy")
const { create } = require("./application")
const { getDocParams, DocumentTypes, isDevAppID } = require("../../db/utils")
async function createApp(appName, appImport) {
const ctx = {
request: {
body: {
templateString: appImport,
name: appName,
},
},
}
return create(ctx)
}
exports.exportApps = async ctx => {
if (env.SELF_HOSTED || !env.MULTI_TENANCY) {
ctx.throw(400, "Exporting only allowed in multi-tenant cloud environments.")
}
const apps = await getAllApps(CouchDB, { all: true })
const globalDBString = await exportDB(getGlobalDBName())
let allDBs = {
global: globalDBString,
}
for (let app of apps) {
// only export the dev apps as they will be the latest, the user can republish the apps
// in their self hosted environment
if (isDevAppID(app._id)) {
allDBs[app.name] = await exportDB(app._id)
}
}
const filename = `cloud-export-${new Date().getTime()}.txt`
ctx.attachment(filename)
ctx.body = sendTempFile(JSON.stringify(allDBs))
}
async function getAllDocType(db, docType) {
const response = await db.allDocs(
getDocParams(docType, null, {
include_docs: true,
})
)
return response.rows.map(row => row.doc)
}
exports.importApps = async ctx => {
if (!env.SELF_HOSTED || env.MULTI_TENANCY) {
ctx.throw(400, "Importing only allowed in self hosted environments.")
}
const apps = await getAllApps(CouchDB, { all: true })
if (
apps.length !== 0 ||
!ctx.request.files ||
!ctx.request.files.importFile
) {
ctx.throw(
400,
"Import file is required and environment must be fresh to import apps."
)
}
const importFile = ctx.request.files.importFile
const importString = readFileSync(importFile.path)
const dbs = JSON.parse(importString)
const globalDbImport = dbs.global
// remove from the list of apps
delete dbs.global
const globalDb = getGlobalDB()
// load the global db first
await globalDb.load(stringToReadStream(globalDbImport))
for (let [appName, appImport] of Object.entries(dbs)) {
await createApp(appName, appImport)
}
// once apps are created clean up the global db
let users = await getAllDocType(globalDb, DocumentTypes.USER)
for (let user of users) {
delete user.tenantId
}
await globalDb.bulkDocs(users)
ctx.body = {
message: "Apps successfully imported.",
}
}

View File

@ -5,7 +5,6 @@ const {
generateRowID, generateRowID,
DocumentTypes, DocumentTypes,
InternalTables, InternalTables,
generateMemoryViewID,
} = require("../../../db/utils") } = require("../../../db/utils")
const userController = require("../user") const userController = require("../user")
const { const {
@ -20,7 +19,12 @@ const { fullSearch, paginatedSearch } = require("./internalSearch")
const { getGlobalUsersFromMetadata } = require("../../../utilities/global") const { getGlobalUsersFromMetadata } = require("../../../utilities/global")
const inMemoryViews = require("../../../db/inMemoryView") const inMemoryViews = require("../../../db/inMemoryView")
const env = require("../../../environment") const env = require("../../../environment")
const { migrateToInMemoryView } = require("../view/utils") const {
migrateToInMemoryView,
migrateToDesignView,
getFromDesignDoc,
getFromMemoryDoc,
} = require("../view/utils")
const CALCULATION_TYPES = { const CALCULATION_TYPES = {
SUM: "sum", SUM: "sum",
@ -74,33 +78,24 @@ async function getRawTableData(ctx, db, tableId) {
} }
async function getView(db, viewName) { async function getView(db, viewName) {
let viewInfo let mainGetter = env.SELF_HOSTED ? getFromDesignDoc : getFromMemoryDoc
async function getFromDesignDoc() { let secondaryGetter = env.SELF_HOSTED ? getFromMemoryDoc : getFromDesignDoc
const designDoc = await db.get("_design/database") let migration = env.SELF_HOSTED ? migrateToDesignView : migrateToInMemoryView
viewInfo = designDoc.views[viewName] let viewInfo,
return viewInfo migrate = false
}
let migrate = false
if (env.SELF_HOSTED) {
viewInfo = await getFromDesignDoc()
} else {
try { try {
viewInfo = await db.get(generateMemoryViewID(viewName)) viewInfo = await mainGetter(db, viewName)
if (viewInfo) {
viewInfo = viewInfo.view
}
} catch (err) { } catch (err) {
// check if it can be retrieved from design doc (needs migrated) // check if it can be retrieved from design doc (needs migrated)
if (err.status !== 404) { if (err.status !== 404) {
viewInfo = null viewInfo = null
} else { } else {
viewInfo = await getFromDesignDoc() viewInfo = await secondaryGetter(db, viewName)
migrate = !!viewInfo migrate = !!viewInfo
} }
} }
}
if (migrate) { if (migrate) {
await migrateToInMemoryView(db, viewName) await migration(db, viewName)
} }
if (!viewInfo) { if (!viewInfo) {
throw "View does not exist." throw "View does not exist."

View File

@ -107,3 +107,30 @@ exports.migrateToInMemoryView = async (db, viewName) => {
await db.put(designDoc) await db.put(designDoc)
await exports.saveView(db, null, viewName, view) await exports.saveView(db, null, viewName, view)
} }
exports.migrateToDesignView = async (db, viewName) => {
let view = await db.get(generateMemoryViewID(viewName))
const designDoc = await db.get("_design/database")
designDoc.views[viewName] = view.view
await db.put(designDoc)
await db.remove(view._id, view._rev)
}
exports.getFromDesignDoc = async (db, viewName) => {
const designDoc = await db.get("_design/database")
let view = designDoc.views[viewName]
if (view == null) {
throw { status: 404, message: "Unable to get view" }
}
return view
}
exports.getFromMemoryDoc = async (db, viewName) => {
let view = await db.get(generateMemoryViewID(viewName))
if (view) {
view = view.view
} else {
throw { status: 404, message: "Unable to get view" }
}
return view
}

View File

@ -0,0 +1,13 @@
const Router = require("@koa/router")
const controller = require("../controllers/cloud")
const authorized = require("../../middleware/authorized")
const { BUILDER } = require("@budibase/auth/permissions")
const router = Router()
router
.get("/api/cloud/export", authorized(BUILDER), controller.exportApps)
// has to be public, only run if apps don't exist
.post("/api/cloud/import", controller.importApps)
module.exports = router

View File

@ -24,6 +24,7 @@ const hostingRoutes = require("./hosting")
const backupRoutes = require("./backup") const backupRoutes = require("./backup")
const metadataRoutes = require("./metadata") const metadataRoutes = require("./metadata")
const devRoutes = require("./dev") const devRoutes = require("./dev")
const cloudRoutes = require("./cloud")
exports.mainRoutes = [ exports.mainRoutes = [
authRoutes, authRoutes,
@ -49,6 +50,7 @@ exports.mainRoutes = [
backupRoutes, backupRoutes,
metadataRoutes, metadataRoutes,
devRoutes, devRoutes,
cloudRoutes,
// these need to be handled last as they still use /api/:tableId // these need to be handled last as they still use /api/:tableId
// this could be breaking as koa may recognise other routes as this // this could be breaking as koa may recognise other routes as this
tableRoutes, tableRoutes,

View File

@ -317,7 +317,7 @@ describe("/rows", () => {
await request await request
.get(`/api/views/derp`) .get(`/api/views/derp`)
.set(config.defaultHeaders()) .set(config.defaultHeaders())
.expect(400) .expect(404)
}) })
it("should be able to run on a view", async () => { it("should be able to run on a view", async () => {

View File

@ -110,6 +110,8 @@ function getDocParams(docType, docId = null, otherProps = {}) {
} }
} }
exports.getDocParams = getDocParams
/** /**
* Gets parameters for retrieving tables, this is a utility function for the getDocParams function. * Gets parameters for retrieving tables, this is a utility function for the getDocParams function.
*/ */

View File

@ -44,6 +44,7 @@ module.exports = {
NODE_ENV: process.env.NODE_ENV, NODE_ENV: process.env.NODE_ENV,
JEST_WORKER_ID: process.env.JEST_WORKER_ID, JEST_WORKER_ID: process.env.JEST_WORKER_ID,
BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT, BUDIBASE_ENVIRONMENT: process.env.BUDIBASE_ENVIRONMENT,
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
// minor // minor
SALT_ROUNDS: process.env.SALT_ROUNDS, SALT_ROUNDS: process.env.SALT_ROUNDS,
LOGGER: process.env.LOGGER, LOGGER: process.env.LOGGER,

View File

@ -19,6 +19,7 @@ const {
USER_METDATA_PREFIX, USER_METDATA_PREFIX,
LINK_USER_METADATA_PREFIX, LINK_USER_METADATA_PREFIX,
} = require("../../db/utils") } = require("../../db/utils")
const MemoryStream = require("memorystream")
const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..") const TOP_LEVEL_PATH = join(__dirname, "..", "..", "..")
const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules") const NODE_MODULES_PATH = join(TOP_LEVEL_PATH, "node_modules")
@ -111,29 +112,88 @@ exports.apiFileReturn = contents => {
* to the temporary backup file (to return via API if required). * to the temporary backup file (to return via API if required).
*/ */
exports.performBackup = async (appId, backupName) => { exports.performBackup = async (appId, backupName) => {
const path = join(budibaseTempDir(), backupName) return exports.exportDB(appId, {
const writeStream = fs.createWriteStream(path) exportName: backupName,
// perform couch dump
const instanceDb = new CouchDB(appId)
await instanceDb.dump(writeStream, {
// filter out anything that has a user metadata structure in its ID
filter: doc => filter: doc =>
!( !(
doc._id.includes(USER_METDATA_PREFIX) || doc._id.includes(USER_METDATA_PREFIX) ||
doc.includes(LINK_USER_METADATA_PREFIX) doc.includes(LINK_USER_METADATA_PREFIX)
), ),
}) })
}
/**
* exports a DB to either file or a variable (memory).
* @param {string} dbName the DB which is to be exported.
* @param {string} exportName optional - the file name to export to, if not in memory.
* @param {function} filter optional - a filter function to clear out any un-wanted docs.
* @return Either the file stream or the variable (if no export name provided).
*/
exports.exportDB = async (
dbName,
{ exportName, filter } = { exportName: undefined, filter: undefined }
) => {
let stream,
appString = "",
path = null
if (exportName) {
path = join(budibaseTempDir(), exportName)
stream = fs.createWriteStream(path)
} else {
stream = new MemoryStream()
stream.on("data", chunk => {
appString += chunk.toString()
})
}
// perform couch dump
const instanceDb = new CouchDB(dbName)
await instanceDb.dump(stream, {
filter,
})
// just in memory, return the final string
if (!exportName) {
return appString
}
// write the file to the object store // write the file to the object store
if (env.SELF_HOSTED) { if (env.SELF_HOSTED) {
await streamUpload( await streamUpload(
ObjectStoreBuckets.BACKUPS, ObjectStoreBuckets.BACKUPS,
join(appId, backupName), join(dbName, exportName),
fs.createReadStream(path) fs.createReadStream(path)
) )
} }
return fs.createReadStream(path) return fs.createReadStream(path)
} }
/**
* Writes the provided contents to a temporary file, which can be used briefly.
* @param {string} fileContents contents which will be written to a temp file.
* @return {string} the path to the temp file.
*/
exports.storeTempFile = fileContents => {
const path = join(budibaseTempDir(), uuid())
fs.writeFileSync(path, fileContents)
return path
}
/**
* Utility function for getting a file read stream - a simple in memory buffered read
* stream doesn't work for pouchdb.
*/
exports.stringToFileStream = contents => {
const path = exports.storeTempFile(contents)
return fs.createReadStream(path)
}
/**
* Creates a temp file and returns it from the API.
* @param {string} fileContents the contents to be returned in file.
*/
exports.sendTempFile = fileContents => {
const path = exports.storeTempFile(fileContents)
return fs.createReadStream(path)
}
/** /**
* Uploads the latest client library to the object store. * Uploads the latest client library to the object store.
* @param {string} appId The ID of the app which is being created. * @param {string} appId The ID of the app which is being created.

View File

@ -3,6 +3,7 @@ const { OBJ_STORE_DIRECTORY } = require("../constants")
const { sanitizeKey } = require("@budibase/auth/src/objectStore") const { sanitizeKey } = require("@budibase/auth/src/objectStore")
const CouchDB = require("../db") const CouchDB = require("../db")
const { generateMetadataID } = require("../db/utils") const { generateMetadataID } = require("../db/utils")
const Readable = require("stream").Readable
const BB_CDN = "https://cdn.budi.live" const BB_CDN = "https://cdn.budi.live"
@ -124,3 +125,12 @@ exports.escapeDangerousCharacters = string => {
.replace(/[\r]/g, "\\r") .replace(/[\r]/g, "\\r")
.replace(/[\t]/g, "\\t") .replace(/[\t]/g, "\\t")
} }
exports.stringToReadStream = string => {
return new Readable({
read() {
this.push(string)
this.push(null)
},
})
}

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@ async function init() {
COUCH_DB_PASSWORD: "budibase", COUCH_DB_PASSWORD: "budibase",
// empty string is false // empty string is false
MULTI_TENANCY: "", MULTI_TENANCY: "",
DISABLE_ACCOUNT_PORTAL: "",
ACCOUNT_PORTAL_URL: "http://localhost:10001", ACCOUNT_PORTAL_URL: "http://localhost:10001",
} }
let envFile = "" let envFile = ""

View File

@ -66,7 +66,7 @@ async function saveUser(
} }
// check root account users in account portal // check root account users in account portal
if (!env.SELF_HOSTED) { if (!env.SELF_HOSTED && !env.DISABLE_ACCOUNT_PORTAL) {
const account = await accounts.getAccount(email) const account = await accounts.getAccount(email)
if (account && account.verified && account.tenantId !== tenantId) { if (account && account.verified && account.tenantId !== tenantId) {
throw `Email address ${email} already in use.` throw `Email address ${email} already in use.`
@ -133,7 +133,7 @@ exports.save = async ctx => {
} }
const parseBooleanParam = param => { const parseBooleanParam = param => {
if (param && param == "false") { if (param && param === "false") {
return false return false
} else { } else {
return true return true
@ -161,6 +161,17 @@ exports.adminUser = async ctx => {
// write usage quotas for cloud // write usage quotas for cloud
if (!env.SELF_HOSTED) { if (!env.SELF_HOSTED) {
// could be a scenario where it exists, make sure its clean
try {
const usageQuota = await db.get(
StaticDatabases.PLATFORM_INFO.docs.usageQuota
)
if (usageQuota) {
await db.remove(usageQuota._id, usageQuota._rev)
}
} catch (err) {
// don't worry about errors
}
await db.post(generateNewUsageQuotaDoc()) await db.post(generateNewUsageQuotaDoc())
} }

View File

@ -5,5 +5,6 @@ exports.fetch = async ctx => {
multiTenancy: !!env.MULTI_TENANCY, multiTenancy: !!env.MULTI_TENANCY,
cloud: !env.SELF_HOSTED, cloud: !env.SELF_HOSTED,
accountPortalUrl: env.ACCOUNT_PORTAL_URL, accountPortalUrl: env.ACCOUNT_PORTAL_URL,
disableAccountPortal: env.DISABLE_ACCOUNT_PORTAL,
} }
} }

View File

@ -32,6 +32,7 @@ module.exports = {
REDIS_PASSWORD: process.env.REDIS_PASSWORD, REDIS_PASSWORD: process.env.REDIS_PASSWORD,
INTERNAL_API_KEY: process.env.INTERNAL_API_KEY, INTERNAL_API_KEY: process.env.INTERNAL_API_KEY,
MULTI_TENANCY: process.env.MULTI_TENANCY, MULTI_TENANCY: process.env.MULTI_TENANCY,
DISABLE_ACCOUNT_PORTAL: process.env.DISABLE_ACCOUNT_PORTAL,
ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL, ACCOUNT_PORTAL_URL: process.env.ACCOUNT_PORTAL_URL,
SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED, SMTP_FALLBACK_ENABLED: process.env.SMTP_FALLBACK_ENABLED,
SMTP_USER: process.env.SMTP_USER, SMTP_USER: process.env.SMTP_USER,