mirror of https://github.com/Budibase/budibase.git
committed by
GitHub
24 changed files with 560 additions and 693 deletions
@ -0,0 +1,33 @@ |
|||
<script> |
|||
import { Select, Label } from "@budibase/bbui" |
|||
import { currentAsset } from "builderStore" |
|||
import { findAllMatchingComponents } from "builderStore/componentUtils" |
|||
|
|||
export let parameters |
|||
|
|||
$: components = findAllMatchingComponents($currentAsset.props, component => |
|||
component._component.endsWith("s3upload") |
|||
) |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Label small>S3 Upload Component</Label> |
|||
<Select |
|||
bind:value={parameters.componentId} |
|||
options={components} |
|||
getOptionLabel={x => x._instanceName} |
|||
getOptionValue={x => x._id} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
display: grid; |
|||
column-gap: var(--spacing-l); |
|||
row-gap: var(--spacing-s); |
|||
grid-template-columns: 120px 1fr; |
|||
align-items: center; |
|||
max-width: 400px; |
|||
margin: 0 auto; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,15 @@ |
|||
<script> |
|||
import { Select } from "@budibase/bbui" |
|||
import { datasources } from "stores/backend" |
|||
|
|||
export let value = null |
|||
|
|||
$: dataSources = $datasources.list |
|||
.filter(ds => ds.source === "S3" && !ds.config?.endpoint) |
|||
.map(ds => ({ |
|||
label: ds.name, |
|||
value: ds._id, |
|||
})) |
|||
</script> |
|||
|
|||
<Select options={dataSources} {value} on:change /> |
|||
@ -0,0 +1,143 @@ |
|||
<script> |
|||
import Field from "./Field.svelte" |
|||
import { CoreDropzone, ProgressCircle } from "@budibase/bbui" |
|||
import { getContext, onMount, onDestroy } from "svelte" |
|||
|
|||
export let datasourceId |
|||
export let bucket |
|||
export let key |
|||
export let field |
|||
export let label |
|||
export let disabled = false |
|||
export let validation |
|||
|
|||
let fieldState |
|||
let fieldApi |
|||
|
|||
const { API, notificationStore, uploadStore } = getContext("sdk") |
|||
const component = getContext("component") |
|||
|
|||
// 5GB cap per item sent via S3 REST API |
|||
const MaxFileSize = 1000000000 * 5 |
|||
|
|||
// Actual file data to upload |
|||
let data |
|||
let loading = false |
|||
|
|||
const handleFileTooLarge = () => { |
|||
notificationStore.actions.warning( |
|||
"Files cannot exceed 5GB. Please try again with a smaller file." |
|||
) |
|||
} |
|||
|
|||
// Process the file input and return a serializable structure expected by |
|||
// the dropzone component to display the file |
|||
const processFiles = async fileList => { |
|||
return await new Promise(resolve => { |
|||
if (!fileList?.length) { |
|||
return [] |
|||
} |
|||
|
|||
// Don't read in non-image files |
|||
data = fileList[0] |
|||
if (!data.type?.startsWith("image")) { |
|||
resolve([ |
|||
{ |
|||
name: data.name, |
|||
type: data.type, |
|||
}, |
|||
]) |
|||
} |
|||
|
|||
// Read image files and display as preview |
|||
const reader = new FileReader() |
|||
reader.addEventListener( |
|||
"load", |
|||
() => { |
|||
resolve([ |
|||
{ |
|||
url: reader.result, |
|||
name: data.name, |
|||
type: data.type, |
|||
}, |
|||
]) |
|||
}, |
|||
false |
|||
) |
|||
reader.readAsDataURL(fileList[0]) |
|||
}) |
|||
} |
|||
|
|||
const upload = async () => { |
|||
loading = true |
|||
try { |
|||
const res = await API.externalUpload(datasourceId, bucket, key, data) |
|||
notificationStore.actions.success("File uploaded successfully") |
|||
loading = false |
|||
return res |
|||
} catch (error) { |
|||
notificationStore.actions.error(`Error uploading file: ${error}`) |
|||
} |
|||
} |
|||
|
|||
onMount(() => { |
|||
uploadStore.actions.registerFileUpload($component.id, upload) |
|||
}) |
|||
|
|||
onDestroy(() => { |
|||
uploadStore.actions.unregisterFileUpload($component.id) |
|||
}) |
|||
</script> |
|||
|
|||
<Field |
|||
{label} |
|||
{field} |
|||
{disabled} |
|||
{validation} |
|||
type="s3upload" |
|||
bind:fieldState |
|||
bind:fieldApi |
|||
defaultValue={[]} |
|||
> |
|||
<div class="content"> |
|||
{#if fieldState} |
|||
<CoreDropzone |
|||
value={fieldState.value} |
|||
disabled={loading || fieldState.disabled} |
|||
error={fieldState.error} |
|||
on:change={e => { |
|||
fieldApi.setValue(e.detail) |
|||
}} |
|||
{processFiles} |
|||
{handleFileTooLarge} |
|||
maximum={1} |
|||
fileSizeLimit={MaxFileSize} |
|||
/> |
|||
{/if} |
|||
{#if loading} |
|||
<div class="overlay" /> |
|||
<div class="loading"> |
|||
<ProgressCircle /> |
|||
</div> |
|||
{/if} |
|||
</div> |
|||
</Field> |
|||
|
|||
<style> |
|||
.content { |
|||
position: relative; |
|||
} |
|||
.overlay, |
|||
.loading { |
|||
position: absolute; |
|||
top: 0; |
|||
height: 100%; |
|||
width: 100%; |
|||
display: grid; |
|||
place-items: center; |
|||
} |
|||
.overlay { |
|||
background-color: var(--spectrum-global-color-gray-50); |
|||
opacity: 0.5; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,42 @@ |
|||
import { writable, get } from "svelte/store" |
|||
|
|||
export const createUploadStore = () => { |
|||
const store = writable([]) |
|||
|
|||
// Registers a new file upload component
|
|||
const registerFileUpload = (componentId, callback) => { |
|||
if (!componentId || !callback) { |
|||
return |
|||
} |
|||
|
|||
store.update(state => { |
|||
state.push({ |
|||
componentId, |
|||
callback, |
|||
}) |
|||
return state |
|||
}) |
|||
} |
|||
|
|||
// Unregisters a file upload component
|
|||
const unregisterFileUpload = componentId => { |
|||
store.update(state => state.filter(c => c.componentId !== componentId)) |
|||
} |
|||
|
|||
// Processes a file upload for a given component ID
|
|||
const processFileUpload = async componentId => { |
|||
if (!componentId) { |
|||
return |
|||
} |
|||
|
|||
const component = get(store).find(c => c.componentId === componentId) |
|||
return await component?.callback() |
|||
} |
|||
|
|||
return { |
|||
subscribe: store.subscribe, |
|||
actions: { registerFileUpload, unregisterFileUpload, processFileUpload }, |
|||
} |
|||
} |
|||
|
|||
export const uploadStore = createUploadStore() |
|||
File diff suppressed because it is too large
@ -0,0 +1,98 @@ |
|||
jest.mock("node-fetch") |
|||
jest.mock("aws-sdk", () => ({ |
|||
config: { |
|||
update: jest.fn(), |
|||
}, |
|||
DynamoDB: { |
|||
DocumentClient: jest.fn(), |
|||
}, |
|||
S3: jest.fn(() => ({ |
|||
getSignedUrl: jest.fn(() => { |
|||
return "my-url" |
|||
}), |
|||
})), |
|||
})) |
|||
|
|||
const setup = require("./utilities") |
|||
|
|||
describe("/attachments", () => { |
|||
let request = setup.getRequest() |
|||
let config = setup.getConfig() |
|||
let app |
|||
|
|||
afterAll(setup.afterAll) |
|||
|
|||
beforeEach(async () => { |
|||
app = await config.init() |
|||
}) |
|||
|
|||
describe("generateSignedUrls", () => { |
|||
let datasource |
|||
|
|||
beforeEach(async () => { |
|||
datasource = await config.createDatasource({ |
|||
datasource: { |
|||
type: "datasource", |
|||
name: "Test", |
|||
source: "S3", |
|||
config: {}, |
|||
}, |
|||
}) |
|||
}) |
|||
|
|||
it("should be able to generate a signed upload URL", async () => { |
|||
const bucket = "foo" |
|||
const key = "bar" |
|||
const res = await request |
|||
.post(`/api/attachments/${datasource._id}/url`) |
|||
.send({ bucket, key }) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(200) |
|||
expect(res.body.signedUrl).toEqual("my-url") |
|||
expect(res.body.publicUrl).toEqual( |
|||
`https://${bucket}.s3.eu-west-1.amazonaws.com/${key}` |
|||
) |
|||
}) |
|||
|
|||
it("should handle an invalid datasource ID", async () => { |
|||
const res = await request |
|||
.post(`/api/attachments/foo/url`) |
|||
.send({ |
|||
bucket: "foo", |
|||
key: "bar", |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(400) |
|||
expect(res.body.message).toEqual( |
|||
"The specified datasource could not be found" |
|||
) |
|||
}) |
|||
|
|||
it("should require a bucket parameter", async () => { |
|||
const res = await request |
|||
.post(`/api/attachments/${datasource._id}/url`) |
|||
.send({ |
|||
bucket: undefined, |
|||
key: "bar", |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(400) |
|||
expect(res.body.message).toEqual("bucket and key values are required") |
|||
}) |
|||
|
|||
it("should require a key parameter", async () => { |
|||
const res = await request |
|||
.post(`/api/attachments/${datasource._id}/url`) |
|||
.send({ |
|||
bucket: "foo", |
|||
}) |
|||
.set(config.defaultHeaders()) |
|||
.expect("Content-Type", /json/) |
|||
.expect(400) |
|||
expect(res.body.message).toEqual("bucket and key values are required") |
|||
}) |
|||
}) |
|||
}) |
|||
Loading…
Reference in new issue