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:
commit
81c5ed5554
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"]
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue