Skip to content

Commit

Permalink
command: implement 'remove bucket' feature (#310)
Browse files Browse the repository at this point in the history
Resolves #303
  • Loading branch information
ocakhasan authored Jul 5, 2021
1 parent ed0556d commit 5495dd4
Show file tree
Hide file tree
Showing 5 changed files with 234 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ storage services and local filesystems.
- Set Server Side Encryption using AWS Key Management Service (KMS)
- Set Access Control List (ACL) for objects/files on the upload, copy, move.
- Print object contents to stdout
- Create buckets
- Create or remove buckets
- Summarize objects sizes, grouping by storage class
- Wildcard support for all operations
- Multiple arguments support for delete operation
Expand Down
1 change: 1 addition & 0 deletions command/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ func Main(ctx context.Context, args []string) error {
deleteCommand,
moveCommand,
makeBucketCommand,
removeBucketCommand,
sizeCommand,
catCommand,
runCommand,
Expand Down
88 changes: 88 additions & 0 deletions command/rb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package command

import (
"context"

"github.com/urfave/cli/v2"

"github.com/peak/s5cmd/log"
"github.com/peak/s5cmd/log/stat"
"github.com/peak/s5cmd/storage"
"github.com/peak/s5cmd/storage/url"
)

var removeBucketHelpTemplate = `Name:
{{.HelpName}} - {{.Usage}}
Usage:
{{.HelpName}} bucketname
Options:
{{range .VisibleFlags}}{{.}}
{{end}}
Examples:
1. Deletes S3 bucket with given name
> s5cmd {{.HelpName}} bucketname
`

var removeBucketCommand = &cli.Command{
Name: "rb",
HelpName: "rb",
Usage: "remove bucket",
CustomHelpTemplate: removeBucketHelpTemplate,
Before: func(c *cli.Context) error {
err := validateMBCommand(c) // uses same validation function with make bucket command.
if err != nil {
printError(givenCommand(c), c.Command.Name, err)
}
return err
},
Action: func(c *cli.Context) (err error) {
defer stat.Collect(c.Command.FullName(), &err)()

return RemoveBucket{
src: c.Args().First(),
op: c.Command.Name,
fullCommand: givenCommand(c),

storageOpts: NewStorageOpts(c),
}.Run(c.Context)
},
}

// RemoveBucket holds bucket deletion operation flags and states.
type RemoveBucket struct {
src string
op string
fullCommand string

storageOpts storage.Options
}

// Run removes a bucket.
func (b RemoveBucket) Run(ctx context.Context) error {
bucket, err := url.New(b.src)
if err != nil {
printError(b.fullCommand, b.op, err)
return err
}

client, err := storage.NewRemoteClient(ctx, &url.URL{}, b.storageOpts)
if err != nil {
printError(b.fullCommand, b.op, err)
return err
}

if err := client.RemoveBucket(ctx, bucket.Bucket); err != nil {
printError(b.fullCommand, b.op, err)
return err
}

msg := log.InfoMessage{
Operation: b.op,
Source: bucket,
}
log.Info(msg)

return nil
}
132 changes: 132 additions & 0 deletions e2e/rb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package e2e

import (
"fmt"
"testing"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/s3"

"gotest.tools/v3/icmd"
)

func TestRemoveBucketSuccess(t *testing.T) {

t.Parallel()
s3client, s5cmd, cleanup := setup(t)
defer cleanup()

bucketName := "bucket"
src := fmt.Sprintf("s3://%v", bucketName)

createBucket(t, s3client, bucketName)

cmd := s5cmd("rb", src)
result := icmd.RunCmd(cmd)

result.Assert(t, icmd.Success)

assertLines(t, result.Stdout(), map[int]compareFunc{
0: equals(`rb %v`, src),
})

_, err := s3client.HeadBucket(&s3.HeadBucketInput{Bucket: aws.String(bucketName)})

if err == nil {
t.Errorf("bucket still exists after remove bucket operation\n")
}
}

func TestRemoveBucketSuccessJson(t *testing.T) {
t.Parallel()
s3client, s5cmd, cleanup := setup(t)
defer cleanup()

bucketName := "bucket"
src := fmt.Sprintf("s3://%v", bucketName)

createBucket(t, s3client, bucketName)

cmd := s5cmd("--json", "rb", src)
result := icmd.RunCmd(cmd)

result.Assert(t, icmd.Success)

jsonText := `
{
"operation": "rb",
"success": true,
"source": "%v"
}
`

assertLines(t, result.Stdout(), map[int]compareFunc{
0: json(jsonText, src),
}, jsonCheck(true))

_, err := s3client.HeadBucket(&s3.HeadBucketInput{Bucket: aws.String(bucketName)})
if err == nil {
t.Errorf("bucket still exists after remove bucket operation\n")
}
}

func TestRemoveBucketFailure(t *testing.T) {
t.Parallel()
_, s5cmd, cleanup := setup(t)
defer cleanup()

bucketName := "invalid/bucket/name"
src := fmt.Sprintf("s3://%s", bucketName)
cmd := s5cmd("rb", src)
result := icmd.RunCmd(cmd)

result.Assert(t, icmd.Expected{ExitCode: 1})

assertLines(t, result.Stderr(), map[int]compareFunc{
0: equals(`ERROR "rb %v": invalid s3 bucket`, src),
})
}

func TestRemoveBucketFailureJson(t *testing.T) {
t.Parallel()
_, s5cmd, cleanup := setup(t)
defer cleanup()

bucketName := "invalid/bucket/name"
src := fmt.Sprintf("s3://%s", bucketName)
cmd := s5cmd("--json", "rb", src)
result := icmd.RunCmd(cmd)

result.Assert(t, icmd.Expected{ExitCode: 1})

assertLines(t, result.Stderr(), map[int]compareFunc{
0: equals(`{"operation":"rb","command":"rb %v","error":"invalid s3 bucket"}`, src),
}, jsonCheck(true))
}

func TestRemoveBucketWithObject(t *testing.T) {
t.Parallel()
const (
bucket = "test-bucket"
fileContent = "this is a file content"
fileName = "file1.txt"
)

s3client, s5cmd, cleanup := setup(t)
defer cleanup()

createBucket(t, s3client, bucket)
putFile(t, s3client, bucket, fileName, fileContent)

bucketName := fmt.Sprintf("s3://%v", bucket)
cmd := s5cmd("rb", bucketName)
result := icmd.RunCmd(cmd)

result.Assert(t, icmd.Expected{ExitCode: 1})

expected := fmt.Sprintf(`ERROR "rb %v": BucketNotEmpty:`, bucketName) // error due to non-empty bucket.

assertLines(t, result.Stderr(), map[int]compareFunc{
0: match(expected),
})
}
12 changes: 12 additions & 0 deletions storage/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,18 @@ func (s *S3) MakeBucket(ctx context.Context, name string) error {
return err
}

// RemoveBucket removes an S3 bucket with the given name.
func (s *S3) RemoveBucket(ctx context.Context, name string) error {
if s.dryRun {
return nil
}

_, err := s.api.DeleteBucketWithContext(ctx, &s3.DeleteBucketInput{
Bucket: aws.String(name),
})
return err
}

// SessionCache holds session.Session according to s3Opts and it synchronizes
// access/modification.
type SessionCache struct {
Expand Down

0 comments on commit 5495dd4

Please sign in to comment.