Skip to content

Commit

Permalink
Add mb (make-bucket) command (#86)
Browse files Browse the repository at this point in the history
* Resolves #25

- added mb (make bucket) command
  • Loading branch information
ilkinulas authored Mar 4, 2020
1 parent ab77f69 commit 9698760
Show file tree
Hide file tree
Showing 12 changed files with 121 additions and 2 deletions.
2 changes: 2 additions & 0 deletions core/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ var Commands = []CommandMap{

{"du", op.Size, []opt.ParamType{opt.S3ObjOrDir}, noOpts},
{"du", op.Size, []opt.ParamType{opt.S3WildObj}, noOpts},

{"mb", op.MakeBucket, []opt.ParamType{opt.S3Bucket}, noOpts},
}

// String formats the CommandMap using its Operation and ParamTypes
Expand Down
1 change: 1 addition & 0 deletions core/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ var globalCmdRegistry = map[op.Operation]commandFunc{
op.List: List,
op.ListBuckets: ListBuckets,
op.Size: Size,
op.MakeBucket: MakeBucket,
}
16 changes: 16 additions & 0 deletions core/job_exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,19 @@ func Size(ctx context.Context, job *Job) *JobResponse {

return jobResponse(err)
}

func MakeBucket(ctx context.Context, job *Job) *JobResponse {
bucket := job.args[0]

client, err := storage.NewClient(bucket)
if err != nil {
return jobResponse(err)
}

err = client.MakeBucket(ctx, bucket.Bucket)
if err != nil {
return jobResponse(err)
}
log.Logger.Success("Successfully created bucket %s.", bucket)
return jobResponse(nil)
}
6 changes: 5 additions & 1 deletion core/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func parseArgumentByType(s string, t opt.ParamType, fnObj *objurl.ObjectURL) (*o
case opt.Unchecked, opt.UncheckedOneOrMore:
return objurl.New(s)

case opt.S3Obj, opt.S3ObjOrDir, opt.S3WildObj, opt.S3Dir, opt.S3SimpleObj:
case opt.S3Bucket, opt.S3Obj, opt.S3ObjOrDir, opt.S3WildObj, opt.S3Dir, opt.S3SimpleObj:
url, err := objurl.New(s)
if err != nil {
return nil, err
Expand All @@ -44,6 +44,10 @@ func parseArgumentByType(s string, t opt.ParamType, fnObj *objurl.ObjectURL) (*o

s = url.Absolute()

if t == opt.S3Bucket && !url.IsBucket() {
return nil, errors.New("invalid s3 bucket")
}

if (t == opt.S3Obj || t == opt.S3ObjOrDir || t == opt.S3SimpleObj) && objurl.HasGlobCharacter(url.Path) {
return nil, errors.New("s3 key cannot contain wildcards")
}
Expand Down
39 changes: 39 additions & 0 deletions e2e/mb_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
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 Test_MakeBucket_success(t *testing.T) {
t.Parallel()
s3client, s5cmd, cleanup := setup(t)
defer cleanup()

bucketName := "test-bucket"
cmd := s5cmd(fmt.Sprintf("mb s3://%s", bucketName))
result := icmd.RunCmd(cmd)

result.Assert(t, icmd.Success)

_, err := s3client.HeadBucket(&s3.HeadBucketInput{Bucket: aws.String(bucketName)})
if err != nil {
t.Errorf("unexpected error %v", err)
}
}

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

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

result.Assert(t, icmd.Expected{ExitCode: 127})
}
5 changes: 5 additions & 0 deletions objurl/objurl.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ func (o *ObjectURL) IsRemote() bool {
return o.Type == remoteObject
}

// IsBucket returns true if the object url contains only bucket name
func (o *ObjectURL) IsBucket() bool {
return o.IsRemote() && o.Path == ""
}

// Absolute returns the absolute URL format of the object.
func (o *ObjectURL) Absolute() string {
if !o.IsRemote() {
Expand Down
32 changes: 32 additions & 0 deletions objurl/objurl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,35 @@ func Test_parseNonBatch(t *testing.T) {
})
}
}

func TestObjectURL_IsBucket(t *testing.T) {
tests := []struct {
input string
want bool
want_error bool
}{
{"s3://bucket", true, false},
{"s3://bucket/file", false, false},
{"bucket", false, false},
{"s3://", false, true},
}
for _, tc := range tests {
url, err := New(tc.input)
if tc.want_error && err != nil {
continue
}

if tc.want_error && err == nil {
t.Errorf("expecting error for input %s", tc.input)
}

if err != nil {
t.Errorf("unexpected error: %v for input %s", err, tc.input)
continue
}

if url.IsBucket() != tc.want {
t.Errorf("isBucket should return %v for %s", tc.want, tc.input)
}
}
}
6 changes: 5 additions & 1 deletion op/op.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package op

import (
"fmt"

"github.com/peak/s5cmd/opt"
"github.com/peak/s5cmd/stats"
)
Expand All @@ -29,6 +28,7 @@ const (
LocalCopy // Copy from local to local
LocalDelete // Delete local file
AliasGet // Alias for Download
MakeBucket // Make Bucket
)

var batchOperations = []Operation{
Expand Down Expand Up @@ -106,6 +106,8 @@ func (o Operation) String() string {
return "get"
case AliasBatchGet:
return "batch-get"
case MakeBucket:
return "make-bucket"
}

return fmt.Sprintf("Unknown:%d", o)
Expand Down Expand Up @@ -166,6 +168,8 @@ func (o Operation) Describe(l opt.OptionList) string {
return "Batch copy local files"
case LocalDelete:
return "Delete local files"
case MakeBucket:
return "Creates an S3 bucket"
}

return fmt.Sprintf("Unknown:%d", o)
Expand Down
3 changes: 3 additions & 0 deletions opt/opt.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ type ParamType int
const (
Unchecked ParamType = iota // Arbitrary single parameter
UncheckedOneOrMore // One or more arbitrary parameters (special case)
S3Bucket // Bucket
S3Obj // Bucket or bucket + key
S3Dir // Bucket or bucket + key + "/" (prefix)
S3ObjOrDir // Bucket or bucket + key [+ "/"]
Expand All @@ -181,6 +182,8 @@ func (p ParamType) String() string {
return "param"
case UncheckedOneOrMore:
return "param..."
case S3Bucket:
return "s3://bucket"
case S3Obj:
return "s3://bucket[/object]"
case S3SimpleObj:
Expand Down
4 changes: 4 additions & 0 deletions storage/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,10 @@ func (f *Filesystem) ListBuckets(_ context.Context, _ string) ([]Bucket, error)
return nil, f.notimplemented("ListBuckets")
}

func (f *Filesystem) MakeBucket(_ context.Context, _ string) error {
return f.notimplemented("MakeBucket")
}

func (f *Filesystem) UpdateRegion(_ string) error {
return f.notimplemented("UpdateRegion")
}
Expand Down
8 changes: 8 additions & 0 deletions storage/s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,14 @@ func (s *S3) ListBuckets(ctx context.Context, prefix string) ([]Bucket, error) {
return buckets, nil
}

// MakeBucket creates an S3 bucket with the given name.
func (s *S3) MakeBucket(ctx context.Context, name string) error {
_, err := s.api.CreateBucketWithContext(ctx, &s3.CreateBucketInput{
Bucket: aws.String(name),
})
return err
}

// UpdateRegion overrides AWS session with the region of given bucket.
func (s *S3) UpdateRegion(bucket string) error {
o, err := s.api.GetBucketLocation(&s3.GetBucketLocationInput{
Expand Down
1 change: 1 addition & 0 deletions storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type Storage interface {
Delete(context.Context, *objurl.ObjectURL) error
MultiDelete(context.Context, <-chan *objurl.ObjectURL) <-chan *Object
ListBuckets(context.Context, string) ([]Bucket, error)
MakeBucket(context.Context, string) error
UpdateRegion(string) error
}

Expand Down

0 comments on commit 9698760

Please sign in to comment.