Skip to content

Instantly share code, notes, and snippets.

@sethp
Created August 28, 2023 21:58
Show Gist options
  • Save sethp/68b2895a006ab749dff57cf949165d44 to your computer and use it in GitHub Desktop.
Save sethp/68b2895a006ab749dff57cf949165d44 to your computer and use it in GitHub Desktop.
signed multipart uploads demo
package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/http"
"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/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/awsdocs/aws-doc-sdk-examples/gov2/demotools"
"github.com/awsdocs/aws-doc-sdk-examples/gov2/s3/actions"
)
func run(ctx context.Context) error {
var (
questioner = demotools.NewQuestioner()
)
var (
uploadFilename, uploadKey = "go.sum", "test-object"
bucketName = "my-bucket-19e02b5"
)
s3Client, presigner, err := configureClients(ctx)
if err != nil {
return err
}
uploadFile, err := os.Open(uploadFilename)
if err != nil {
return err
}
defer uploadFile.Close()
upload, err := s3Client.CreateMultipartUpload(ctx, &s3.CreateMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(uploadKey),
})
if err != nil {
return err
}
presignedPutRequest, err := presigner.PresignClient.PresignUploadPart(ctx, &s3.UploadPartInput{
Bucket: aws.String(bucketName),
Key: aws.String(uploadKey),
UploadId: upload.UploadId,
// only one part to demonstrate the behavior, in reality chunking on time/size boundaries would be better
PartNumber: 1,
}, func(opts *s3.PresignOptions) {
// no idea if this is the amount of time to start or finish the request; I hope it's start
opts.Expires = time.Duration(60 * int64(time.Second))
})
if err != nil {
return err
}
log.Printf("Got a presigned %v request to URL:\n\t%v\n", presignedPutRequest.Method,
presignedPutRequest.URL)
log.Println("Using net/http to send the request...")
info, err := uploadFile.Stat()
if err != nil {
return err
}
putResponse, err := func() (*http.Response, error) {
putRequest, err := http.NewRequest(presignedPutRequest.Method, presignedPutRequest.URL, uploadFile)
if err != nil {
return nil, err
}
putRequest.ContentLength = info.Size()
return http.DefaultClient.Do(putRequest)
}()
if err != nil {
return err
}
log.Printf("%v object %v with presigned URL returned %v.", presignedPutRequest.Method,
uploadKey, putResponse.StatusCode)
if putResponse.StatusCode > 399 {
body, err := io.ReadAll(putResponse.Body)
return errors.Join(err, errors.New("bad response: "+string(body)))
}
_, err = s3Client.CompleteMultipartUpload(ctx, &s3.CompleteMultipartUploadInput{
Bucket: aws.String(bucketName),
Key: aws.String(uploadKey),
UploadId: upload.UploadId,
MultipartUpload: &types.CompletedMultipartUpload{
Parts: []types.CompletedPart{{
ETag: &putResponse.Header[http.CanonicalHeaderKey("etag")][0],
PartNumber: 1,
}},
},
})
if err != nil {
return err
}
log.Printf("Let's presign a request to download the object.")
questioner.Ask("Press Enter when you're ready.")
presignedGetRequest, err := presigner.GetObject(bucketName, uploadKey, 60)
if err != nil {
return err
}
log.Printf("Got a presigned %v request to URL:\n\t%v\n", presignedGetRequest.Method,
presignedGetRequest.URL)
log.Println("Using net/http to send the request...")
getResponse, err := http.Get(presignedGetRequest.URL)
if err != nil {
return err
}
log.Printf("%v object %v with presigned URL returned %v.", presignedGetRequest.Method,
uploadKey, getResponse.StatusCode)
defer getResponse.Body.Close()
downloadBody, err := io.ReadAll(getResponse.Body)
if err != nil {
return err
}
fmt.Printf("Downloaded %v bytes\n", len(downloadBody))
log.Println("Let's presign a request to delete the object.")
questioner.Ask("Press Enter when you're ready.")
presignedDelRequest, err := presigner.DeleteObject(bucketName, uploadKey)
if err != nil {
return err
}
log.Printf("Got a presigned %v request to URL:\n\t%v\n", presignedDelRequest.Method,
presignedDelRequest.URL)
log.Println("Using net/http to send the request...")
delResponse, err := func() (*http.Response, error) {
delRequest, err := http.NewRequest(presignedDelRequest.Method, presignedDelRequest.URL, nil)
if err != nil {
return nil, err
}
return http.DefaultClient.Do(delRequest)
}()
if err != nil {
return err
}
log.Printf("%v object %v with presigned URL returned %v.\n", presignedDelRequest.Method,
uploadKey, delResponse.StatusCode)
return nil
}
func configureClients(ctx context.Context) (s3Client *s3.Client, presigner actions.Presigner, err error) {
var (
sdkConfig aws.Config
provider = "s3"
)
if len(os.Args) > 1 {
provider = os.Args[1]
}
switch provider {
case "s3":
sdkConfig, err = config.LoadDefaultConfig(ctx, config.WithSharedConfigProfile("panoptesan"))
if err != nil {
return nil, actions.Presigner{}, err
}
case "b2":
// via https://help.backblaze.com/hc/en-us/articles/360047629713-Using-the-AWS-Go-SDK-with-B2
// and https://stackoverflow.com/questions/66137645/how-do-i-configure-s3forcepathstyle-with-aws-golang-v2-sdk
sdkConfig = aws.Config{
Region: "us-west-004",
Credentials: credentials.NewStaticCredentialsProvider(os.Getenv("B2_KEY"), os.Getenv("B2_SECRET"), ""),
EndpointResolverWithOptions: aws.EndpointResolverWithOptionsFunc(func(_, region string, _ ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{
PartitionID: "aws",
URL: fmt.Sprintf("https://s3.%s.backblazeb2.com", region),
SigningRegion: "us-east-1",
}, nil
}),
}
default:
return nil, actions.Presigner{}, fmt.Errorf("unrecognized provider: %s", provider)
}
s3Client = s3.NewFromConfig(sdkConfig, func(o *s3.Options) {
o.UsePathStyle = true
})
presigner = actions.Presigner{PresignClient: s3.NewPresignClient(s3Client)}
return
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err := run(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment