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 81c5ed5554
7 changed files with 130 additions and 50 deletions

View File

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

View File

@ -10,6 +10,7 @@ const fs = require("fs")
const env = require("../environment")
const { budibaseTempDir, ObjectStoreBuckets } = require("./utils")
const { v4 } = require("uuid")
const { APP_PREFIX, APP_DEV_PREFIX } = require("../db/utils")
const streamPipeline = promisify(stream.pipeline)
// use this as a temporary store of buckets that are being created
@ -28,6 +29,16 @@ const STRING_CONTENT_TYPES = [
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) {
return {
Version: "2012-10-17",
@ -61,7 +72,7 @@ exports.ObjectStore = bucket => {
s3ForcePathStyle: true,
signatureVersion: "v4",
params: {
Bucket: bucket,
Bucket: sanitizeBucket(bucket),
},
}
if (env.MINIO_URL) {
@ -75,6 +86,7 @@ exports.ObjectStore = bucket => {
* if it does not exist then it will create it.
*/
exports.makeSureBucketExists = async (client, bucketName) => {
bucketName = sanitizeBucket(bucketName)
try {
await client
.headBucket({
@ -114,16 +126,16 @@ exports.makeSureBucketExists = async (client, bucketName) => {
* Uploads the contents of a file given the required parameters, useful when
* 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 fileBytes = fs.readFileSync(path)
const objectStore = exports.ObjectStore(bucket)
await exports.makeSureBucketExists(objectStore, bucket)
const objectStore = exports.ObjectStore(bucketName)
await exports.makeSureBucketExists(objectStore, bucketName)
const config = {
// windows file paths need to be converted to forward slashes for s3
Key: sanitize(filename).replace(/\\/g, "/"),
Key: sanitizeKey(filename),
Body: fileBytes,
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
* through to the object store.
*/
exports.streamUpload = async (bucket, filename, stream) => {
const objectStore = exports.ObjectStore(bucket)
await exports.makeSureBucketExists(objectStore, bucket)
exports.streamUpload = async (bucketName, filename, stream) => {
const objectStore = exports.ObjectStore(bucketName)
await exports.makeSureBucketExists(objectStore, bucketName)
const params = {
Bucket: bucket,
Key: sanitize(filename).replace(/\\/g, "/"),
Bucket: sanitizeBucket(bucketName),
Key: sanitizeKey(filename),
Body: stream,
}
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
* will be converted, otherwise it will be returned as a buffer stream.
*/
exports.retrieve = async (bucket, filepath) => {
const objectStore = exports.ObjectStore(bucket)
exports.retrieve = async (bucketName, filepath) => {
const objectStore = exports.ObjectStore(bucketName)
const params = {
Bucket: bucket,
Key: sanitize(filepath).replace(/\\/g, "/"),
Bucket: sanitizeBucket(bucketName),
Key: sanitizeKey(filepath),
}
const response = await objectStore.getObject(params).promise()
// currently these are all strings
@ -171,17 +183,21 @@ exports.retrieve = async (bucket, filepath) => {
/**
* Same as retrieval function but puts to a temporary file.
*/
exports.retrieveToTmp = async (bucket, filepath) => {
const data = await exports.retrieve(bucket, filepath)
exports.retrieveToTmp = async (bucketName, filepath) => {
bucketName = sanitizeBucket(bucketName)
filepath = sanitizeKey(filepath)
const data = await exports.retrieve(bucketName, filepath)
const outputPath = join(budibaseTempDir(), v4())
fs.writeFileSync(outputPath, data)
return outputPath
}
exports.deleteFolder = async (bucket, folder) => {
const client = exports.ObjectStore(bucket)
exports.deleteFolder = async (bucketName, folder) => {
bucketName = sanitizeBucket(bucketName)
folder = sanitizeKey(folder)
const client = exports.ObjectStore(bucketName)
const listParams = {
Bucket: bucket,
Bucket: bucketName,
Prefix: folder,
}
@ -190,7 +206,7 @@ exports.deleteFolder = async (bucket, folder) => {
return
}
const deleteParams = {
Bucket: bucket,
Bucket: bucketName,
Delete: {
Objects: [],
},
@ -203,28 +219,31 @@ exports.deleteFolder = async (bucket, folder) => {
response = await client.deleteObjects(deleteParams).promise()
// can only empty 1000 items at once
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 = []
const files = fs.readdirSync(localPath, { withFileTypes: true })
for (let file of files) {
const path = join(bucketPath, file.name)
const path = sanitizeKey(join(bucketPath, file.name))
const local = join(localPath, file.name)
if (file.isDirectory()) {
uploads.push(exports.uploadDirectory(bucket, local, path))
uploads.push(exports.uploadDirectory(bucketName, local, path))
} else {
uploads.push(
exports.streamUpload(bucket, path, fs.createReadStream(local))
exports.streamUpload(bucketName, path, fs.createReadStream(local))
)
}
}
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)
if (!response.ok) {
throw new Error(`unexpected response ${response.statusText}`)
@ -233,7 +252,7 @@ exports.downloadTarball = async (url, bucket, path) => {
const tmpPath = join(budibaseTempDir(), path)
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath))
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 tmpPath

View File

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

View File

@ -8,6 +8,7 @@
MenuItem,
Link,
} from "@budibase/bbui"
import { AppStatus } from "constants"
import { url } from "@roxi/routify"
export let app
@ -15,11 +16,16 @@
export let exportApp
export let deleteApp
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>
<div class="title" class:last>
<div class="preview" use:gradient={{ seed: app.name }} />
<Link href={$url(`../../app/${app._id}`)}>
<Link {href} {target}>
<Heading size="XS">
{app.name}
</Heading>
@ -29,15 +35,12 @@
Edited {Math.round(Math.random() * 10 + 1)} months ago
</div>
<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" />
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}
</div>
<div class:last>

View File

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

View File

@ -93,12 +93,13 @@
onMount(async () => {
checkKeys()
await apps.load()
await apps.load(appStatus)
loaded = true
})
</script>
<Page wide>
<<<<<<< HEAD
{#if $apps.length}
<Layout noPadding>
<div class="title">
@ -132,7 +133,42 @@
icon="ViewRow"
/>
</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>
<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
class:appGrid={layout === "grid"}
class:appTable={layout === "table"}
@ -140,6 +176,7 @@
{#each $apps as app, idx (app._id)}
<svelte:component
this={layout === "grid" ? AppCard : AppRow}
{appStatus}
{app}
{openApp}
{exportApp}
@ -148,8 +185,8 @@
/>
{/each}
</div>
</Layout>
{/if}
{/if}
</Layout>
{#if !$apps.length && !creatingApp && loaded}
<div class="empty-wrapper">
<Modal inline>

View File

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