In this example we will be using a pre-signed URL for AWS S3 file upload and download. URL will be valid for 15 minutes by default.


Files


aws/config.go


My S3 bucket is in an another account hence I am using STS client.


package aws

import (
"context"

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

func NewCrossAccountConfigWithRole(ctx context.Context, roleARN string) (aws.Config, error) {
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return aws.Config{}, err
}

stsClient := sts.NewFromConfig(cfg)
stsCreds := stscreds.NewAssumeRoleProvider(stsClient, roleARN)

cfg.Credentials = aws.NewCredentialsCache(stsCreds)

return cfg, nil
}

aws/args.go


package aws

import "time"

type PresignedURLArgs struct {
Bucket string
Key string
Expiry time.Duration
}

aws/s3.go


package aws

import (
"context"
"fmt"

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

type S3 struct {
client *s3.Client
signer *s3.PresignClient
}

func NewS3(cfg aws.Config) S3 {
client := s3.NewFromConfig(cfg)

return S3{
client: client,
signer: s3.NewPresignClient(client),
}
}

// PresignedUploadURL creates a presigned request URL that can be used to upload
// an object in a bucket. The URL is valid for the specified number of seconds.
func (s S3) PresignedUploadURL(ctx context.Context, args PresignedURLArgs) (string, error) {
input := s3.PutObjectInput{
Bucket: aws.String(args.Bucket),
Key: aws.String(args.Key),
}

expiry := func(opts *s3.PresignOptions) {
opts.Expires = args.Expiry
}

req, err := s.signer.PresignPutObject(ctx, &input, expiry)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}

return req.URL, nil
}

// PresignedDownloadURL creates a presigned request URL that can be used to download
// an object from a bucket. The URL is valid for the specified number of seconds.
func (s S3) PresignedDownloadURL(ctx context.Context, args PresignedURLArgs) (string, error) {
input := s3.GetObjectInput{
Bucket: aws.String(args.Bucket),
Key: aws.String(args.Key),
}

expiry := func(opts *s3.PresignOptions) {
opts.Expires = args.Expiry
}

req, err := s.signer.PresignGetObject(ctx, &input, expiry)
if err != nil {
return "", fmt.Errorf("failed to create request: %w", err)
}

return req.URL, nil
}

api/upload.go


package api

import (
"context"
"fmt"
"net/http"
"strings"
)

func Upload(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, strings.NewReader(`{"users":["a,b,c"]}`))
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}

if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response: %s", res.Status)
}

return nil
}

api/download.go


package api

import (
"context"
"fmt"
"io"
"net/http"
"os"
)

func Download(ctx context.Context, url, path string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("failed to create request: %w", err)
}

res, err := http.DefaultTransport.RoundTrip(req)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
if res.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected response: %s", res.Status)
}
defer res.Body.Close()

file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
}
defer file.Close()

// In a memory efficient manner, read maximum 32KB at a time from input and
// write it to output then repeat until the EOL.
if _, err = io.Copy(file, res.Body); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}

return nil
}

main.go


package main

import (
"aws/api"
"aws/aws"
"context"
"log"
"time"
)

var (
bucket = "inanzzz-development"
key = "users.json"
expiry = time.Second * 10
)

func main() {
ctx := context.Background()

awsConfig, err := aws.NewCrossAccountConfigWithRole(ctx, "arn:aws:iam::1234567890:role/services")
if err != nil {
log.Fatal(err)
}

s3 := aws.NewS3(awsConfig)

// UPLOAD ------------------------------------------------------------------
uploadArgs := aws.PresignedURLArgs{
Bucket: bucket,
Key: key,
Expiry: expiry,
}

uploadURL, err := s3.PresignedUploadURL(ctx, uploadArgs)
if err != nil {
log.Fatalln(err)
}

if err := api.Upload(ctx, uploadURL); err != nil {
log.Fatalln(err)
}

// DOWNLOAD ----------------------------------------------------------------
downloadArgs := aws.PresignedURLArgs{
Bucket: bucket,
Key: key,
Expiry: expiry,
}

downloadURL, err := s3.PresignedDownloadURL(ctx, downloadArgs)
if err != nil {
log.Fatalln(err)
}

if err := api.Download(ctx, downloadURL, "downloaded-users.json"); err != nil {
log.Fatalln(err)
}
}