In this article, we will compare the steps involved to upload a file to AWS S3 between Documenso and AWS S3 image upload example.
We start with the simple example provided by Vercel.
examples/aws-s3-image-upload
Vercel provides a good working example of uploading a file to AWS S3.
This example’s README provides two options, either you can use an existing S3 bucket or create a new bucket. Understanding this helps
you configure your upload feature correctly.
It is about the time we look at the source code. We are looking for an input element with type=file
. In the app/page.tsx, you will find this below code:
return (
<main>
<h1>Upload a File to S3</h1>
<form onSubmit={handleSubmit}>
<input
id="file"
type="file"
onChange={(e) => {
const files = e.target.files
if (files) {
setFile(files[0])
}
}}
accept="image/png, image/jpeg"
/>
<button type="submit" disabled={uploading}>
Upload
</button>
</form>
</main>
)
}
onChange
onChange
updates state using setFile
, but it does not do the uploading. upload happens when you submit this form.
onChange={(e) => {
const files = e.target.files
if (files) {
setFile(files[0])
}
}}
handleSubmit
A lot is happening in the handleSubmit function. We need to analyse the list of operations in this handleSubmit function. I have written the comments inside this code snippet to explain the steps.
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!file) {
alert('Please select a file to upload.')
return
}
setUploading(true)
const response = await fetch(
process.env.NEXT_PUBLIC_BASE_URL + '/api/upload',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ filename: file.name, contentType: file.type }),
}
)
if (response.ok) {
const { url, fields } = await response.json()
const formData = new FormData()
Object.entries(fields).forEach(([key, value]) => {
formData.append(key, value as string)
})
formData.append('file', file)
const uploadResponse = await fetch(url, {
method: 'POST',
body: formData,
})
if (uploadResponse.ok) {
alert('Upload successful!')
} else {
console.error('S3 Upload Error:', uploadResponse)
alert('Upload failed.')
}
} else {
alert('Failed to get pre-signed URL.')
}
setUploading(false)
}
api/upload
api/upload/route.ts has the below code:
import { createPresignedPost } from '@aws-sdk/s3-presigned-post'
import { S3Client } from '@aws-sdk/client-s3'
import { v4 as uuidv4 } from 'uuid'
export async function POST(request: Request) {
const { filename, contentType } = await request.json()
try {
const client = new S3Client({ region: process.env.AWS_REGION })
const { url, fields } = await createPresignedPost(client, {
Bucket: process.env.AWS_BUCKET_NAME,
Key: uuidv4(),
Conditions: [
['content-length-range', 0, 10485760], // up to 10 MB
['starts-with', '$Content-Type', contentType],
],
Fields: {
acl: 'public-read',
'Content-Type': contentType,
},
Expires: 600, // Seconds before the presigned post expires. 3600 by default.
})
return Response.json({ url, fields })
} catch (error) {
return Response.json({ error: error.message })
}
}
The first request in the handleSubmit was to /api/upload and sends content type and filename as payload. It is parsed as below:
const { filename, contentType } = await request.json()
Next step is to create a S3 client and then create a presigned post that returns url and field. You would use this url
to upload your file.
With this knowledge, let’s analyse how the upload works in Documenso and draw some comparison.
PDF file upload in Documenso
Let’s start with the input element with type=file
. Code is organized differently in Documenso. You would find input element in a file named document-dropzone.tsx.
<input {...getInputProps()} />
<p className="text-foreground mt-8 font-medium">{_(heading[type])}</p>
Here getInputProps
is returned useDropzone
. Documenso uses react-dropzone.
import { useDropzone } from 'react-dropzone';
onDrop calls props.onDrop, you will find an attribute value named onFileDrop
in upload-document.tsx.
<DocumentDropzone
className="h-[min(400px,50vh)]"
disabled={remaining.documents === 0 || !session?.user.emailVerified}
disabledMessage={disabledMessage}
onDrop={onFileDrop}
onDropRejected={onFileDropRejected}
/>
Let’s see what happens in onFileDrop function.
const onFileDrop = async (file: File) => {
try {
setIsLoading(true);
const { type, data } = await putPdfFile(file);
const { id: documentDataId } = await createDocumentData({
type,
data,
});
const { id } = await createDocument({
title: file.name,
documentDataId,
teamId: team?.id,
});
void refreshLimits();
toast({
title: _(msg`Document uploaded`),
description: _(msg`Your document has been uploaded successfully.`),
duration: 5000,
});
analytics.capture('App: Document Uploaded', {
userId: session?.user.id,
documentId: id,
timestamp: new Date().toISOString(),
});
router.push(`${formatDocumentsPath(team?.url)}/${id}/edit`);
} catch (err) {
const error = AppError.parseError(err);
console.error(err);
if (error.code === 'INVALID_DOCUMENT_FILE') {
toast({
title: _(msg`Invalid file`),
description: _(msg`You cannot upload encrypted PDFs`),
variant: 'destructive',
});
} else if (err instanceof TRPCClientError) {
toast({
title: _(msg`Error`),
description: err.message,
variant: 'destructive',
});
} else {
toast({
title: _(msg`Error`),
description: _(msg`An error occurred while uploading your document.`),
variant: 'destructive',
});
}
} finally {
setIsLoading(false);
}
};
There’s a lot of happening but for our analysis, let’s only consider the function named putFile
.
putPdfFile
putPdfFile is defined in upload/put-file.ts
/**
* Uploads a document file to the appropriate storage location and creates
* a document data record.
*/
export const putPdfFile = async (file: File) => {
const isEncryptedDocumentsAllowed = await getFlag('app_allow_encrypted_documents').catch(
() => false,
);
const pdf = await PDFDocument.load(await file.arrayBuffer()).catch((e) => {
console.error(`PDF upload parse error: ${e.message}`);
throw new AppError('INVALID_DOCUMENT_FILE');
});
if (!isEncryptedDocumentsAllowed && pdf.isEncrypted) {
throw new AppError('INVALID_DOCUMENT_FILE');
}
if (!file.name.endsWith('.pdf')) {
file.name = `${file.name}.pdf`;
}
removeOptionalContentGroups(pdf);
const bytes = await pdf.save();
const { type, data } = await putFile(new File([bytes], file.name, { type: 'application/pdf' }));
return await createDocumentData({ type, data });
};
putFile
This calls putFile
function.
/**
* Uploads a file to the appropriate storage location.
*/
export const putFile = async (file: File) => {
const NEXT_PUBLIC_UPLOAD_TRANSPORT = env('NEXT_PUBLIC_UPLOAD_TRANSPORT');
return await match(NEXT_PUBLIC_UPLOAD_TRANSPORT)
.with('s3', async () => putFileInS3(file))
.otherwise(async () => putFileInDatabase(file));
};
putFileInS3
const putFileInS3 = async (file: File) => {
const { getPresignPostUrl } = await import('./server-actions');
const { url, key } = await getPresignPostUrl(file.name, file.type);
const body = await file.arrayBuffer();
const reponse = await fetch(url, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
},
body,
});
if (!reponse.ok) {
throw new Error(
`Failed to upload file "${file.name}", failed with status code ${reponse.status}`,
);
}
return {
type: DocumentDataType.S3_PATH,
data: key,
};
};
getPresignPostUrl
export const getPresignPostUrl = async (fileName: string, contentType: string) => {
const client = getS3Client();
const { getSignedUrl } = await import('@aws-sdk/s3-request-presigner');
let token: JWT | null = null;
try {
const baseUrl = APP_BASE_URL() ?? 'http://localhost:3000';
token = await getToken({
req: new NextRequest(baseUrl, {
headers: headers(),
}),
});
} catch (err) {
// Non server-component environment
}
// Get the basename and extension for the file
const { name, ext } = path.parse(fileName);
let key = `${alphaid(12)}/${slugify(name)}${ext}`;
if (token) {
key = `${token.id}/${key}`;
}
const putObjectCommand = new PutObjectCommand({
Bucket: process.env.NEXT_PRIVATE_UPLOAD_BUCKET,
Key: key,
ContentType: contentType,
});
const url = await getSignedUrl(client, putObjectCommand, {
expiresIn: ONE_HOUR / ONE_SECOND,
});
return { key, url };
};
Comparison
You do not see any POST request in Documenso. It uses a function named getSignedUrl to get the url, whereas
vercel example makes aPOST
request toapi/upload
route.Input element can be located easily in Vercel example as this is just an example, but Documenso is found
to be using react-dropzone and the input element is located according to business context.
About us:
At Thinkthroo, we study large open source projects and provide architectural guides. We have developed reusable Components, built with tailwind, that you can use in your project.
We offer Next.js, React and Node development services.
Book a meeting with us to discuss your project.
References:
https://github.com/documenso/documenso/blob/main/packages/lib/universal/upload/put-file.ts#L69
https://github.com/vercel/examples/blob/main/solutions/aws-s3-image-upload/README.md
https://github.com/vercel/examples/tree/main/solutions/aws-s3-image-upload
https://github.com/vercel/examples/blob/main/solutions/aws-s3-image-upload/app/page.tsx#L58C5-L76C12
https://github.com/vercel/examples/blob/main/solutions/aws-s3-image-upload/app/api/upload/route.ts
https://github.com/documenso/documenso/blob/main/packages/ui/primitives/document-dropzone.tsx#L157
https://github.com/documenso/documenso/blob/main/packages/lib/universal/upload/put-file.ts#L22
Top comments (0)