diff --git a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx
index 9eb63ad6c8..f73e00369e 100644
--- a/apps/builder/src/components/ImageUploadContent/UploadButton.tsx
+++ b/apps/builder/src/components/ImageUploadContent/UploadButton.tsx
@@ -26,9 +26,15 @@ export const UploadButton = ({
setIsUploading(false)
},
onSuccess: async (data) => {
+ if (!file) return
+ const formData = new FormData()
+ Object.entries(data.formData).forEach(([key, value]) => {
+ formData.append(key, value)
+ })
+ formData.append('file', file)
const upload = await fetch(data.presignedUrl, {
- method: 'PUT',
- body: file,
+ method: 'POST',
+ body: formData,
})
if (!upload.ok) {
diff --git a/apps/builder/src/features/billing/components/UsageProgressBars.tsx b/apps/builder/src/features/billing/components/UsageProgressBars.tsx
index 30b5c9fdc6..5388885c00 100644
--- a/apps/builder/src/features/billing/components/UsageProgressBars.tsx
+++ b/apps/builder/src/features/billing/components/UsageProgressBars.tsx
@@ -9,12 +9,11 @@ import {
Tooltip,
} from '@chakra-ui/react'
import { AlertIcon } from '@/components/icons'
-import { Plan, Workspace } from '@typebot.io/prisma'
+import { Workspace } from '@typebot.io/prisma'
import React from 'react'
import { parseNumberWithCommas } from '@typebot.io/lib'
-import { getChatsLimit, getStorageLimit } from '@typebot.io/lib/pricing'
+import { getChatsLimit } from '@typebot.io/lib/pricing'
import { defaultQueryOptions, trpc } from '@/lib/trpc'
-import { storageToReadable } from '../helpers/storageToReadable'
import { useScopedI18n } from '@/locales'
type Props = {
@@ -30,19 +29,12 @@ export const UsageProgressBars = ({ workspace }: Props) => {
defaultQueryOptions
)
const totalChatsUsed = data?.totalChatsUsed ?? 0
- const totalStorageUsed = data?.totalStorageUsed ?? 0
const workspaceChatsLimit = getChatsLimit(workspace)
- const workspaceStorageLimit = getStorageLimit(workspace)
- const workspaceStorageLimitGigabites =
- workspaceStorageLimit * 1024 * 1024 * 1024
const chatsPercentage = Math.round(
(totalChatsUsed / workspaceChatsLimit) * 100
)
- const storagePercentage = Math.round(
- (totalStorageUsed / workspaceStorageLimitGigabites) * 100
- )
return (
@@ -103,63 +95,6 @@ export const UsageProgressBars = ({ workspace }: Props) => {
colorScheme={totalChatsUsed >= workspaceChatsLimit ? 'red' : 'blue'}
/>
- {workspace.plan !== Plan.FREE && (
-
-
-
-
- {scopedT('storage.heading')}
-
- {storagePercentage >= 80 && (
-
- {scopedT('storage.alert.soonReach')}
-
-
- {scopedT('storage.alert.updatePlan')}
-
- }
- >
-
-
-
-
- )}
-
-
-
- {storageToReadable(totalStorageUsed)}
-
-
- /{' '}
- {workspaceStorageLimit === -1
- ? scopedT('unlimited')
- : `${workspaceStorageLimit} GB`}
-
-
-
-
- )}
)
}
diff --git a/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx b/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx
index d68fa464c7..49e336c1de 100644
--- a/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx
+++ b/apps/builder/src/features/blocks/inputs/fileUpload/components/FileInputSettings.tsx
@@ -1,8 +1,8 @@
-import { FormLabel, HStack, Stack, Text } from '@chakra-ui/react'
+import { FormLabel, Stack } from '@chakra-ui/react'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { FileInputOptions, Variable } from '@typebot.io/schemas'
import React from 'react'
-import { TextInput, NumberInput } from '@/components/inputs'
+import { TextInput } from '@/components/inputs'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
@@ -24,9 +24,6 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
const handleVariableChange = (variable?: Variable) =>
onOptionsChange({ ...options, variableId: variable?.id })
- const handleSizeLimitChange = (sizeLimit?: number) =>
- onOptionsChange({ ...options, sizeLimit })
-
const handleRequiredChange = (isRequired: boolean) =>
onOptionsChange({ ...options, isRequired })
@@ -48,16 +45,6 @@ export const FileInputSettings = ({ options, onOptionsChange }: Props) => {
initialValue={options.isMultipleAllowed}
onCheckChange={handleMultipleFilesChange}
/>
-
-
- MB
-
-
Placeholder:
{
workspace?.plan,
])
- const storageLimitPercentage = useMemo(() => {
- if (!usageData?.totalStorageUsed || !workspace?.plan) return 0
- return Math.round(
- (usageData.totalStorageUsed /
- 1024 /
- 1024 /
- 1024 /
- getStorageLimit({
- additionalStorageIndex: workspace.additionalStorageIndex,
- plan: workspace.plan,
- customStorageLimit: workspace.customStorageLimit,
- })) *
- 100
- )
- }, [
- usageData?.totalStorageUsed,
- workspace?.additionalStorageIndex,
- workspace?.customStorageLimit,
- workspace?.plan,
- ])
-
return (
<>
{chatsLimitPercentage > ALERT_CHATS_PERCENT_THRESHOLD && (
@@ -74,22 +52,6 @@ export const UsageAlertBanners = ({ workspace }: Props) => {
/>
)}
- {storageLimitPercentage > ALERT_STORAGE_PERCENT_THRESHOLD && (
-
-
- Your workspace collected{' '}
- {storageLimitPercentage}% of your total storage
- allowed. Upgrade your plan or delete some existing results to
- continue collecting files from your user beyond this limit.
- >
- }
- buttonLabel="Upgrade"
- />
-
- )}
>
)
}
diff --git a/apps/builder/src/features/upload/api/generateUploadUrl.ts b/apps/builder/src/features/upload/api/generateUploadUrl.ts
index c03d40a462..f568340df3 100644
--- a/apps/builder/src/features/upload/api/generateUploadUrl.ts
+++ b/apps/builder/src/features/upload/api/generateUploadUrl.ts
@@ -2,7 +2,7 @@ import { authenticatedProcedure } from '@/helpers/server/trpc'
import { z } from 'zod'
import { env } from '@typebot.io/env'
import { TRPCError } from '@trpc/server'
-import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl'
+import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy'
import prisma from '@/lib/prisma'
import { isWriteWorkspaceForbidden } from '@/features/workspace/helpers/isWriteWorkspaceForbidden'
import { isWriteTypebotForbidden } from '@/features/typebot/helpers/isWriteTypebotForbidden'
@@ -54,6 +54,7 @@ export const generateUploadUrl = authenticatedProcedure
.output(
z.object({
presignedUrl: z.string(),
+ formData: z.record(z.string(), z.any()),
fileUrl: z.string(),
})
)
@@ -76,16 +77,17 @@ export const generateUploadUrl = authenticatedProcedure
uploadProps: filePathProps,
})
- const presignedUrl = await generatePresignedUrl({
+ const presignedPostPolicy = await generatePresignedPostPolicy({
fileType,
filePath,
})
return {
- presignedUrl,
+ presignedUrl: presignedPostPolicy.postURL,
+ formData: presignedPostPolicy.formData,
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
- : presignedUrl.split('?')[0],
+ : `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
}
})
diff --git a/apps/builder/src/pages/api/storage/upload-url.ts b/apps/builder/src/pages/api/storage/upload-url.ts
index a6cce8897c..49fbe854cc 100644
--- a/apps/builder/src/pages/api/storage/upload-url.ts
+++ b/apps/builder/src/pages/api/storage/upload-url.ts
@@ -5,7 +5,7 @@ import {
methodNotAllowed,
notAuthenticated,
} from '@typebot.io/lib/api'
-import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl'
+import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy'
import { env } from '@typebot.io/env'
const handler = async (
@@ -25,9 +25,15 @@ const handler = async (
const filePath = req.query.filePath as string | undefined
const fileType = req.query.fileType as string | undefined
if (!filePath || !fileType) return badRequest(res)
- const presignedUrl = await generatePresignedUrl({ fileType, filePath })
+ const presignedPostPolicy = await generatePresignedPostPolicy({
+ fileType,
+ filePath,
+ })
- return res.status(200).send({ presignedUrl })
+ return res.status(200).send({
+ presignedUrl: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
+ formData: presignedPostPolicy.formData,
+ })
}
return methodNotAllowed(res)
}
diff --git a/apps/docs/docs/editor/blocks/inputs/file-upload.mdx b/apps/docs/docs/editor/blocks/inputs/file-upload.mdx
index 2f1df61067..3730255b56 100644
--- a/apps/docs/docs/editor/blocks/inputs/file-upload.mdx
+++ b/apps/docs/docs/editor/blocks/inputs/file-upload.mdx
@@ -29,4 +29,6 @@ The File upload input block allows you to collect files from your user.
The placeholder accepts [HTML](https://en.wikipedia.org/wiki/HTML).
-Note that there is a 10MB fixed limit per file.
+## Size limit
+
+There is a 10MB fixed limit per uploaded file. If you want your respondents to upload larger files, you should ask them to upload their files to a cloud storage service (e.g. Google Drive, Dropbox, etc.) and share the link with you.
diff --git a/apps/docs/docs/self-hosting/configuration.md b/apps/docs/docs/self-hosting/configuration.md
index 4bfed6133f..819b2fcb0a 100644
--- a/apps/docs/docs/self-hosting/configuration.md
+++ b/apps/docs/docs/self-hosting/configuration.md
@@ -11,18 +11,19 @@ Parameters marked with are required.
## General
-| Parameter | Default | Description |
-| --------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| DATABASE_URL | | The database URL |
-| ENCRYPTION_SECRET | | A 256-bit key used to encrypt sensitive data. It is strongly recommended to [generate](https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) a new one. The secret should be the same between builder and viewer. |
-| NEXTAUTH_URL | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) |
-| NEXT_PUBLIC_VIEWER_URL | | The viewer base URL. Should be the publicly accessible URL (i.e. `https://bot.domain.com`) |
-| ADMIN_EMAIL | | The email that will get an `UNLIMITED` plan on user creation. The associated user will be able to bypass database rules. |
-| NEXTAUTH_URL_INTERNAL | | The internal builder base URL. You have to set it only when `NEXTAUTH_URL` can't be reached by your builder container / server. For a docker deployment, you should set it to `http://localhost:3000`. |
-| DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` |
-| DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. |
-| NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. |
-| DEBUG | false | If enabled, the server will print valuable logs to debug config issues. |
+| Parameter | Default | Description |
+| ------------------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| DATABASE_URL | | The database URL |
+| ENCRYPTION_SECRET | | A 256-bit key used to encrypt sensitive data. It is strongly recommended to [generate](https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx) a new one. The secret should be the same between builder and viewer. |
+| NEXTAUTH_URL | | The builder base URL. Should be the publicly accessible URL (i.e. `https://typebot.domain.com`) |
+| NEXT_PUBLIC_VIEWER_URL | | The viewer base URL. Should be the publicly accessible URL (i.e. `https://bot.domain.com`) |
+| ADMIN_EMAIL | | The email that will get an `UNLIMITED` plan on user creation. The associated user will be able to bypass database rules. |
+| NEXTAUTH_URL_INTERNAL | | The internal builder base URL. You have to set it only when `NEXTAUTH_URL` can't be reached by your builder container / server. For a docker deployment, you should set it to `http://localhost:3000`. |
+| DEFAULT_WORKSPACE_PLAN | FREE | Default workspace plan on user creation or when a user creates a new workspace. Possible values are `FREE`, `STARTER`, `PRO`, `LIFETIME`, `UNLIMITED`. The default plan for admin user is `UNLIMITED` |
+| DISABLE_SIGNUP | false | Disable new user sign ups. Invited users are still able to sign up. |
+| NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID | | Typebot ID used for the onboarding. Onboarding page is skipped if not provided. |
+| DEBUG | false | If enabled, the server will print valuable logs to debug config issues. |
+| NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE | | Limits the size of each file that can be uploaded in the bots (i.e. Set `10` to limit the file upload to 10MB) |
## Email (Auth, notifications)
diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts
index 0599975157..b9b11582ca 100644
--- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts
+++ b/apps/viewer/src/features/blocks/inputs/fileUpload/api/deprecated/getUploadUrl.ts
@@ -10,7 +10,7 @@ import {
} from '@typebot.io/schemas'
import { byId, isDefined } from '@typebot.io/lib'
import { z } from 'zod'
-import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl'
+import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy'
import { env } from '@typebot.io/env'
export const getUploadUrl = publicProcedure
@@ -34,6 +34,7 @@ export const getUploadUrl = publicProcedure
.output(
z.object({
presignedUrl: z.string(),
+ formData: z.record(z.string(), z.any()),
hasReachedStorageLimit: z.boolean(),
})
)
@@ -61,13 +62,15 @@ export const getUploadUrl = publicProcedure
message: 'File upload block not found',
})
- const presignedUrl = await generatePresignedUrl({
+ const presignedPostPolicy = await generatePresignedPostPolicy({
fileType,
filePath,
+ maxFileSize: env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
})
return {
- presignedUrl,
+ presignedUrl: `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
+ formData: presignedPostPolicy.formData,
hasReachedStorageLimit: false,
}
})
diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts
index 1c66a0bb61..175f679392 100644
--- a/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts
+++ b/apps/viewer/src/features/blocks/inputs/fileUpload/api/generateUploadUrl.ts
@@ -2,8 +2,9 @@ import { publicProcedure } from '@/helpers/server/trpc'
import prisma from '@/lib/prisma'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
-import { generatePresignedUrl } from '@typebot.io/lib/s3/generatePresignedUrl'
+import { generatePresignedPostPolicy } from '@typebot.io/lib/s3/generatePresignedPostPolicy'
import { env } from '@typebot.io/env'
+import { InputBlockType, publicTypebotSchema } from '@typebot.io/schemas'
export const generateUploadUrl = publicProcedure
.meta({
@@ -28,6 +29,7 @@ export const generateUploadUrl = publicProcedure
.output(
z.object({
presignedUrl: z.string(),
+ formData: z.record(z.string(), z.any()),
fileUrl: z.string(),
})
)
@@ -44,6 +46,7 @@ export const generateUploadUrl = publicProcedure
typebotId: filePathProps.typebotId,
},
select: {
+ groups: true,
typebot: {
select: {
workspaceId: true,
@@ -62,15 +65,30 @@ export const generateUploadUrl = publicProcedure
const filePath = `public/workspaces/${workspaceId}/typebots/${filePathProps.typebotId}/results/${filePathProps.resultId}/${filePathProps.fileName}`
- const presignedUrl = await generatePresignedUrl({
+ const fileUploadBlock = publicTypebotSchema._def.schema.shape.groups
+ .parse(publicTypebot.groups)
+ .flatMap((group) => group.blocks)
+ .find((block) => block.id === filePathProps.blockId)
+
+ if (fileUploadBlock?.type !== InputBlockType.FILE)
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: "Can't find file upload block",
+ })
+
+ const presignedPostPolicy = await generatePresignedPostPolicy({
fileType,
filePath,
+ maxFileSize:
+ fileUploadBlock.options.sizeLimit ??
+ env.NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE,
})
return {
- presignedUrl,
+ presignedUrl: presignedPostPolicy.postURL,
+ formData: presignedPostPolicy.formData,
fileUrl: env.S3_PUBLIC_CUSTOM_DOMAIN
? `${env.S3_PUBLIC_CUSTOM_DOMAIN}/${filePath}`
- : presignedUrl.split('?')[0],
+ : `${presignedPostPolicy.postURL}/${presignedPostPolicy.formData.key}`,
}
})
diff --git a/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts b/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts
index c8db4ad584..0eb4d8a1bd 100644
--- a/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts
+++ b/apps/viewer/src/features/blocks/inputs/fileUpload/fileUpload.spec.ts
@@ -93,25 +93,4 @@ test.describe('Storage limit is reached', () => {
fakeStorage: THREE_GIGABYTES,
})
})
-
- test("shouldn't upload anything if limit has been reached", async ({
- page,
- }) => {
- await page.goto(`/${typebotId}-public`)
- await page
- .locator(`input[type="file"]`)
- .setInputFiles([
- getTestAsset('typebots/api.json'),
- getTestAsset('typebots/fileUpload.json'),
- getTestAsset('typebots/hugeGroup.json'),
- ])
- await expect(page.locator(`text="3"`)).toBeVisible()
- await page.locator('text="Upload 3 files"').click()
- await expect(page.locator(`text="3 files uploaded"`)).toBeVisible()
- await page.evaluate(() =>
- window.localStorage.setItem('workspaceId', 'starterWorkspace')
- )
- await page.goto(`${env.NEXTAUTH_URL}/typebots/${typebotId}/results`)
- await expect(page.locator('text="150%"')).toBeVisible()
- })
})
diff --git a/apps/viewer/src/features/chat/queries/upsertAnswer.ts b/apps/viewer/src/features/chat/queries/upsertAnswer.ts
index 76a5c8ba33..49a359a938 100644
--- a/apps/viewer/src/features/chat/queries/upsertAnswer.ts
+++ b/apps/viewer/src/features/chat/queries/upsertAnswer.ts
@@ -1,8 +1,6 @@
import prisma from '@/lib/prisma'
-import { isNotDefined } from '@typebot.io/lib'
import { Prisma } from '@typebot.io/prisma'
-import { InputBlock, InputBlockType, SessionState } from '@typebot.io/schemas'
-import got from 'got'
+import { InputBlock, SessionState } from '@typebot.io/schemas'
type Props = {
answer: Omit
@@ -11,12 +9,9 @@ type Props = {
itemId?: string
state: SessionState
}
-export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
+export const upsertAnswer = async ({ answer, block, state }: Props) => {
const resultId = state.typebotsQueue[0].resultId
if (!resultId) return
- if (reply.includes('http') && block.type === InputBlockType.FILE) {
- answer.storageUsed = await computeStorageUsed(reply)
- }
const where = {
resultId,
blockId: block.id,
@@ -33,7 +28,6 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
where,
data: {
content: answer.content,
- storageUsed: answer.storageUsed,
itemId: answer.itemId,
},
})
@@ -41,18 +35,3 @@ export const upsertAnswer = async ({ answer, reply, block, state }: Props) => {
data: [{ ...answer, resultId }],
})
}
-
-const computeStorageUsed = async (reply: string) => {
- let storageUsed = 0
- const fileUrls = reply.split(', ')
- const hasReachedStorageLimit = fileUrls[0] === null
- if (!hasReachedStorageLimit) {
- for (const url of fileUrls) {
- const { headers } = await got(url)
- const size = headers['content-length']
- if (isNotDefined(size)) continue
- storageUsed += parseInt(size, 10)
- }
- }
- return storageUsed
-}
diff --git a/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts b/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts
index f793aa6d9e..682f5cff35 100644
--- a/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts
+++ b/packages/deprecated/bot-engine/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts
@@ -23,6 +23,7 @@ export const uploadFiles = async ({
i += 1
const { data } = await sendRequest<{
presignedUrl: string
+ formData: Record
hasReachedStorageLimit: boolean
}>(
`${basePath}/storage/upload-url?filePath=${encodeURIComponent(
@@ -35,9 +36,14 @@ export const uploadFiles = async ({
const url = data.presignedUrl
if (data.hasReachedStorageLimit) urls.push(null)
else {
- const upload = await fetch(url, {
- method: 'PUT',
- body: file,
+ const formData = new FormData()
+ Object.entries(data.formData).forEach(([key, value]) => {
+ formData.append(key, value)
+ })
+ formData.append('file', file)
+ const upload = await fetch(data.presignedUrl, {
+ method: 'POST',
+ body: formData,
})
if (!upload.ok) continue
diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx
index 455848d760..3bcd29e056 100644
--- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx
+++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/components/FileUploadForm.tsx
@@ -7,6 +7,7 @@ import { Button } from '@/components/Button'
import { Spinner } from '@/components/Spinner'
import { uploadFiles } from '../helpers/uploadFiles'
import { guessApiHost } from '@/utils/guessApiHost'
+import { getRuntimeVariable } from '@typebot.io/env/getRuntimeVariable'
type Props = {
context: BotContext
@@ -25,15 +26,14 @@ export const FileUploadForm = (props: Props) => {
const onNewFiles = (files: FileList) => {
setErrorMessage(undefined)
const newFiles = Array.from(files)
+ const sizeLimit =
+ props.block.options.sizeLimit ??
+ getRuntimeVariable('NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE')
if (
- newFiles.some(
- (file) =>
- file.size > (props.block.options.sizeLimit ?? 10) * 1024 * 1024
- )
+ sizeLimit &&
+ newFiles.some((file) => file.size > sizeLimit * 1024 * 1024)
)
- return setErrorMessage(
- `A file is larger than ${props.block.options.sizeLimit ?? 10}MB`
- )
+ return setErrorMessage(`A file is larger than ${sizeLimit}MB`)
if (!props.block.options.isMultipleAllowed && files)
return startSingleFileUpload(newFiles[0])
setSelectedFiles([...selectedFiles(), ...newFiles])
diff --git a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts
index c0e5af889a..2792739c64 100644
--- a/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts
+++ b/packages/embeds/js/src/features/blocks/inputs/fileUpload/helpers/uploadFiles.ts
@@ -28,6 +28,7 @@ export const uploadFiles = async ({
i += 1
const { data } = await sendRequest<{
presignedUrl: string
+ formData: Record
fileUrl: string
}>({
method: 'POST',
@@ -40,9 +41,14 @@ export const uploadFiles = async ({
if (!data?.presignedUrl) continue
else {
+ const formData = new FormData()
+ Object.entries(data.formData).forEach(([key, value]) => {
+ formData.append(key, value)
+ })
+ formData.append('file', file)
const upload = await fetch(data.presignedUrl, {
- method: 'PUT',
- body: file,
+ method: 'POST',
+ body: formData,
})
if (!upload.ok) continue
diff --git a/packages/env/env.ts b/packages/env/env.ts
index 6f6abfdc96..072db6d20c 100644
--- a/packages/env/env.ts
+++ b/packages/env/env.ts
@@ -35,6 +35,7 @@ const baseEnv = {
.transform((string) => string.split(',')),
NEXT_PUBLIC_VIEWER_INTERNAL_URL: z.string().url().optional(),
NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID: z.string().min(1).optional(),
+ NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: z.coerce.number().optional(),
},
runtimeEnv: {
NEXT_PUBLIC_E2E_TEST: getRuntimeVariable('NEXT_PUBLIC_E2E_TEST'),
@@ -45,6 +46,9 @@ const baseEnv = {
NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID: getRuntimeVariable(
'NEXT_PUBLIC_ONBOARDING_TYPEBOT_ID'
),
+ NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE: getRuntimeVariable(
+ 'NEXT_PUBLIC_BOT_FILE_UPLOAD_MAX_SIZE'
+ ),
},
}
const githubEnv = {
diff --git a/packages/lib/s3/generatePresignedPostPolicy.ts b/packages/lib/s3/generatePresignedPostPolicy.ts
new file mode 100644
index 0000000000..e514fefc34
--- /dev/null
+++ b/packages/lib/s3/generatePresignedPostPolicy.ts
@@ -0,0 +1,40 @@
+import { env } from '@typebot.io/env'
+import { Client, PostPolicyResult } from 'minio'
+
+type Props = {
+ filePath: string
+ fileType?: string
+ maxFileSize?: number
+}
+
+const tenMinutes = 10 * 60
+
+export const generatePresignedPostPolicy = async ({
+ filePath,
+ fileType,
+ maxFileSize,
+}: Props): Promise => {
+ if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
+ throw new Error(
+ 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY'
+ )
+
+ const minioClient = new Client({
+ endPoint: env.S3_ENDPOINT,
+ port: env.S3_PORT,
+ useSSL: env.S3_SSL,
+ accessKey: env.S3_ACCESS_KEY,
+ secretKey: env.S3_SECRET_KEY,
+ region: env.S3_REGION,
+ })
+
+ const postPolicy = minioClient.newPostPolicy()
+ if (maxFileSize)
+ postPolicy.setContentLengthRange(0, maxFileSize * 1024 * 1024)
+ postPolicy.setKey(filePath)
+ postPolicy.setBucket(env.S3_BUCKET)
+ postPolicy.setExpires(new Date(Date.now() + tenMinutes))
+ if (fileType) postPolicy.setContentType(fileType)
+
+ return minioClient.presignedPostPolicy(postPolicy)
+}
diff --git a/packages/lib/s3/generatePresignedUrl.ts b/packages/lib/s3/generatePresignedUrl.ts
deleted file mode 100644
index 2003cf8057..0000000000
--- a/packages/lib/s3/generatePresignedUrl.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { env } from '@typebot.io/env'
-import { Client } from 'minio'
-
-type GeneratePresignedUrlProps = {
- filePath: string
- fileType?: string
-}
-
-const tenMinutes = 10 * 60
-
-export const generatePresignedUrl = async ({
- filePath,
- fileType,
-}: GeneratePresignedUrlProps): Promise => {
- if (!env.S3_ENDPOINT || !env.S3_ACCESS_KEY || !env.S3_SECRET_KEY)
- throw new Error(
- 'S3 not properly configured. Missing one of those variables: S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY'
- )
-
- const minioClient = new Client({
- endPoint: env.S3_ENDPOINT,
- port: env.S3_PORT,
- useSSL: env.S3_SSL,
- accessKey: env.S3_ACCESS_KEY,
- secretKey: env.S3_SECRET_KEY,
- region: env.S3_REGION,
- })
-
- return minioClient.presignedUrl('PUT', env.S3_BUCKET, filePath, tenMinutes, {
- 'Content-Type': fileType,
- })
-}
diff --git a/packages/schemas/features/blocks/inputs/file.ts b/packages/schemas/features/blocks/inputs/file.ts
index 46a455834d..5fd624670b 100644
--- a/packages/schemas/features/blocks/inputs/file.ts
+++ b/packages/schemas/features/blocks/inputs/file.ts
@@ -12,7 +12,7 @@ export const fileInputOptionsSchema = optionBaseSchema.merge(
clear: z.string().optional(),
skip: z.string().optional(),
}),
- sizeLimit: z.number().optional(),
+ sizeLimit: z.number().optional().describe('Deprecated'),
})
)