🛂 (fileUpload) Improve file upload size limit enforcement
Closes #799, closes #797
This commit is contained in:
@@ -23,6 +23,7 @@ export const uploadFiles = async ({
|
||||
i += 1
|
||||
const { data } = await sendRequest<{
|
||||
presignedUrl: string
|
||||
formData: Record<string, string>
|
||||
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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -28,6 +28,7 @@ export const uploadFiles = async ({
|
||||
i += 1
|
||||
const { data } = await sendRequest<{
|
||||
presignedUrl: string
|
||||
formData: Record<string, string>
|
||||
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
|
||||
|
||||
4
packages/env/env.ts
vendored
4
packages/env/env.ts
vendored
@@ -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 = {
|
||||
|
||||
40
packages/lib/s3/generatePresignedPostPolicy.ts
Normal file
40
packages/lib/s3/generatePresignedPostPolicy.ts
Normal file
@@ -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<PostPolicyResult> => {
|
||||
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)
|
||||
}
|
||||
@@ -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<string> => {
|
||||
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,
|
||||
})
|
||||
}
|
||||
@@ -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'),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user