Golang: Presign upload/download URLs for S3


The AWS documentation for presigning URLs for file upload/download in S3 are a little sparse. The most complicated part of the project is setting up the configuration and required client objects. Once those are created using them to get the desired URLs is straightforward.

Read more

Setup

Both the upload and download flows require some similar parts. They both leverage an aws.Config, aws.Client, and s3.PresignClient. Here are the required imports and a very basic setup. At the end of both the Upload and Download sections is a complete example if you don’t want to read the breakdown.

Imports and Configuration

For the most simple use case I only require two separate imports. Both of these packages are separate modules so they will need to be added separately to my go.mod file. The easiest way to install these is to run the go get commands below as dependency modules will be included too:

go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/service/s3

After I have the modules installed I can import them and set up our configuration:

package main

import (
  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/config"
  "github.com/aws/aws-sdk-go-v2/credentials"
  "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
  awsConfig := config.LoadDefaultConfig(
    context.Background(),
    config.WithCredentialsProvider(
      credentials.NewStaticCredentialsProvider(
        "ACCESS_KEY_ID",
        "SECRET_ACCESS_KEY",
        "SESSION_TOKEN", // This is optional
      ),
    ),
  )
}

In this example I am using the background context. Depending on the application being built it may make more sense to provide different contexts. The ACCESS_KEY_ID and SECRET_ACCESS_KEY are both required to interact with AWS APIs. In the example I am using a static credentials provider which is NOT RECOMMENDED. Make sure to use an alternative credential provider as specified in the documentation: https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/. The best option is to create a credential file as specified in that documentation (The docs are actually pretty good about describing this part).

Now that I have the aws configuration created I can use it to create the two client objects required for uploads and downloads:

// imports omitted for brevity
func main() {
  // awsConfig definition omitted for brevity
  // awsConfig := ...
  client := s3.NewFromConfig(awsConfig)
  presignClient := s3.NewPresignClient(client)
}

With these two clients created I can now create my presigned upload and download URLs.

Upload

To create an UploadURL I need to specify the destination for the file. It’s common to also include an expiration for the link so I have done so below. There are a lot of options to provide when creating an upload url which are specified here: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3#PutObjectInput. I will put the Upload and Download code into their own functions to keep things simple:

func UploadURL(client *s3.PresignClient, fileKey string) (string, error) {
  expiration := time.Hour * 2
  putObjectArgs := s3.PutObjectInput{
    Bucket: "my-bucket",
    Key: &fileKey,
  }
  res, err := client.PresignPutObject(
    context.Background(),
    &putObjectArgs,
    s3.WithPresignExpires(expiration)
  )
  if err != nil {
    return "", err
  }
  return res.URL, nil
}

My new UploadURL function takes a presign client and a fileKey. It returns a string or an error. URLs created by this function expire in two hours, and all files will be saved into the my-bucket bucket.

Download

To create a DownloadURL is almost identical to the process for an Upload. There are also a lot of options for download url creation, which can be found here: https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/s3#GetObjectInput. I like to make my downloading files provide a nice filename, so I will include a content disposition as well as the required parameters. Now for the example:

func DownloadURL(client *s3.PresignClient, fileKey *string, filename string) (string, error) {
  expiration := time.Now().Add(time.Hour * 12)
  disposition := fmt.Sprintf("attachment; filename=\"%v\"", filename)
  getObjectArgs := s3.GetObjectInput{
    Bucket: "my-bucket",
    ResponseExpires: &expiration,
    Key: fileKey,
    ResponseContentDisposition: disposition
  }
  
  res, err := client.PresignGetObject(context.Background(), &getObjectArgs)
  if err != nil {
    return "", err
  }
  return res.URL, nil
}

URLs created by this function will expire in twelve hours (as opposed to the two hours I used earlier). The most confusing part of the AWS SDK is building the configuration and appropriate clients. Once that is sorted out the download and upload logic is very straightforward.

Full example

The example below will create an upload url and a download url and print them out.

package main

import (
  "context"
  "fmt"
  "time"

  "github.com/aws/aws-sdk-go-v2/aws"
  "github.com/aws/aws-sdk-go-v2/config"
  "github.com/aws/aws-sdk-go-v2/credentials"
  "github.com/aws/aws-sdk-go-v2/service/s3"
)

func main() {
  // Create an aws configuration
  awsConfig := config.LoadDefaultConfig(
    context.Background(),
    config.WithCredentialsProvider(
      credentials.NewStaticCredentialsProvider(
        "ACCESS_KEY_ID",
        "SECRET_ACCESS_KEY",
        "SESSION_TOKEN", // This is optional
      ),
    ),
  )
  // Create the aws clients
  client := s3.NewFromConfig(awsConfig)
  presignClient := s3.NewPresignClient(client)
  
  // Create the URLs
  uploadURL, err := UploadURL(&presignClient, "myfile.txt")
  if err != nil {
    fmt.Println("Whoops, couldn't create upload url!")
  }
  downloadURL, err := DownloadURL(&presignClient, "some-file-key", "friendlyname.txt")
  if err != nil {
    fmt.Println("Whoops, couldn't create download url!")
  }
  // Print the URLs out
  fmt.Printf("Upload URL: %v", uploadURL)
  fmt.Printf("Download URL: %v", downloadURL) 
}

// UploadURL creates a presigned url for uploading a file at the
// destination (fileKey) in the my-bucket bucket
func UploadURL(client *s3.PresignClient, fileKey string) (string, error) {
  expiration := time.Now().Add(time.Hour * 2)
  putObjectArgs := s3.PutObjectInput{
    Bucket: "my-bucket",
    Key: &fileKey,
    Expires: &expiration,
  }
  res, err := client.PresignPutObject(context.Background(), &putObjectArgs)
  if err != nil {
    return "", err
  }
  return res.URL, nil
}

// DownloadURL creates a presigned download URL for the given file (stored
// at `fileKey` in the my-bucket bucket) and gives it a download name of
// `filename`
func DownloadURL(client *s3.PresignClient, fileKey *string, filename string) (string, error) {
  expiration := time.Now().Add(time.Hour * 12)
  disposition := fmt.Sprintf("attachment; filename=\"%v\"", filename)
  getObjectArgs := s3.GetObjectInput{
    Bucket: "my-bucket",
    ResponseExpires: &expiration,
    Key: fileKey,
    ResponseContentDisposition: disposition
  }
  
  res, err := client.PresignGetObject(context.Background(), &getObjectArgs)
  if err != nil {
    return "", err
  }
  return res.URL, nil
}

Leave a Reply

Your email address will not be published.