From 35d7b10832be0eef7f0f58dffcb2bf7a9b821d97 Mon Sep 17 00:00:00 2001 From: Rohan Verma Date: Mon, 19 Jul 2021 12:33:48 +0530 Subject: [PATCH 1/3] fix: minor doc changes --- policy.go | 2 +- sign.go | 2 +- simples3.go | 14 +++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/policy.go b/policy.go index d4c037f..a0c9e8e 100644 --- a/policy.go +++ b/policy.go @@ -1,4 +1,4 @@ -// LICENSE MIT +// LICENSE BSD-2-Clause-FreeBSD // Copyright (c) 2018, Rohan Verma // Copyright (c) 2017, L Campbell // forked from: https://github.com/lye/s3/ diff --git a/sign.go b/sign.go index 9418d86..84a5216 100644 --- a/sign.go +++ b/sign.go @@ -1,4 +1,4 @@ -// LICENSE MIT +// LICENSE BSD-2-Clause-FreeBSD // Copyright (c) 2018, Rohan Verma // Copyright (C) 2012 Blake Mizerany // contains code from: github.com/bmizerany/aws4 diff --git a/simples3.go b/simples3.go index b875fe3..fb7867b 100644 --- a/simples3.go +++ b/simples3.go @@ -1,4 +1,4 @@ -// LICENSE MIT +// LICENSE BSD-2-Clause-FreeBSD // Copyright (c) 2018, Rohan Verma package simples3 @@ -62,12 +62,12 @@ type UploadInput struct { // 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" -// +// +// 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"` From 2a57ff9944a753a5d9f6f5a7855e9905dd72acda Mon Sep 17 00:00:00 2001 From: Joice Date: Mon, 19 Jul 2021 07:16:49 +0000 Subject: [PATCH 2/3] feat: Adds support for custom metadata (#13) * feat: Adds support for custom metadata assigning metadata for files will be beneficial to find files. ref: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html * fix: review suggestions by @rhnvrm - metadata moved to UploadInput as optional field. - changed x-amz-meta as constant - changed sprintf to simple string concat * FIX: changed amazon meta prefix changed variable name for amazon meta prefix to the top, prefix variable is exported. removed redundant comment for metadata --- simples3.go | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/simples3.go b/simples3.go index fb7867b..ed58590 100644 --- a/simples3.go +++ b/simples3.go @@ -25,6 +25,10 @@ import ( const ( securityCredentialsURL = "http://169.254.169.254/latest/meta-data/iam/security-credentials/" ) +const ( + // AMZMetaPrefix to prefix metadata key. + AMZMetaPrefix = "x-amz-meta-" +) // S3 provides a wrapper around your S3 credentials. type S3 struct { @@ -55,6 +59,7 @@ type UploadInput struct { // optional fields ContentDisposition string ACL string + CustomMetadata map[string]string // Setting key/value pairs adds user-defined metadata keys to the object, prefixed with AMZMetaPrefix. Body io.ReadSeeker } @@ -307,7 +312,8 @@ func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { if err != nil { return UploadResponse{}, err } - policies, err := s3.CreateUploadPolicies(UploadConfig{ + + uc := UploadConfig{ UploadURL: s3.getURL(u.Bucket), BucketName: u.Bucket, ObjectKey: u.ObjectKey, @@ -318,8 +324,18 @@ func (s3 *S3) FileUpload(u UploadInput) (UploadResponse, error) { MetaData: map[string]string{ "success_action_status": "201", // returns XML doc on success }, - }) + } + + // Set custom metadata. + for k, v := range u.CustomMetadata { + if !strings.HasPrefix(k, AMZMetaPrefix) { + k = AMZMetaPrefix + k + } + + uc.MetaData[k] = v + } + policies, err := s3.CreateUploadPolicies(uc) if err != nil { return UploadResponse{}, err } From eac235b2b6a521e32d3b37f51cce4aa3832b7913 Mon Sep 17 00:00:00 2001 From: Rohan Verma Date: Mon, 19 Jul 2021 13:41:36 +0530 Subject: [PATCH 3/3] feat: add support for FileDetails/HEAD API call (#14) --- simples3.go | 95 +++++++++++++++++++++++++++++++++++++++++++++++- simples3_test.go | 51 ++++++++++++++++++++++++-- 2 files changed, 141 insertions(+), 5 deletions(-) diff --git a/simples3.go b/simples3.go index ed58590..3818ebd 100644 --- a/simples3.go +++ b/simples3.go @@ -42,12 +42,33 @@ type S3 struct { URIFormat string } -// DownloadInput is passed to FileUpload as a parameter. +// DownloadInput is passed to FileDownload as a parameter. type DownloadInput struct { Bucket string ObjectKey string } +// DetailsInput is passed to FileDetails as a parameter. +type DetailsInput struct { + Bucket string + ObjectKey string +} + +// DetailsResponse is returned by FileDetails. +type DetailsResponse struct { + ContentType string + ContentLength string + AcceptRanges string + Date string + Etag string + LastModified string + Server string + AmzID2 string + AmzRequestID string + AmzMeta map[string]string + ExtraHeaders map[string]string +} + // UploadInput is passed to FileUpload as a parameter. type UploadInput struct { // essential fields @@ -422,6 +443,78 @@ func (s3 *S3) FileDelete(u DeleteInput) error { return nil } +func (s3 *S3) FileDetails(u DetailsInput) (DetailsResponse, error) { + req, err := http.NewRequest( + http.MethodHead, s3.getURL(u.Bucket, u.ObjectKey), nil, + ) + if err != nil { + return DetailsResponse{}, err + } + + if err := s3.signRequest(req); err != nil { + return DetailsResponse{}, err + } + + res, err := s3.getClient().Do(req) + if err != nil { + return DetailsResponse{}, err + } + + if res.StatusCode != 200 { + return DetailsResponse{}, fmt.Errorf("status code: %s", res.Status) + } + + var out DetailsResponse + for k, v := range res.Header { + lk := strings.ToLower(k) + + switch lk { + case "content-type": + out.ContentType = getFirstString(v) + case "content-length": + out.ContentLength = getFirstString(v) + case "accept-ranges": + out.AcceptRanges = getFirstString(v) + case "date": + out.Date = getFirstString(v) + case "etag": + out.Etag = getFirstString(v) + case "last-modified": + out.LastModified = getFirstString(v) + case "server": + out.Server = getFirstString(v) + case "x-amz-id-2": + out.AmzID2 = getFirstString(v) + case "x-amz-request-id": + out.AmzRequestID = getFirstString(v) + default: + if strings.HasPrefix(lk, AMZMetaPrefix) { + if out.AmzMeta == nil { + out.AmzMeta = map[string]string{} + } + + out.AmzMeta[k] = getFirstString(v) + } else { + if out.ExtraHeaders == nil { + out.ExtraHeaders = map[string]string{} + } + + out.ExtraHeaders[k] = getFirstString(v) + } + } + } + + return out, nil +} + +func getFirstString(s []string) string { + if len(s) > 0 { + return s[0] + } + + return "" +} + // if object matches reserved string, no need to encode them var reservedObjectNames = regexp.MustCompile("^[a-zA-Z0-9-_.~/]+$") diff --git a/simples3_test.go b/simples3_test.go index 2b99e71..6169d5e 100644 --- a/simples3_test.go +++ b/simples3_test.go @@ -39,10 +39,11 @@ func TestS3_FileUpload(t *testing.T) { u UploadInput } tests := []struct { - name string - fields tConfig - args args - wantErr bool + name string + fields tConfig + args args + testDetails bool + wantErr bool }{ { name: "Upload test.txt", @@ -63,6 +64,29 @@ func TestS3_FileUpload(t *testing.T) { }, wantErr: false, }, + { + name: "Upload test.txt with custom metadata", + fields: tConfig{ + AccessKey: os.Getenv("AWS_S3_ACCESS_KEY"), + SecretKey: os.Getenv("AWS_S3_SECRET_KEY"), + Endpoint: os.Getenv("AWS_S3_ENDPOINT"), + Region: os.Getenv("AWS_S3_REGION"), + }, + args: args{ + UploadInput{ + Bucket: os.Getenv("AWS_S3_BUCKET"), + ObjectKey: "test_metadata.txt", + ContentType: "text/plain", + FileName: "test.txt", + Body: testTxt, + CustomMetadata: map[string]string{ + "test-metadata": "foo-bar", + }, + }, + }, + wantErr: false, + testDetails: true, + }, { name: "Upload avatar.png", fields: tConfig{ @@ -111,10 +135,29 @@ func TestS3_FileUpload(t *testing.T) { if (err != nil) != tt.wantErr { t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr) } + + // reset file, to reuse in further tests. + tt.args.u.Body.Seek(0, 0) + // check for empty response if (resp == UploadResponse{}) { t.Errorf("S3.FileUpload() returned empty response, %v", resp) } + + if tt.testDetails { + dResp, err := s3.FileDetails(DetailsInput{ + Bucket: tt.args.u.Bucket, + ObjectKey: tt.args.u.ObjectKey, + }) + + if (err != nil) != tt.wantErr { + t.Errorf("S3.FileUpload() error = %v, wantErr %v", err, tt.wantErr) + } + + if len(dResp.AmzMeta) != 1 { + t.Errorf("S3.FileDetails() returned incorrect metadata, got: %v", dResp.AmzMeta) + } + } }) } }