Altering object store so that all writes/reads to the object store have the dev app prefix replaced with standard app.

This commit is contained in:
mike12345567 2021-05-13 13:29:53 +01:00
commit f4e3e1d196
7 changed files with 130 additions and 50 deletions

View File

@ -1,6 +1,9 @@
const { newid } = require("../hashing") const { newid } = require("../hashing")
const Replication = require("./Replication") const Replication = require("./Replication")
const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
exports.ViewNames = { exports.ViewNames = {
USER_BY_EMAIL: "by_email", USER_BY_EMAIL: "by_email",
} }
@ -13,17 +16,16 @@ exports.StaticDatabases = {
const DocumentTypes = { const DocumentTypes = {
USER: "us", USER: "us",
APP: "app",
GROUP: "group", GROUP: "group",
CONFIG: "config", CONFIG: "config",
TEMPLATE: "template", TEMPLATE: "template",
APP: "app",
APP_DEV: "app_dev",
} }
exports.DocumentTypes = DocumentTypes exports.DocumentTypes = DocumentTypes
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR
const UNICODE_MAX = "\ufff0" exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR
const SEPARATOR = "_"
exports.SEPARATOR = SEPARATOR exports.SEPARATOR = SEPARATOR
/** /**

View File

@ -10,6 +10,7 @@ const fs = require("fs")
const env = require("../environment") const env = require("../environment")
const { budibaseTempDir, ObjectStoreBuckets } = require("./utils") const { budibaseTempDir, ObjectStoreBuckets } = require("./utils")
const { v4 } = require("uuid") const { v4 } = require("uuid")
const { APP_PREFIX, APP_DEV_PREFIX } = require("../db/utils")
const streamPipeline = promisify(stream.pipeline) const streamPipeline = promisify(stream.pipeline)
// use this as a temporary store of buckets that are being created // use this as a temporary store of buckets that are being created
@ -28,6 +29,16 @@ const STRING_CONTENT_TYPES = [
CONTENT_TYPE_MAP.js, CONTENT_TYPE_MAP.js,
] ]
// does normal sanitization and then swaps dev apps to apps
function sanitizeKey(input) {
return sanitize(sanitizeBucket(input)).replace(/\\/g, "/")
}
// simply handles the dev app to app conversion
function sanitizeBucket(input) {
return input.replace(new RegExp(APP_DEV_PREFIX, "g"), APP_PREFIX)
}
function publicPolicy(bucketName) { function publicPolicy(bucketName) {
return { return {
Version: "2012-10-17", Version: "2012-10-17",
@ -61,7 +72,7 @@ exports.ObjectStore = bucket => {
s3ForcePathStyle: true, s3ForcePathStyle: true,
signatureVersion: "v4", signatureVersion: "v4",
params: { params: {
Bucket: bucket, Bucket: sanitizeBucket(bucket),
}, },
} }
if (env.MINIO_URL) { if (env.MINIO_URL) {
@ -75,6 +86,7 @@ exports.ObjectStore = bucket => {
* if it does not exist then it will create it. * if it does not exist then it will create it.
*/ */
exports.makeSureBucketExists = async (client, bucketName) => { exports.makeSureBucketExists = async (client, bucketName) => {
bucketName = sanitizeBucket(bucketName)
try { try {
await client await client
.headBucket({ .headBucket({
@ -114,16 +126,16 @@ exports.makeSureBucketExists = async (client, bucketName) => {
* Uploads the contents of a file given the required parameters, useful when * Uploads the contents of a file given the required parameters, useful when
* temp files in use (for example file uploaded as an attachment). * temp files in use (for example file uploaded as an attachment).
*/ */
exports.upload = async ({ bucket, filename, path, type, metadata }) => { exports.upload = async ({ bucket: bucketName, filename, path, type, metadata }) => {
const extension = [...filename.split(".")].pop() const extension = [...filename.split(".")].pop()
const fileBytes = fs.readFileSync(path) const fileBytes = fs.readFileSync(path)
const objectStore = exports.ObjectStore(bucket) const objectStore = exports.ObjectStore(bucketName)
await exports.makeSureBucketExists(objectStore, bucket) await exports.makeSureBucketExists(objectStore, bucketName)
const config = { const config = {
// windows file paths need to be converted to forward slashes for s3 // windows file paths need to be converted to forward slashes for s3
Key: sanitize(filename).replace(/\\/g, "/"), Key: sanitizeKey(filename),
Body: fileBytes, Body: fileBytes,
ContentType: type || CONTENT_TYPE_MAP[extension.toLowerCase()], ContentType: type || CONTENT_TYPE_MAP[extension.toLowerCase()],
} }
@ -137,13 +149,13 @@ exports.upload = async ({ bucket, filename, path, type, metadata }) => {
* Similar to the upload function but can be used to send a file stream * Similar to the upload function but can be used to send a file stream
* through to the object store. * through to the object store.
*/ */
exports.streamUpload = async (bucket, filename, stream) => { exports.streamUpload = async (bucketName, filename, stream) => {
const objectStore = exports.ObjectStore(bucket) const objectStore = exports.ObjectStore(bucketName)
await exports.makeSureBucketExists(objectStore, bucket) await exports.makeSureBucketExists(objectStore, bucketName)
const params = { const params = {
Bucket: bucket, Bucket: sanitizeBucket(bucketName),
Key: sanitize(filename).replace(/\\/g, "/"), Key: sanitizeKey(filename),
Body: stream, Body: stream,
} }
return objectStore.upload(params).promise() return objectStore.upload(params).promise()
@ -153,11 +165,11 @@ exports.streamUpload = async (bucket, filename, stream) => {
* retrieves the contents of a file from the object store, if it is a known content type it * retrieves the contents of a file from the object store, if it is a known content type it
* will be converted, otherwise it will be returned as a buffer stream. * will be converted, otherwise it will be returned as a buffer stream.
*/ */
exports.retrieve = async (bucket, filepath) => { exports.retrieve = async (bucketName, filepath) => {
const objectStore = exports.ObjectStore(bucket) const objectStore = exports.ObjectStore(bucketName)
const params = { const params = {
Bucket: bucket, Bucket: sanitizeBucket(bucketName),
Key: sanitize(filepath).replace(/\\/g, "/"), Key: sanitizeKey(filepath),
} }
const response = await objectStore.getObject(params).promise() const response = await objectStore.getObject(params).promise()
// currently these are all strings // currently these are all strings
@ -171,17 +183,21 @@ exports.retrieve = async (bucket, filepath) => {
/** /**
* Same as retrieval function but puts to a temporary file. * Same as retrieval function but puts to a temporary file.
*/ */
exports.retrieveToTmp = async (bucket, filepath) => { exports.retrieveToTmp = async (bucketName, filepath) => {
const data = await exports.retrieve(bucket, filepath) bucketName = sanitizeBucket(bucketName)
filepath = sanitizeKey(filepath)
const data = await exports.retrieve(bucketName, filepath)
const outputPath = join(budibaseTempDir(), v4()) const outputPath = join(budibaseTempDir(), v4())
fs.writeFileSync(outputPath, data) fs.writeFileSync(outputPath, data)
return outputPath return outputPath
} }
exports.deleteFolder = async (bucket, folder) => { exports.deleteFolder = async (bucketName, folder) => {
const client = exports.ObjectStore(bucket) bucketName = sanitizeBucket(bucketName)
folder = sanitizeKey(folder)
const client = exports.ObjectStore(bucketName)
const listParams = { const listParams = {
Bucket: bucket, Bucket: bucketName,
Prefix: folder, Prefix: folder,
} }
@ -190,7 +206,7 @@ exports.deleteFolder = async (bucket, folder) => {
return return
} }
const deleteParams = { const deleteParams = {
Bucket: bucket, Bucket: bucketName,
Delete: { Delete: {
Objects: [], Objects: [],
}, },
@ -203,28 +219,31 @@ exports.deleteFolder = async (bucket, folder) => {
response = await client.deleteObjects(deleteParams).promise() response = await client.deleteObjects(deleteParams).promise()
// can only empty 1000 items at once // can only empty 1000 items at once
if (response.Deleted.length === 1000) { if (response.Deleted.length === 1000) {
return exports.deleteFolder(bucket, folder) return exports.deleteFolder(bucketName, folder)
} }
} }
exports.uploadDirectory = async (bucket, localPath, bucketPath) => { exports.uploadDirectory = async (bucketName, localPath, bucketPath) => {
bucketName = sanitizeBucket(bucketName)
let uploads = [] let uploads = []
const files = fs.readdirSync(localPath, { withFileTypes: true }) const files = fs.readdirSync(localPath, { withFileTypes: true })
for (let file of files) { for (let file of files) {
const path = join(bucketPath, file.name) const path = sanitizeKey(join(bucketPath, file.name))
const local = join(localPath, file.name) const local = join(localPath, file.name)
if (file.isDirectory()) { if (file.isDirectory()) {
uploads.push(exports.uploadDirectory(bucket, local, path)) uploads.push(exports.uploadDirectory(bucketName, local, path))
} else { } else {
uploads.push( uploads.push(
exports.streamUpload(bucket, path, fs.createReadStream(local)) exports.streamUpload(bucketName, path, fs.createReadStream(local))
) )
} }
} }
await Promise.all(uploads) await Promise.all(uploads)
} }
exports.downloadTarball = async (url, bucket, path) => { exports.downloadTarball = async (url, bucketName, path) => {
bucketName = sanitizeBucket(bucketName)
path = sanitizeKey(path)
const response = await fetch(url) const response = await fetch(url)
if (!response.ok) { if (!response.ok) {
throw new Error(`unexpected response ${response.statusText}`) throw new Error(`unexpected response ${response.statusText}`)
@ -233,7 +252,7 @@ exports.downloadTarball = async (url, bucket, path) => {
const tmpPath = join(budibaseTempDir(), path) const tmpPath = join(budibaseTempDir(), path)
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath))
if (!env.isTest()) { if (!env.isTest()) {
await exports.uploadDirectory(bucket, tmpPath, path) await exports.uploadDirectory(bucketName, tmpPath, path)
} }
// return the temporary path incase there is a use for it // return the temporary path incase there is a use for it
return tmpPath return tmpPath

View File

@ -9,20 +9,29 @@
Link, Link,
} from "@budibase/bbui" } from "@budibase/bbui"
import { gradient } from "actions" import { gradient } from "actions"
import { AppStatus } from "constants"
import { url } from "@roxi/routify" import { url } from "@roxi/routify"
export let app export let app
export let exportApp export let exportApp
export let deleteApp export let deleteApp
export let appStatus
let href =
appStatus === AppStatus.DEV ? $url(`../../app/${app._id}`) : `/${app._id}`
let target = appStatus === AppStatus.DEV ? "_self" : "_target"
</script> </script>
<div class="wrapper"> <div class="wrapper">
<Layout noPadding gap="XS" alignContent="start"> <Layout noPadding gap="XS" alignContent="start">
<div class="preview" use:gradient={{ seed: app.name }} /> <div class="preview" use:gradient={{ seed: app.name }} />
<div class="title"> <div class="title">
<Link href={$url(`../../app/${app._id}`)}> <Link {href} {target}>
<Heading size="XS"> <Heading size="XS">
<<<<<<< HEAD
{app._id} {app._id}
=======
>>>>>>> c3e1b1d30235b8945424cf59a41e112f92942dc6
{app.name} {app.name}
</Heading> </Heading>
</Link> </Link>
@ -34,13 +43,14 @@
<MenuItem on:click={() => deleteApp(app)} icon="Delete"> <MenuItem on:click={() => deleteApp(app)} icon="Delete">
Delete Delete
</MenuItem> </MenuItem>
<MenuItem on:click={() => deleteApp(app)} icon="Code">Develop</MenuItem>
</ActionMenu> </ActionMenu>
</div> </div>
<div class="status"> <div class="status">
<Body noPadding size="S"> <Body noPadding size="S">
Edited {Math.floor(1 + Math.random() * 10)} months ago Edited {Math.floor(1 + Math.random() * 10)} months ago
</Body> </Body>
{#if Math.random() > 0.5} {#if appStatus === AppStatus.DEV && app.lockedBy}
<Icon name="LockClosed" /> <Icon name="LockClosed" />
{/if} {/if}
</div> </div>

View File

@ -8,6 +8,7 @@
MenuItem, MenuItem,
Link, Link,
} from "@budibase/bbui" } from "@budibase/bbui"
import { AppStatus } from "constants"
import { url } from "@roxi/routify" import { url } from "@roxi/routify"
export let app export let app
@ -15,11 +16,16 @@
export let exportApp export let exportApp
export let deleteApp export let deleteApp
export let last export let last
export let appStatus
let href =
appStatus === AppStatus.DEV ? $url(`../../app/${app._id}`) : `/${app._id}`
let target = appStatus === AppStatus.DEV ? "_self" : "_target"
</script> </script>
<div class="title" class:last> <div class="title" class:last>
<div class="preview" use:gradient={{ seed: app.name }} /> <div class="preview" use:gradient={{ seed: app.name }} />
<Link href={$url(`../../app/${app._id}`)}> <Link {href} {target}>
<Heading size="XS"> <Heading size="XS">
{app.name} {app.name}
</Heading> </Heading>
@ -29,15 +35,12 @@
Edited {Math.round(Math.random() * 10 + 1)} months ago Edited {Math.round(Math.random() * 10 + 1)} months ago
</div> </div>
<div class:last> <div class:last>
{#if Math.random() < 0.33} {#if app.lockedBy}
<div class="status status--locked-you" />
Locked by {app.lockedBy.email}
{:else}
<div class="status status--open" /> <div class="status status--open" />
Open Open
{:else if Math.random() < 0.33}
<div class="status status--locked-other" />
Locked by Will Wheaton
{:else}
<div class="status status--locked-you" />
Locked by you
{/if} {/if}
</div> </div>
<div class:last> <div class:last>

View File

@ -9,6 +9,10 @@ export const FrontendTypes = {
NONE: "none", NONE: "none",
} }
export const AppStatus = {
DEV: "dev",
}
// fields on the user table that cannot be edited // fields on the user table that cannot be edited
export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId", "status"] export const UNEDITABLE_USER_FIELDS = ["email", "password", "roleId", "status"]

View File

@ -93,12 +93,13 @@
onMount(async () => { onMount(async () => {
checkKeys() checkKeys()
await apps.load() await apps.load(appStatus)
loaded = true loaded = true
}) })
</script> </script>
<Page wide> <Page wide>
<<<<<<< HEAD
{#if $apps.length} {#if $apps.length}
<Layout noPadding> <Layout noPadding>
<div class="title"> <div class="title">
@ -132,7 +133,42 @@
icon="ViewRow" icon="ViewRow"
/> />
</ActionGroup> </ActionGroup>
=======
<Layout noPadding>
<div class="title">
<Heading>Apps</Heading>
<ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import app</Button>
<Button cta on:click={initiateAppCreation}>Create new app</Button>
</ButtonGroup>
</div>
<div class="filter">
<div class="select">
<Select
bind:value={appStatus}
options={[
{ label: "Deployed", value: "deployed" },
{ label: "In Development", value: "dev" },
]}
/>
>>>>>>> c3e1b1d30235b8945424cf59a41e112f92942dc6
</div> </div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div>
{#if $apps.length}
<div <div
class:appGrid={layout === "grid"} class:appGrid={layout === "grid"}
class:appTable={layout === "table"} class:appTable={layout === "table"}
@ -140,6 +176,7 @@
{#each $apps as app, idx (app._id)} {#each $apps as app, idx (app._id)}
<svelte:component <svelte:component
this={layout === "grid" ? AppCard : AppRow} this={layout === "grid" ? AppCard : AppRow}
{appStatus}
{app} {app}
{openApp} {openApp}
{exportApp} {exportApp}
@ -148,8 +185,8 @@
/> />
{/each} {/each}
</div> </div>
</Layout> {/if}
{/if} </Layout>
{#if !$apps.length && !creatingApp && loaded} {#if !$apps.length && !creatingApp && loaded}
<div class="empty-wrapper"> <div class="empty-wrapper">
<Modal inline> <Modal inline>

View File

@ -1,7 +1,12 @@
const newid = require("./newid") const newid = require("./newid")
const {
DocumentTypes: CoreDocTypes,
APP_DEV_PREFIX,
APP_PREFIX,
SEPARATOR,
} = require("@budibase/auth").db
const UNICODE_MAX = "\ufff0" const UNICODE_MAX = "\ufff0"
const SEPARATOR = "_"
const StaticDatabases = { const StaticDatabases = {
BUILDER: { BUILDER: {
@ -16,13 +21,13 @@ const AppStatus = {
} }
const DocumentTypes = { const DocumentTypes = {
APP: CoreDocTypes.APP,
APP_DEV: CoreDocTypes.APP_DEV,
TABLE: "ta", TABLE: "ta",
ROW: "ro", ROW: "ro",
USER: "us", USER: "us",
AUTOMATION: "au", AUTOMATION: "au",
LINK: "li", LINK: "li",
APP: "app",
APP_DEV: "app_dev",
ROLE: "role", ROLE: "role",
WEBHOOK: "wh", WEBHOOK: "wh",
INSTANCE: "inst", INSTANCE: "inst",
@ -45,8 +50,8 @@ const SearchIndexes = {
ROWS: "rows", ROWS: "rows",
} }
exports.APP_PREFIX = DocumentTypes.APP + SEPARATOR exports.APP_PREFIX = APP_PREFIX
exports.APP_DEV_PREFIX = DocumentTypes.APP_DEV + SEPARATOR exports.APP_DEV_PREFIX = APP_DEV_PREFIX
exports.StaticDatabases = StaticDatabases exports.StaticDatabases = StaticDatabases
exports.ViewNames = ViewNames exports.ViewNames = ViewNames
exports.InternalTables = InternalTables exports.InternalTables = InternalTables