diff --git a/gov2/s3/actions/presigner.go b/gov2/s3/actions/presigner.go index a7f2b4f1508..40bd93d1a11 100644 --- a/gov2/s3/actions/presigner.go +++ b/gov2/s3/actions/presigner.go @@ -83,4 +83,22 @@ func (presigner Presigner) DeleteObject(bucketName string, objectKey string) (*v } // snippet-end:[gov2.s3.PresignDeleteObject] + +// snippet-start:[gov2.s3.PresignPostObject] + +func (presigner Presigner) PresignPostObject(bucketName string, objectKey string, lifetimeSecs int64) (*s3.PresignedPostRequest, error) { + request, err := presigner.PresignClient.PresignPostObject(context.TODO(), &s3.PutObjectInput{ + Bucket: aws.String(bucketName), + Key: aws.String(objectKey), + }, func(options *s3.PresignPostOptions) { + options.Expires = time.Duration(lifetimeSecs) * time.Second + }) + if err != nil { + log.Printf("Couldn't get a presigned post request to put %v:%v. Here's why: %v\n", bucketName, objectKey, err) + } + return request, nil +} + +// snippet-end:[gov2.s3.PresignPostObject] + // snippet-end:[gov2.s3.Presigner.complete] diff --git a/gov2/s3/cmd/main.go b/gov2/s3/cmd/main.go index 4def4165d1e..ff5fd603b0b 100644 --- a/gov2/s3/cmd/main.go +++ b/gov2/s3/cmd/main.go @@ -20,12 +20,12 @@ import ( // // `-scenario` can be one of the following: // -// * `getstarted` - Runs the interactive get started scenario that shows you how to use -// Amazon Simple Storage Service (Amazon S3) actions to work with -// S3 buckets and objects. -// * `presigning` - Runs the interactive presigning scenario that shows you how to -// get presigned requests that contain temporary credentials -// and can be used to make requests from any HTTP client. +// - `getstarted` - Runs the interactive get started scenario that shows you how to use +// Amazon Simple Storage Service (Amazon S3) actions to work with +// S3 buckets and objects. +// - `presigning` - Runs the interactive presigning scenario that shows you how to +// get presigned requests that contain temporary credentials +// and can be used to make requests from any HTTP client. func main() { scenarioMap := map[string]func(sdkConfig aws.Config){ "getstarted": runGetStartedScenario, diff --git a/gov2/s3/go.sum b/gov2/s3/go.sum index 5fc541a206b..b0bec5e5026 100644 --- a/gov2/s3/go.sum +++ b/gov2/s3/go.sum @@ -40,21 +40,7 @@ github.com/awsdocs/aws-doc-sdk-examples/gov2/demotools v0.0.0-20240907001412-a93 github.com/awsdocs/aws-doc-sdk-examples/gov2/demotools v0.0.0-20240907001412-a9375541143b/go.mod h1:iBzksyiv5HVU+cymGDQbbvcecca+rsARJlDFL8np8oE= github.com/awsdocs/aws-doc-sdk-examples/gov2/testtools v0.0.0-20240907001412-a9375541143b h1:UmPy4pArM7SIhTX2Xn5bhOkgI9onSUQ1Y9fxgDJ3pHU= github.com/awsdocs/aws-doc-sdk-examples/gov2/testtools v0.0.0-20240907001412-a9375541143b/go.mod h1:9Oj/8PZn3D5Ftp/Z1QWrIEFE0daERMqfJawL9duHRfc= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/gov2/s3/scenarios/scenario_presigning.go b/gov2/s3/scenarios/scenario_presigning.go index 3ea585ef99e..3a1398be85b 100644 --- a/gov2/s3/scenarios/scenario_presigning.go +++ b/gov2/s3/scenarios/scenario_presigning.go @@ -4,9 +4,11 @@ package scenarios import ( + "bytes" "fmt" "io" "log" + "mime/multipart" "net/http" "os" "strings" @@ -23,6 +25,7 @@ import ( // unit testing. type IHttpRequester interface { Get(url string) (resp *http.Response, err error) + Post(url, contentType string, body io.Reader) (resp *http.Response, err error) Put(url string, contentLength int64, body io.Reader) (resp *http.Response, err error) Delete(url string) (resp *http.Response, err error) } @@ -33,6 +36,15 @@ type HttpRequester struct{} func (httpReq HttpRequester) Get(url string) (resp *http.Response, err error) { return http.Get(url) } +func (httpReq HttpRequester) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + postRequest, err := http.NewRequest("POST", url, body) + if err != nil { + return nil, err + } + postRequest.Header.Set("Content-Type", contentType) + return http.DefaultClient.Do(postRequest) +} + func (httpReq HttpRequester) Put(url string, contentLength int64, body io.Reader) (resp *http.Response, err error) { putRequest, err := http.NewRequest("PUT", url, body) if err != nil { @@ -51,6 +63,43 @@ func (httpReq HttpRequester) Delete(url string) (resp *http.Response, err error) // snippet-end:[gov2.s3.IHttpRequester.helper] +// snippet-start:[gov2.s3.MultipartUpload.helper] +func sendMultipartRequest(url string, fields map[string]string, file *os.File, filePath string, httpRequester IHttpRequester) (*http.Response, error) { + // Create a buffer to hold the multipart data + var requestBody bytes.Buffer + writer := multipart.NewWriter(&requestBody) + + // Add form fields + for key, val := range fields { + err := writer.WriteField(key, val) + if err != nil { + return nil, err + } + } + + // Always has to be named like this, and always has to be the last one + fileField := "file" + part, err := writer.CreateFormFile(fileField, filePath) + if err != nil { + return nil, err + } + _, err = io.Copy(part, file) + if err != nil { + return nil, err + } + + // Close the writer to finalize the multipart message + err = writer.Close() + if err != nil { + return nil, err + } + + // make the request + return httpRequester.Post(url, writer.FormDataContentType(), &requestBody) +} + +// snippet-end:[gov2.s3.MultipartUpload.helper] + // snippet-start:[gov2.s3.Scenario_Presigning] // RunPresigningScenario is an interactive example that shows you how to get presigned @@ -76,7 +125,7 @@ func (httpReq HttpRequester) Delete(url string) (resp *http.Response, err error) func RunPresigningScenario(sdkConfig aws.Config, questioner demotools.IQuestioner, httpRequester IHttpRequester) { defer func() { if r := recover(); r != nil { - fmt.Printf("Something went wrong with the demo.") + fmt.Printf("Something went wrong with the demo") } }() @@ -159,6 +208,24 @@ func RunPresigningScenario(sdkConfig aws.Config, questioner demotools.IQuestione log.Println(string(downloadBody[:100])) log.Println(strings.Repeat("-", 88)) + log.Println("Now we'll create a new request to put the same object using a presigned post request") + presignPostRequest, err := presigner.PresignPostObject(bucketName, uploadKey, 60) + if err != nil { + panic(err) + } + log.Printf("Got a presigned post request to url %v with values %v\n", presignPostRequest.URL, presignPostRequest.Values) + log.Println("Using net/http multipart to send the request...") + uploadFile, err = os.Open(uploadFilename) + if err != nil { + panic(err) + } + defer uploadFile.Close() + multiPartResponse, err := sendMultipartRequest(presignPostRequest.URL, presignPostRequest.Values, uploadFile, uploadKey, httpRequester) + if err != nil { + panic(err) + } + log.Printf("Presign post object %v with presigned URL returned %v.", uploadKey, multiPartResponse.StatusCode) + 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) diff --git a/gov2/s3/scenarios/scenario_presigning_test.go b/gov2/s3/scenarios/scenario_presigning_test.go index b76c6acc6f7..26331aa3b68 100644 --- a/gov2/s3/scenarios/scenario_presigning_test.go +++ b/gov2/s3/scenarios/scenario_presigning_test.go @@ -27,6 +27,9 @@ type MockHttpRequester struct { func (httpReq MockHttpRequester) Get(url string) (resp *http.Response, err error) { return &http.Response{Status: "Testing", StatusCode: 200, Body: httpReq.GetBody}, nil } +func (httpReq MockHttpRequester) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + return &http.Response{Status: "Testing", StatusCode: 200}, nil +} func (httpReq MockHttpRequester) Put(url string, contentLength int64, body io.Reader) (resp *http.Response, err error) { return &http.Response{Status: "Testing", StatusCode: 200}, nil } @@ -68,6 +71,7 @@ func (scenTest *PresigningScenarioTest) SetupDataAndStubs() []testtools.Stub { stubList = append(stubList, stubs.StubCreateBucket(bucketName, testConfig.Region, nil)) stubList = append(stubList, stubs.StubPresignedRequest("PUT", bucketName, objectKey, nil)) stubList = append(stubList, stubs.StubPresignedRequest("GET", bucketName, objectKey, nil)) + stubList = append(stubList, stubs.StubPresignedRequest("POST", bucketName, objectKey, nil)) stubList = append(stubList, stubs.StubPresignedRequest("DELETE", bucketName, objectKey, nil)) return stubList diff --git a/gov2/s3/stubs/bucket_basics_stubs.go b/gov2/s3/stubs/bucket_basics_stubs.go index 37ded51750c..d1b3620e3f9 100644 --- a/gov2/s3/stubs/bucket_basics_stubs.go +++ b/gov2/s3/stubs/bucket_basics_stubs.go @@ -208,6 +208,16 @@ func StubPresignedRequest(method string, bucketName string, objectKey string, ra case "DELETE": opName = "DeleteObject" input = &s3.DeleteObjectInput{Bucket: aws.String(bucketName), Key: aws.String(objectKey)} + case "POST": + opName = "PutObject" + input = &s3.PutObjectInput{Bucket: aws.String(bucketName), Key: aws.String(objectKey)} + // special case since the object here is different + return testtools.Stub{ + OperationName: opName, + Input: input, + Output: &s3.PresignedPostRequest{URL: "test-url", Values: map[string]string{}}, + Error: raiseErr, + } } return testtools.Stub{ OperationName: opName,