This article explains how to upload images from a Next.js app to a Linode storage bucket.It came about after trying to figure it out during a hackathon I had participated in.
It talks about how you can use pre-signed URLs to upload images to a Linode bucket using the AWS-SDK JavaScript client library. For the actual upload, I’ll create a simple form that accepts images.
Prerequisites
Before you begin, you should have:
A good grasp of React concepts.
A basic Next.js application: If you don’t, create one following the instructions from the official getting started guide.
Node.js and npm installed: You can download them for Windows and Linux in a jiff.
A code editor: I'm using VS Code.
Setting Up Linode
First things first, you’ll need a Linode account:
- Head to the Linode website and click the “Sign Up” button in the top right corner of the homepage.
- Follow the prompts to verify your account.
- Choose a payment method, either credit card or PayPal, and enter your details. First-time users get a $100 credit, which has been plenty for me to experiment with so far.
- Agree to Linode’s terms of service and privacy policy.
- Click “Create Account” to finish up.
Once you’ve verified your email, log in to Linode and you’re ready to go.
Creating a Linode Object Storage Bucket
The object storage bucket is where you’ll keep the images. You could store them directly in a database, but performance would suffer because media files take up so much space. A better approach is to save the images in a storage bucket and keep only the file metadata, like filenames and file paths, in the database.
Create a Linode storage bucket by following these steps:
- In the left-hand sidebar of your Linode dashboard, click “Object Storage” under the “Storage” section.
- Click the “Create a Bucket” button in the top right corner.
- Enter a name for your bucket and choose the region where you want it stored.
- Click “Create Bucket” to finish setting it up.
Open your new bucket, go to the Access tab, and select “Public-read” from the access dropdown. This lets everyone, including your application, read the objects in the bucket, which will come in handy when you start serving the images on your site.
Once your bucket is ready, click the “Create access key” button on your object storage dashboard to generate an access key. You’ll use this key to interact with the bucket.
Copy the access key ID and the secret access key, and save them in a .env file in your application.
Accepting Images From Users
Back in your Next.js folder, navigate to the app directory and create a new folder called upload. Inside it, add a file called page.js.
Then copy the code below into this file. It creates a form with a file input that lets users select an image from their file system.
import { useState } from "react";
export default function Upload() {
const [file, setfile] = useState("");
const handleUpload = (e) => {
e.preventDefault();
// get signed url
// use signed url to upload image
};
const handleChange = (e) => {
const file = e.target.files[0];
setfile(file);
};
return (
<form onSubmit={handleUpload} className={styles["upload-form"]}>
<input type="file" onChange={handleChange} />
<button type="submit">Upload</button>
</form>
);
}
The form calls a function named handleUpload when a user submits it. This function will retrieve a signed URL from an API route we’ll create in the next step, then use it to upload the image to the storage bucket.
Generate a Pre-Signed URL
A pre-signed URL is a URL that gives you temporary access to a specific object in a storage bucket. It lets your app’s users access that object without needing credentials, which helps keep your bucket secure.
Follow the steps below to get a pre-signed URL for uploading images to Linode.
-
Install the
@aws-sdk/s3-request-presignerand@aws-sdk/client-s3libraries using npm by running the following command:
npm install @aws-sdk/s3-request-presigner @aws-sdk/client-s3You'll also need the
uuidpackage to generate unique file names for the uploaded images so install that too.
npm install uuid -
Create an S3 client object using your Linode credentials.
const { S3Client } = require("@aws-sdk/client-s3"); const s3Client = new S3Client({ endpoint: 'your_bucket_endpoint', region: 'your_cluster_region', credentials: { accessKeyId: 'your_access_key_id', secretAccessKey: 'your_secret_access_key', }, });Replace the placeholders with your storage bucket details.
-
In the handler function, create a PutObjectCommand. This command represents a request to store an object in the bucket..
const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); export default async function handler(req, res) { const filepath = `${uuidv4()}-${req.body.filename}`; try { const putObjectCommand = new PutObjectCommand({ Bucket: req.body.bucketname, Key: filepath, ACL: "public-read", // This allows anyone to access the uploaded image }); } catch (error) { return res.json({ error: error.message }); } }
Notice that the
Keyproperty is set to a unique image path, which we create by prepending a unique ID from theuuidpackage to the file name. This is crucial for avoiding accidental data loss, since two images with the same name would overwrite each other.
- Now that you have the S3 client object and the PutObjectCommand, you can use the
getSignedUrlfunction from the@aws-sdk/s3-request-presignerlibrary to get a pre-signed URL for the command. This function generates a URL you can use to upload your object to your Linode bucket.
Here’s how:
```jsx
const { getSignedUrl } = require("@aws-sdk/s3-request-presigner");
export default async function handler(req, res) {
try {
const filepath = `${uuidv4()}-${req.body.filename}`;
const putObjectCommand = new PutObjectCommand({
Bucket: req.body.bucketname,
Key: filepath,
ACL: "public-read", // This allows anyone to access the uploaded image
});
const signedUrl = await getSignedUrl(s3Client, putObjectCommand, {
expiresIn: 60, // Expires in 1 minutes
});
return res.json({
signedUrl: signedUrl,
filepath,
});
} catch (error) {
return res.json({ error: error.message });
}
}
```
The getSignedUrl function takes these arguments:
- The S3 client object.
- The PutObjectCommand.
- An options object that sets the URL’s expiration time to 1 minute (60 seconds).
Once you have the pre-signed URL, you can send it back to the client along with the file path.
Using the Signed URL to Upload Images
In the upload page, update the handleUpload() function so it retrieves the signed URL by making a fetch request to the /api/getSignedUrl endpoint.
const handleUpload = async (e) => {
e.preventDefault();
// get signed url
if (file) {
// getsignedurl
const response = await fetch("/api/getSignedUrl", {
method: "post",
body: {
bucketname: process.env.BUCKET_NAME,
filename: file.name,
},
});
const data = await response.json();
const signedUrl = data.signedUrl;
const filepath = data.filepath; // store filepath in database
}
// use signed url to upload image
};
The response from /api/getSignedUrl also contains the file path to the image. You can save this path in the database and retrieve it later when you serve the images on your site.
Once you have the signed URL, use it to upload the image to Linode with a PUT request.
const handleUpload = async (e) => {
e.preventDefault();
// get signed url
if (file) {
// getsignedurl
const response = await fetch("/api/getSignedUrl", {
method: "post",
body: {
bucketname: process.env.BUCKET_NAME,
filename: file.name,
},
});
const data = await response.json();
const signedUrl = data.signedUrl;
const filepath = data.filepath; // store filepath in database
// Use signed url to upload image
const uploaded = await fetch(`${signedUrl}`, {
method: "put",
body: {
file: file,
},
headers: {
"Content-Type": file.type,
"x-amz-acl": "public-read",
},
});
if (uploaded.status == 200) {
// Successfully uploaded image
} else {
// Could not upload
}
}
};
Here, the headers option sets the content type and makes the image publicly accessible. That way, when you use the image’s file path on your site, you won’t get an access denied error.
Next Steps
This article showed you how to use pre-signed URLs to upload images to an object storage bucket with an AWS S3 client library. As a next step, store the file path returned each time you create a signed URL in a table in your database. For example, if you’re storing profile pictures, you can save the file path in the accounts table, linked to the currently authenticated user.
Hope this was helpful! Thanks for reading.
Top comments (0)