Downloading S3 Objects Using Presigned URLs

Downloading S3 Objects Using Presigned URLs

Background

For my current work project I am generating an Excel workbook and saving it to a private S3 bucket. I have a UI built in React that should allow that Excel file to be downloaded with just a click of a button. At first I thought it had to be complicated: expose an API Gateway endpoint that connects to an AWS Lambda that queries S3 for that object and sends that over by a bytestream. Ugh. One thing though was Excel sheets didn’t translate well to bytestream (or at least it wasn’t obvious how to do it with the library I was working with).

…And then I discovered: presigned URLs

Whoa

With these babies you can generate a somewhat secure temporary link that exposes your S3 file as a downloadable link for the amount of time you set that link’s lifetime to.

As usual, Boto3 documentation on presigned URLs in Python could have been better.

But then...

Huzzah I found a worthy example of usage.

Dynamically Generated Download Link

import logging
import boto3
from botocore.exceptions import ClientError


def create_presigned_url(bucket_name, object_name, expiration=3600)
    # Generate a presigned URL for the S3 object
    s3_client = boto3.client('s3')
    try:
        response = s3_client.generate_presigned_url('get_object',
                                                    Params={'Bucket': bucket_name,
                                                            'Key': object_name},
                                                    ExpiresIn=expiration)
    except ClientError as e:
        logging.error(e)
        return None

    # The response contains the presigned URL
    return response

Now all I had to do was return the link as a response to when data is POST-ed on the frontend and voila, an easy to use and maintain way to make downloads from private S3 possible!

React Client Side Snippet

const DownloadThing = () => {
const [downloadLink, setDownloadLink] = useState('https://via.placeholder.com/150')
const handleDownload = async (e) => {
    try {
     // endpoint points to Lambda that generates workbook and presigned URL
      const res = await fetch(process.env.REACT_APP_API_ENDPOINT, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(data)
      })

      // URL returned from my POST call
      const { downloadurl } = await res.json()
      alert(`Created Excel sheet! ${downloadurl}`)
      setDownloadLink(downloadurl)
    }
    catch (err) {
      console.error(err)
    }
  }
return (
  <>
    <a href={downloadLink} download="test.xlsx">Get From S3</a>
  </>
)
}