From eac235b2b6a521e32d3b37f51cce4aa3832b7913 Mon Sep 17 00:00:00 2001 From: Rohan Verma Date: Mon, 19 Jul 2021 13:41:36 +0530 Subject: [PATCH] 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) + } + } }) } }