DEV Community

Cover image for How to Upload an Image to a Linode Storage Bucket Using a Pre-Signed URL
Mary Gathoni
Mary Gathoni

Posted on Edited on

How to Upload an Image to a Linode Storage Bucket Using a Pre-Signed URL

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:

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

  1. Install the @aws-sdk/s3-request-presigner and @aws-sdk/client-s3 libraries using npm by running the following command:

    npm install @aws-sdk/s3-request-presigner @aws-sdk/client-s3
    

    You'll also need the uuid package to generate unique file names for the uploaded images so install that too.

    npm install uuid
    
  2. 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.

  3. 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 Key property is set to a unique image path, which we create by prepending a unique ID from the uuid package to the file name. This is crucial for avoiding accidental data loss, since two images with the same name would overwrite each other.

  1. Now that you have the S3 client object and the PutObjectCommand, you can use the getSignedUrl function from the @aws-sdk/s3-request-presigner library 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 });
  }
}

```
Enter fullscreen mode Exit fullscreen mode

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

};
Enter fullscreen mode Exit fullscreen mode

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
      }
    }

};
Enter fullscreen mode Exit fullscreen mode

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)