diff --git a/README.md b/README.md index 0422cf7..96a4f53 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,13 @@ s3 := simples3.New(Region, AWSAccessKey, AWSSecretKey) // obtain credentials from IAM attached to the instance. s3 := simples3.NewUsingIAM(Region) +// Note: Consider adding a testTxt.Seek(0, 0) +// in case you have read +// the body, as the pointer is shared by the library. + // File Upload is as simple as providing the following // details. -err := s3.FileUpload(simples3.UploadInput{ +resp, err := s3.FileUpload(simples3.UploadInput{ Bucket: AWSBucket, ObjectKey: "test.txt", ContentType: "text/plain", diff --git a/avatar.png b/avatar.png new file mode 100644 index 0000000..a71cae1 Binary files /dev/null and b/avatar.png differ diff --git a/simples3.go b/simples3.go index c090632..40cc6a1 100644 --- a/simples3.go +++ b/simples3.go @@ -8,6 +8,7 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/json" + "encoding/xml" "errors" "fmt" "io" @@ -40,6 +41,22 @@ type UploadInput struct { Body multipart.File } +// UploadResponse receives the following XML +// in case of success, since we set a 201 response from S3. +// Sample response: +// +// https://s3.amazonaws.com/link-to-the-file +// s3-bucket +// development/8614bd40-691b-4668-9241-3b342c6cf429/image.jpg +// "32-bit-tag" +// +type UploadResponse struct { + Location string `xml:"Location"` + Bucket string `xml:"Bucket"` + Key string `xml:"Key"` + ETag string `xml:"ETag"` +} + // DeleteInput is passed to FileDelete as a parameter. type DeleteInput struct { Bucket string @@ -133,7 +150,7 @@ func detectFileSize(body multipart.File) int64 { // FileUpload makes a POST call with the file written as multipart // and on successful upload, checks for 200 OK. -func (s3 *S3) FileUpload(u UploadInput) error { +func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { policies, err := s3.CreateUploadPolicies(UploadConfig{ UploadURL: fmt.Sprintf(s3.URIFormat, s3.Region, u.Bucket), BucketName: u.Bucket, @@ -141,12 +158,12 @@ func (s3 *S3) FileUpload(u UploadInput) error { ContentType: u.ContentType, FileSize: detectFileSize(u.Body), MetaData: map[string]string{ - "success_action_status": "200", + "success_action_status": "201", // returns XML doc on success }, }) if err != nil { - return err + return UploadResponse{}, err } var b bytes.Buffer @@ -154,28 +171,28 @@ func (s3 *S3) FileUpload(u UploadInput) error { for k, v := range policies.Form { if err := w.WriteField(k, v); err != nil { - return err + return UploadResponse{}, err } } fw, err := w.CreateFormFile("file", u.FileName) if err != nil { - return err + return UploadResponse{}, err } if _, err = io.Copy(fw, u.Body); err != nil { - return err + return UploadResponse{}, err } // Don't forget to close the multipart writer. // If you don't close it, your request will be missing the terminating boundary. if err := w.Close(); err != nil { - return err + return UploadResponse{}, err } // Now that you have a form, you can submit it to your handler. req, err := http.NewRequest("POST", policies.URL, &b) if err != nil { - return err + return UploadResponse{}, err } // Don't forget to set the content type, this will contain the boundary. req.Header.Set("Content-Type", w.FormDataContentType()) @@ -184,18 +201,25 @@ func (s3 *S3) FileUpload(u UploadInput) error { client := &http.Client{} res, err := client.Do(req) if err != nil { - return err + return UploadResponse{}, err } + defer res.Body.Close() // Check the response - if res.Status != "200 OK" { + if res.Status != "201 Created" { err = fmt.Errorf("status code: %s", res.Status) data, _ := ioutil.ReadAll(res.Body) log.Printf("response:%s\n", string(data)) - return err + return UploadResponse{}, err } - return nil + data, err := ioutil.ReadAll(res.Body) + if err != nil { + return UploadResponse{}, err + } + var ur UploadResponse + xml.Unmarshal(data, &ur) + return ur, nil } // FileDelete makes a DELETE call with the file written as multipart diff --git a/simples3_test.go b/simples3_test.go index 8cbcc38..8103d9e 100644 --- a/simples3_test.go +++ b/simples3_test.go @@ -11,6 +11,11 @@ func TestS3_FileUpload(t *testing.T) { return } defer testTxt.Close() + testPng, err := os.Open("avatar.png") + if err != nil { + return + } + defer testPng.Close() type fields struct { AccessKey string @@ -44,13 +49,37 @@ func TestS3_FileUpload(t *testing.T) { }, wantErr: false, }, + { + name: "Upload avatar.png", + fields: fields{ + AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"), + SecretKey: os.Getenv("AWS_S3_SECRET_KEY"), + Region: os.Getenv("AWS_S3_REGION"), + }, + args: args{ + UploadInput{ + Bucket: os.Getenv("AWS_S3_BUCKET"), + Bucket: "zerodha-testbucket", + ObjectKey: "xyz/image.png", + ContentType: "image/png", + FileName: "avatar.png", + Body: testPng, + }, + }, + wantErr: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { s3 := New(tt.fields.Region, tt.fields.AccessKey, tt.fields.SecretKey) - if err := s3.FileUpload(tt.args.u); (err != nil) != tt.wantErr { + resp, err := s3.FileUpload(tt.args.u) + if (err != nil) != tt.wantErr { t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr) } + // check for empty response + if (resp == UploadResponse{}) { + t.Errorf("S3.FileUpload() returned empty response, %v", resp) + } }) } }