Skip to main content

AWS Go SDK

This guide assumes that you have followed the steps in the Getting Started guide, and have the access keys available.

You may continue to use the AWS Go SDK as you normally would, but with the endpoint set to https://fly.storage.tigris.dev.

This example uses the AWS Go SDK v2 and reads the default credentials file or the environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.

Getting started

This shows off a few basic operations with the AWS Go SDK such as PutObject, ListObjectsV2, and GetObject.

package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"time"

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

func main() {
flag.Parse()
if flag.NArg() != 1 {
log.Fatalf("usage: %s <bucket>", flag.Arg(0))
}

bucketName := flag.Arg(0)

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

sdkConfig, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Printf("Couldn't load default configuration. Here's why: %v", err)
return
}

// Create S3 service client
svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev")
o.Region = "auto"
})

// List buckets
result, err := svc.ListBuckets(ctx, &s3.ListBucketsInput{})
if err != nil {
log.Printf("Unable to list buckets. Here's why: %v", err)
return
}

fmt.Println("Buckets:")

for _, b := range result.Buckets {
fmt.Printf("* %s created on %s\n",
aws.ToString(b.Name), aws.ToTime(b.CreationDate))
bucketName = aws.ToString(b.Name)
}

// Upload file
fmt.Println("Upload file:")

os.WriteFile("bar.txt", []byte("Hello, World!"), 0644)

file, err := os.Open("bar.txt")
if err != nil {
log.Printf("Couldn't open file to upload. Here's why: %v\n", err)
return
} else {
defer file.Close()
_, err = svc.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String("bar.txt"),
Body: file,
})
if err != nil {
log.Printf("Couldn't upload file. Here's why: %v\n", err)
}
}

fmt.Println("File uploaded.")

// List objects
fmt.Println("List objects:")

resp, err := svc.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
Bucket: aws.String(bucketName),
})
if err != nil {
log.Printf("Unable to list objects. Here's why: %v", err)
return
}

if len(resp.Contents) == 0 {
log.Printf("No objects found in bucket: %s", bucketName)
return
}

fmt.Println("Objects:")
for _, obj := range resp.Contents {
fmt.Printf("* %s\n", aws.ToString(obj.Key))
}

// Download file
fmt.Println("Download file:")

file, err = os.Create("bar.txt")
if err != nil {
log.Printf("Couldn't create file to download. Here's why: %v", err)
return
}
defer file.Close()

_, err = svc.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String("bar.txt"),
})
if err != nil {
log.Printf("Couldn't download file. Here's why: %v", err)
}
}

Run it with:

go run main.go bucket-name

Conditional operations

Below is an example of how to use the AWS Go SDK to perform conditional operations on objects in Tigris. The example reads an object, modifies it, and then writes it back to the bucket. The write operation is conditional on the object not being modified since it was read.

package main

import (
"bytes"
"context"
"flag"
"io"
"log"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go/transport/http"
)

func WithHeader(key, value string) func(*s3.Options) {
return func(options *s3.Options) {
options.APIOptions = append(options.APIOptions, http.AddHeaderValue(key, value))
}
}

func IfMatch(value string) func(*s3.Options) {
return WithHeader("If-Match", value)
}

func main() {
flag.Parse()

if flag.NArg() != 1 {
log.Fatalf("usage: %s <bucket>", flag.Arg(0))
}
bucketName := flag.Arg(0)
keyName := "mykey"

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Printf("Couldn't load default configuration. Here's why: %v\n", err)
return
}

// Create S3 service client
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev")
o.Region = "auto"
})

// put an object into the bucket as a starting point
_, err = client.PutObject(ctx,
&s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(keyName),
Body: bytes.NewReader([]byte("hello world")),
},
WithHeader("x-tigris-cas", "true"),
)
if err != nil {
log.Fatalf("unable to put object: %v", err)
}

// read the object atomically
out, err := client.GetObject(ctx,
&s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(keyName),
},
WithHeader("x-tigris-cas", "true"),
)
if err != nil {
log.Fatalf("unable to read object: %v", err)
}

body, err := io.ReadAll(out.Body)
if err != nil {
log.Fatalf("unable to read object body: %v", err)
}

// write the object only if the etag matches the one we read
out1, err := client.PutObject(ctx,
&s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(keyName),
Body: bytes.NewBuffer(body),
},
IfMatch(*out.ETag),
)
if err != nil {
log.Fatalf("unable to put object, %v", err)
}
log.Printf("mykey etag is %s", *out1.ETag)
}

Run it with:

go run main.go bucket-name

Using presigned URLs

Presigned URLs can be used with the AWS Go SDK as follows:

package main

import (
"context"
"flag"
"fmt"
"log"
"time"

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

// Client encapsulates the S3 SDK presign client and provides methods to presign requests.
type Client struct {
PresignClient *s3.PresignClient
}

// GetObject makes a presigned request that can be used to get an object from a bucket.
func (p *Client) GetObject(
ctx context.Context,
bucket string,
key string,
expiry time.Duration,
) (*v4.PresignedHTTPRequest, error) {
request, err := p.PresignClient.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiry
})
if err != nil {
log.Printf("Couldn't get a presigned request to get %v:%v. Here's why: %v\n",
bucket, key, err)
}
return request, err
}

// PutObject makes a presigned request that can be used to put an object in a bucket.
func (p *Client) PutObject(
ctx context.Context,
bucket string,
object string,
expiry time.Duration,
) (*v4.PresignedHTTPRequest, error) {
request, err := p.PresignClient.PresignPutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(object),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiry
})
if err != nil {
log.Printf("Couldn't get a presigned request to put %v:%v. Here's why: %v\n",
bucket, object, err)
}
return request, err
}

// DeleteObject makes a presigned request that can be used to delete an object from a bucket.
func (p *Client) DeleteObject(ctx context.Context, bucket string, object string, expiry time.Duration) (*v4.PresignedHTTPRequest, error) {
request, err := p.PresignClient.PresignDeleteObject(ctx, &s3.DeleteObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(object),
}, func(opts *s3.PresignOptions) {
opts.Expires = expiry
})
if err != nil {
log.Printf("Couldn't get a presigned request to delete object %v. Here's why: %v\n", object, err)
}
return request, err
}

func main() {
flag.Parse()
if flag.NArg() != 1 {
log.Fatalf("usage: %s <bucket>", flag.Arg(0))
}

bucketName := flag.Arg(0)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

sdkConfig, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Printf("Couldn't load default configuration. Here's why: %v\n", err)
return
}

// Create S3 service client
svc := s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev")
o.Region = "auto"
})

// Presigning a request
ps := s3.NewPresignClient(svc)
presigner := &Client{PresignClient: ps}

// Presigned URL to upload an object to the bucket
presignedPutReq, err := presigner.PutObject(ctx, bucketName, "bar.txt", 60*time.Minute)
if err != nil {
log.Printf("Couldn't get a presigned request to put bar.txt. Here's why: %v\n", err)
} else {
fmt.Printf("Presigned URL for PUT: %s\n", presignedPutReq.URL)
}

// Presigned URL to download an object from the bucket
presignedGetReq, err := presigner.GetObject(ctx, bucketName, "bar.txt", 60*time.Minute)
if err != nil {
log.Printf("Couldn't get a presigned request to get bar.txt. Here's why: %v\n", err)
} else {
fmt.Printf("Presigned URL for GET: %s\n", presignedGetReq.URL)
}

// Presigned URL to delete an object from the bucket
presignedDeleteReq, err := presigner.DeleteObject(ctx, bucketName, "bar.txt", 60*time.Minute)
if err != nil {
log.Printf("Couldn't get a presigned request to delete bar.txt. Here's why: %v\n", err)
} else {
fmt.Printf("Presigned URL for DELETE: %s\n", presignedDeleteReq.URL)
}
}

You can now use the URL returned by the presignedPutReq.URL and presignedGetReq.URL to upload or download objects.

Run it with:

go run main.go bucket-name

Object Regions

Below is an example of how to use the AWS Go SDK to restrict object region to Europe only (fra region).

package main

import (
"bytes"
"context"
"crypto/rand"
"flag"
"fmt"
"io"
"log"
"strings"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/aws/smithy-go/transport/http"
)

func WithHeader(key, value string) func(*s3.Options) {
return func(options *s3.Options) {
options.APIOptions = append(options.APIOptions, http.AddHeaderValue(key, value))
}
}

func putObjectToMultipleRegions(
ctx context.Context,
client *s3.Client,
bucket, key string,
data []byte,
regions []string,
) error {
_, err := client.PutObject(
ctx,
&s3.PutObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
ContentLength: aws.Int64(int64(len(data))),
},
// Restrict in Europe only
WithHeader("X-Tigris-Regions", strings.Join(regions, ",")),
)

return err
}

func putObjectUsingMultipartToMultipleRegions(
ctx context.Context,
client *s3.Client,
bucket, key string,
data []byte,
regions []string,
) error {
co, err := client.CreateMultipartUpload(
ctx,
&s3.CreateMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
},
// Restrict in Europe only
WithHeader("X-Tigris-Regions", strings.Join(regions, ",")),
)
if err != nil {
return err
}

uo, err := client.UploadPart(
ctx,
&s3.UploadPartInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
UploadId: co.UploadId,
PartNumber: aws.Int32(1),
ContentLength: aws.Int64(int64(len(data))),
},
)
if err != nil {
return err
}

_, err = client.CompleteMultipartUpload(
ctx,
&s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
UploadId: co.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: []types.CompletedPart{
{ETag: uo.ETag, PartNumber: aws.Int32(1)},
},
},
},
)

return err
}

func main() {
flag.Parse()
if flag.NArg() != 1 {
log.Fatalf("usage: %s <bucket>", flag.Arg(0))
}

bucketName := flag.Arg(0)
keyName := "mykey"

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Printf("Couldn't load default configuration. Here's why: %v\n", err)
return
}

// Create S3 service client
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev")
o.Region = "auto"
})

var (
randData = make([]byte, 16384)
)

_, _ = rand.Read(randData)

fmt.Println("Put object to the EU regions")

// Put an object to the EU regions
err = putObjectToMultipleRegions(ctx, client, bucketName, keyName, randData, []string{"eur"})
if err != nil {
log.Fatalf("unable to write object: %v", err)
}

_, _ = rand.Read(randData)

fmt.Println("Put object to the US regions")

// Put an object to the US regions using multipart upload
err = putObjectUsingMultipartToMultipleRegions(ctx, client, bucketName, keyName, randData, []string{"usa"})
if err != nil {
log.Fatalf("unable to write object: %v", err)
}

fmt.Println("Read the object back")

// read the object back
out, err := client.GetObject(context.TODO(),
&s3.GetObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(keyName),
},
)
if err != nil {
log.Fatalf("unable to read object: %v", err)
}

_, err = io.ReadAll(out.Body)
if err != nil {
log.Fatalf("unable to read object body: %v", err)
}
}

Run it with:

go run main.go bucket-name

Metadata Querying

Below is an example of how to use metadata querying with the AWS Go SDK.

package main

import (
"bytes"
"context"
"flag"
"fmt"
"log"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/smithy-go/transport/http"
)

func WithHeader(key, value string) func(*s3.Options) {
return func(options *s3.Options) {
options.APIOptions = append(options.APIOptions, http.AddHeaderValue(key, value))
}
}

func main() {
flag.Parse()
if flag.NArg() != 1 {
log.Fatalf("usage: %s <bucket>", flag.Arg(0))
}

bucketName := flag.Arg(0)
keyName := "examplefile.js"

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
log.Printf("Couldn't load default configuration. Here's why: %v\n", err)
return
}

// Create S3 service client
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String("https://fly.storage.tigris.dev")
o.Region = "auto"
})

contentType := "text/javascript"

fmt.Println("Putting object to the bucket:", keyName)

// put a javascript file in the bucket
_, err = client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(bucketName),
Key: aws.String(keyName),
Body: bytes.NewBuffer([]byte("console.log('Hello, World!')")),
ContentType: aws.String(contentType),
})
if err != nil {
log.Fatalf("Unable to put object. Here's why: %v", err)
}

fmt.Println("Listing objects with Content-Type:", contentType)

resp, err := client.ListObjectsV2(ctx,
&s3.ListObjectsV2Input{
Bucket: aws.String(bucketName),
},
WithHeader("X-Tigris-Query", fmt.Sprintf("`Content-Type` = %q", contentType)),
)
if err != nil {
log.Fatalf("unable to list objects: %v", err)
}

if len(resp.Contents) == 0 {
log.Printf("No objects found with Content-Type: %s", contentType)
return
}

for _, obj := range resp.Contents {
fmt.Println("*", *obj.Key)
}
}

Object Notifications

If you want to be notified when an object is created or deleted in a bucket, use object notifications. This example shows you how to implement a server that listens for and parses object notifications so you can take action when an object is created or deleted.

package main

import (
"crypto/subtle"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
)

// ObjectNotificationReq is the parent object for object notification events.
type ObjectNotificationReq struct {
Events []*ObjectNotificationEvent `json:"events"`
}

// ObjectNotificationEvent contains information about an object being created or deleted.
type ObjectNotificationEvent struct {
EventVersion string `json:"eventVersion"`
EventSource string `json:"eventSource"`
EventName string `json:"eventName"`
EventTime string `json:"eventTime"`
Bucket string `json:"bucket"`
Object *EventObject `json:"object"`
}

// EventObject contains the most important information about an object.
type EventObject struct {
Key string `json:"key"`
Size int32 `json:"size"`
ETag string `json:"eTag"`
}

// eventReciever is a simple http handler that receives object notification events.
//
// This does not do any validation or authentication on any incoming requests.
func eventReceiver(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Error reading request body", http.StatusInternalServerError)
return
}
defer r.Body.Close()

var req ObjectNotificationReq
err = json.Unmarshal(body, &req)
if err != nil {
http.Error(w, "Error unmarshalling request body", http.StatusInternalServerError)
return
}

fmt.Println("Events:")
for _, event := range req.Events {
fmt.Printf("time: %v, event: %v, bucket: %v, key: %v\n", event.EventTime, event.EventName, event.Bucket, event.Object.Key)
}

fmt.Fprint(w, "ok")
}

// basicAuth is a HTTP middleware that checks for basic auth credentials in incoming requests against a static username and password.
//
// Usage:
//
// http.HandleFunc("/basic-auth", basicAuth("user", "pass", eventReceiver))
func basicAuth(username, password string, next http.HandlerFunc) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, pass, ok := r.BasicAuth()
if !ok {
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

if subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}

next.ServeHTTP(w, r)
})
}

// tokenAuth is an HTTP middleware that checks if incoming requests have an API token matching a statically defined value.
//
// Usage:
//
// http.HandleFunc("/token-auth", tokenAuth("secret-token-pass", eventReceiver))
func tokenAuth(token string, next http.HandlerFunc) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if auth == "" || !strings.HasPrefix(auth, "Bearer ") || subtle.ConstantTimeCompare([]byte(strings.TrimPrefix(auth, "Bearer ")), []byte(token)) != 1 {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
fmt.Println("token auth successful")

next.ServeHTTP(w, r)
})
}

func main() {
http.HandleFunc("/no-auth", eventReceiver)
http.Handle("/basic-auth", basicAuth("user", "pass", eventReceiver))
http.Handle("/token-auth", tokenAuth("secret-token-pass", eventReceiver))

log.Println("Server running on http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}