Skip to content

Commit

Permalink
chore(cli): add S3 bucket object tree to svc show (#4966)
Browse files Browse the repository at this point in the history
sample output:
```
% copilot svc show 
Service name: static-site
About

  Application  bugbash-static
  Name         static-site
  Type         Static Site

Routes

  Environment  URL
  -----------  ---
  test         d1vwytbnh1k1gy.cloudfront.net
  prod         d1yb44q5409rdj.cloudfront.net

S3 Bucket Objects

  Environment  test
.
├── error.html
├── index.html
├── css
│   ├── Style.css
│   ├── all.min.css
│   └── bootstrap.min.css
└── images
    ├── bg-masthead.jpg
    └── new
        └── hi.html

  Environment  prod
.
├── ReadMe.md
├── error.html
├── index.html
├── Images
│   ├── AWS-free.PNG
│   ├── S3WebsiteHosting-Architecture.PNG
│   ├── SampleWebsite.PNG
│   ├── Step1-CreateBucket-B.PNG
│   ├── Step1-CreateBucket-C.PNG
│   ├── Step1-CreateBucket-D.PNG
│   ├── Step1-CreateBucket-E.PNG
│   ├── Step1-CreateBucket.PNG
│   ├── Step2-UploadContents.PNG
│   ├── Step3-BucketPolicy-A.PNG
│   ├── Step3-BucketPolicy-B.PNG
│   ├── Step3-BucketPolicy.PNG
│   ├── Step4-A.PNG
│   └── Step4-B.PNG
├── css
│   ├── Style.css
│   ├── all.min.css
│   └── bootstrap.min.css
└── images
    ├── bg-masthead.jpg
    └── new
        └── hi.html
```

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the Apache 2.0 License.
  • Loading branch information
huanjani authored Jun 12, 2023
1 parent d79a46d commit 8e41bea
Show file tree
Hide file tree
Showing 8 changed files with 501 additions and 12 deletions.
4 changes: 2 additions & 2 deletions internal/pkg/aws/ecr/ecr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,15 +413,15 @@ func TestClearRepository(t *testing.T) {
},
wantError: nil,
},
"returns error if fail to check repo existance": {
"returns error if fail to check repo existence": {
mockECRClient: func(m *mocks.Mockapi) {
m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
RepositoryName: aws.String(mockRepoName),
}).Return(nil, mockAwsError)
},
wantError: fmt.Errorf("ecr repo mockRepoName describe images: %w", mockAwsError),
},
"returns error if fail to check repo existance because of non-awserr error type": {
"returns error if fail to check repo existence because of non-awserr error type": {
mockECRClient: func(m *mocks.Mockapi) {
m.EXPECT().DescribeImages(&ecr.DescribeImagesInput{
RepositoryName: aws.String(mockRepoName),
Expand Down
15 changes: 15 additions & 0 deletions internal/pkg/aws/s3/mocks/mock_s3.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

96 changes: 90 additions & 6 deletions internal/pkg/aws/s3/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package s3
import (
"errors"
"fmt"
"github.com/xlab/treeprint"
"io"
"mime"
"path/filepath"
Expand All @@ -30,6 +31,9 @@ const (

// Object location prefixes.
s3URIPrefix = "s3://"

// Delimiter for ListObjectsV2Input.
slashDelimiter = "/"
)

type s3ManagerAPI interface {
Expand All @@ -38,6 +42,7 @@ type s3ManagerAPI interface {

type s3API interface {
ListObjectVersions(input *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error)
ListObjectsV2(input *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error)
DeleteObjects(input *s3.DeleteObjectsInput) (*s3.DeleteObjectsOutput, error)
HeadBucket(input *s3.HeadBucketInput) (*s3.HeadBucketOutput, error)
}
Expand Down Expand Up @@ -77,13 +82,12 @@ func (s *S3) EmptyBucket(bucket string) error {
var listResp *s3.ListObjectVersionsOutput
var err error

// Bucket is exists check to make sure the bucket exists before proceeding in emptying it
isExists, err := s.isBucketExists(bucket)
bucketExists, err := s.bucketExists(bucket)
if err != nil {
return fmt.Errorf("unable to determine the existance of bucket %s: %w", bucket, err)
return fmt.Errorf("unable to determine the existence of bucket %s: %w", bucket, err)
}

if !isExists {
if !bucketExists {
return nil
}

Expand Down Expand Up @@ -190,8 +194,7 @@ func FormatARN(partition, location string) string {
return fmt.Sprintf("arn:%s:s3:::%s", partition, location)
}

// Check whether the bucket exists before proceeding with empty the bucket
func (s *S3) isBucketExists(bucket string) (bool, error) {
func (s *S3) bucketExists(bucket string) (bool, error) {
input := &s3.HeadBucketInput{
Bucket: aws.String(bucket),
}
Expand All @@ -206,6 +209,87 @@ func (s *S3) isBucketExists(bucket string) (bool, error) {
return true, nil
}

// GetBucketTree retrieves the objects in an S3 bucket and creates an ASCII tree representing their folder structure.
func (s *S3) GetBucketTree(bucket string) (string, error) {
exists, err := s.bucketExists(bucket)
if err != nil {
return "", err
}
if !exists {
return "", nil
}

var contents []*s3.Object
var prefixes []*s3.CommonPrefix
listResp := &s3.ListObjectsV2Output{}
for {
listParams := &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Delimiter: aws.String(slashDelimiter),
ContinuationToken: listResp.NextContinuationToken,
}
listResp, err = s.s3Client.ListObjectsV2(listParams)
if err != nil {
return "", fmt.Errorf("list objects for bucket %s: %w", bucket, err)
}
contents = append(contents, listResp.Contents...)
prefixes = append(prefixes, listResp.CommonPrefixes...)
if listResp.NextContinuationToken == nil {
break
}
}

tree := treeprint.New()
// Add top-level files.
for _, object := range contents {
tree.AddNode(aws.StringValue(object.Key))
}
// Recursively add folders and their children.
if err := s.addNodes(tree, prefixes, bucket); err != nil {
return "", err
}
return tree.String(), nil
}

func (s *S3) addNodes(tree treeprint.Tree, prefixes []*s3.CommonPrefix, bucket string) error {
if len(prefixes) == 0 {
return nil
}

listResp := &s3.ListObjectsV2Output{}
var err error
for _, prefix := range prefixes {
var respContents []*s3.Object
var respPrefixes []*s3.CommonPrefix
branch := tree.AddBranch(filepath.Base(aws.StringValue(prefix.Prefix)))
for {
listParams := &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Delimiter: aws.String(slashDelimiter),
ContinuationToken: listResp.ContinuationToken,
Prefix: prefix.Prefix,
}
listResp, err = s.s3Client.ListObjectsV2(listParams)
if err != nil {
return fmt.Errorf("list objects for bucket %s: %w", bucket, err)
}
respContents = append(respContents, listResp.Contents...)
respPrefixes = append(respPrefixes, listResp.CommonPrefixes...)
if listResp.NextContinuationToken == nil {
break
}
}
for _, file := range respContents {
fileName := filepath.Base(aws.StringValue(file.Key))
branch.AddNode(fileName)
}
if err := s.addNodes(branch, respPrefixes, bucket); err != nil {
return err
}
}
return nil
}

func (s *S3) upload(bucket, key string, buf io.Reader) (string, error) {
in := &s3manager.UploadInput{
Body: buf,
Expand Down
Loading

0 comments on commit 8e41bea

Please sign in to comment.