packages/docs/docs/presigned-urls.mdx
This article provides guidance for webapps wanting to allow users to upload videos and other assets. We recommend to generate a presigned URL server-side that allows a user to directly upload a file into your cloud storage without having to pass the file through your server.
You can set constraints such as maximal file size and file type, apply rate limiting, require authentication, and predefine the storage location.
The traditional way of implementing a file upload would be to let the client upload the file onto a server, which then stores the file on disk or forwards the upload to cloud storage. While this approach works, it's not ideal due to several reasons.
Here is an example for storing user uploads are stored in AWS S3.
In your bucket on the AWS console, go to Permissions and allow PUT requests via CORS:
[
{
"AllowedHeaders": ["*"],
"AllowedMethods": ["PUT"],
"AllowedOrigins": ["*"],
"ExposeHeaders": [],
"MaxAgeSeconds": 3000
}
]
:::note
It may prove useful to also allow the GET method via CORS so you can fetch the assets after uploading.
:::
Your AWS user policy must at least have the ability to put an object and make it public:
{
"Sid": "Presign",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:PutObjectAcl"],
"Resource": ["arn:aws:s3:::{YOUR_BUCKET_NAME}/*"]
}
First, accept a file in your frontend, for example using <input type="file">. You should get a File, from which you can determine the content type and content length:
import {interpolate} from 'remotion';
const file: File = {} as unknown as File;
// ---cut---
const contentType = file.type || 'application/octet-stream';
const arrayBuffer = await file.arrayBuffer();
const contentLength = arrayBuffer.byteLength;
This example uses @aws-sdk/s3-request-presigner and the AWS SDK imported from @remotion/lambda. By calling the function below, two URLs are generated:
presignedUrl is a URL to which the file can be uploaded toreadUrl is the URL from which the file can be read from.import {getSignedUrl} from '@aws-sdk/s3-request-presigner';
import {AwsRegion, getAwsClient} from '@remotion/lambda/client';
export const generatePresignedUrl = async (contentType: string, contentLength: number, expiresIn: number, bucketName: string, region: AwsRegion): Promise<{presignedUrl: string; readUrl: string}> => {
if (contentLength > 1024 * 1024 * 200) {
throw new Error(`File may not be over 200MB. Yours is ${contentLength} bytes.`);
}
const {client, sdk} = getAwsClient({
region: process.env.REMOTION_AWS_REGION as AwsRegion,
service: 's3',
});
const key = crypto.randomUUID();
const command = new sdk.PutObjectCommand({
Bucket: bucketName,
Key: key,
ACL: 'public-read',
ContentLength: contentLength,
ContentType: contentType,
});
const presignedUrl = await getSignedUrl(client, command, {
expiresIn,
});
// The location of the asset after the upload
const readUrl = `https://${bucketName}.s3.${region}.amazonaws.com/${key}`;
return {presignedUrl, readUrl};
};
Explanation:
@aws-sdk/client-s3 package directly.Here is a sample snippet for the Next.js App Router.
The endpoint is available under api/upload/route.ts.
import {NextResponse} from 'next/server';
import {getSignedUrl} from '@aws-sdk/s3-request-presigner';
import {AwsRegion, getAwsClient} from '@remotion/lambda/client';
const generatePresignedUrl = async ({contentType, contentLength, expiresIn, bucketName, region}: {contentType: string; contentLength: number; expiresIn: number; bucketName: string; region: AwsRegion}): Promise<{presignedUrl: string; readUrl: string}> => {
if (contentLength > 1024 * 1024 * 200) {
throw new Error(`File may not be over 200MB. Yours is ${contentLength} bytes.`);
}
const {client, sdk} = getAwsClient({
region: process.env.REMOTION_AWS_REGION as AwsRegion,
service: 's3',
});
const key = crypto.randomUUID();
const command = new sdk.PutObjectCommand({
Bucket: bucketName,
Key: key,
ACL: 'public-read',
ContentLength: contentLength,
ContentType: contentType,
});
const presignedUrl = await getSignedUrl(client, command, {
expiresIn,
});
// The location of the asset after the upload
const readUrl = `https://${bucketName}.s3.${region}.amazonaws.com/${key}`;
return {presignedUrl, readUrl};
};
export const POST = async (request: Request) => {
if (!process.env.REMOTION_AWS_BUCKET_NAME) {
throw new Error('REMOTION_AWS_BUCKET_NAME is not set');
}
if (!process.env.REMOTION_AWS_REGION) {
throw new Error('REMOTION_AWS_REGION is not set');
}
const json = await request.json();
if (!Number.isFinite(json.size)) {
throw new Error('size is not a number');
}
if (typeof json.contentType !== 'string') {
throw new Error('contentType is not a string');
}
const {presignedUrl, readUrl} = await generatePresignedUrl({
contentType: json.contentType,
contentLength: json.size,
expiresIn: 60 * 60 * 24 * 7,
bucketName: process.env.REMOTION_AWS_BUCKET_NAME as string,
region: process.env.REMOTION_AWS_REGION as AwsRegion,
});
return NextResponse.json({presignedUrl, readUrl});
};
This is how you can call it in the frontend:
const file: File = {} as unknown as File;
// ---cut---
const presignedResponse = await fetch('/api/upload', {
method: 'POST',
body: JSON.stringify({
size: file.size,
contentType: file.type,
// ^?
}),
});
const json = (await presignedResponse.json()) as {
presignedUrl: string;
readUrl: string;
};
:::note This example does not implement any rate limiting or authentication. :::
Send the presigned URL back to the client. Afterwards, you can now perform an upload using the built-in fetch() function:
import {interpolate} from 'remotion';
const presignedUrl = 'hi';
const file: File = {} as unknown as File;
const contentType = file.type || 'application/octet-stream';
const arrayBuffer = await file.arrayBuffer();
// ---cut---
await fetch(presignedUrl, {
method: 'PUT',
body: arrayBuffer,
headers: {
'content-type': contentType,
},
});
As of October 2024, if you need to track the progress of the upload, you need to use XMLHTTPRequest.
export type UploadProgress = {
progress: number;
loadedBytes: number;
totalBytes: number;
};
export type OnUploadProgress = (options: UploadProgress) => void;
export const uploadWithProgress = ({file, url, onProgress}: {file: File; url: string; onProgress: OnUploadProgress}): Promise<void> => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('PUT', url);
xhr.upload.onprogress = function (event) {
if (event.lengthComputable) {
onProgress({
progress: event.loaded / event.total,
loadedBytes: event.loaded,
totalBytes: event.total,
});
}
};
xhr.onload = function () {
if (xhr.status === 200) {
resolve();
} else {
reject(new Error(`Upload failed with status: ${xhr.status}`));
}
};
xhr.onerror = function () {
reject(new Error('Network error occurred during upload'));
};
xhr.setRequestHeader('content-type', file.type);
xhr.send(file);
});
};