04/06/2023 - AWS, GO
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.
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
}
package aws
import "time"
type PresignedURLArgs struct {
Bucket string
Key string
Expiry time.Duration
}
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
}
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
}
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
}
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)
}
}