diff --git a/cmd/ostor/main.go b/cmd/ostor/main.go index 2b2ba0e..c2ed313 100644 --- a/cmd/ostor/main.go +++ b/cmd/ostor/main.go @@ -63,6 +63,34 @@ func main() { Flags: []cli.Flag{ emailFlag(), }, + Subcommands: []*cli.Command{ + { + Name: "delete", + Aliases: []string{"d"}, + Action: cmd.DeleteBucket, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "bucket", + Required: true, + }, + &cli.BoolFlag{ + Name: "confirm", + Value: false, + }, + }, + }, + { + Name: "show", + Aliases: []string{"s"}, + Action: cmd.ShowBucket, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "bucket", + Required: true, + }, + }, + }, + }, }, { Name: "stats", diff --git a/go.mod b/go.mod index c63e62a..1a1abdc 100644 --- a/go.mod +++ b/go.mod @@ -10,19 +10,30 @@ require ( require ( github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect + github.com/minio/md5-simd v1.1.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/sys v0.22.0 // indirect + golang.org/x/crypto v0.28.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) require ( github.com/fatih/color v1.17.0 github.com/gorilla/mux v1.8.1 + github.com/minio/minio-go/v7 v7.0.79 github.com/rodaine/table v1.3.0 github.com/urfave/cli/v2 v2.27.5 - golang.org/x/net v0.27.0 // indirect + golang.org/x/net v0.30.0 // indirect ) diff --git a/go.sum b/go.sum index eb6b4cc..bb6ba38 100644 --- a/go.sum +++ b/go.sum @@ -3,14 +3,27 @@ github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-resty/resty/v2 v2.15.3 h1:bqff+hcqAflpiF591hhJzNdkRsFhlB96CYfBwSFvql8= github.com/go-resty/resty/v2 v2.15.3/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -18,12 +31,18 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= +github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= +github.com/minio/minio-go/v7 v7.0.79 h1:SvJZpj3hT0RN+4KiuX/FxLfPZdsuegy6d/2PiemM/bM= +github.com/minio/minio-go/v7 v7.0.79/go.mod h1:84gmIilaX4zcvAWWzJ5Z1WI5axN+hAbM5w25xf8xvC0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE= github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU= +github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= +github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -39,12 +58,21 @@ github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/cmd/buckets.go b/internal/cmd/buckets.go index 2382252..28b3646 100644 --- a/internal/cmd/buckets.go +++ b/internal/cmd/buckets.go @@ -1,12 +1,41 @@ package cmd import ( + "context" + "fmt" + "github.com/Luzilla/acronis-s3-usage/internal/utils" "github.com/Luzilla/acronis-s3-usage/pkg/ostor" + "github.com/Luzilla/acronis-s3-usage/pkg/s3" "github.com/rodaine/table" "github.com/urfave/cli/v2" ) +// this executes the action on 'behalf' of the user by returning the account +// and using the first credential pair to run the delete operations +func DeleteBucket(cCtx *cli.Context) error { + client := cCtx.Context.Value(OstorClient).(*ostor.Ostor) + + s3, err := s3.NewS3(cCtx.String("s3-endpoint"), cCtx.String("email"), client) + if err != nil { + return err + } + + ctx := context.Background() + bucketName := cCtx.String("bucket") + + if err := s3.CheckBucket(ctx, bucketName); err != nil { + return err + } + + if err := s3.DeleteBucket(ctx, bucketName); err != nil { + return err + } + + fmt.Println("Bucket deleted") + return nil +} + func ListBuckets(cCtx *cli.Context) error { client := cCtx.Context.Value(OstorClient).(*ostor.Ostor) @@ -18,9 +47,38 @@ func ListBuckets(cCtx *cli.Context) error { tbl := table.New("Bucket", "Size (current)", "Owner", "Created At") tbl.WithHeaderFormatter(headerFmt()).WithFirstColumnFormatter(columnFmt()) - for _, b := range buckets.Buckets { - tbl.AddRow(b.Name, utils.PrettyByteSize(b.Size.Current), b.OwnerID, b.CreatedAt) + if len(buckets.Buckets) > 0 { + for _, b := range buckets.Buckets { + tbl.AddRow(b.Name, utils.PrettyByteSize(b.Size.Current), b.OwnerID, b.CreatedAt) + } + } else { + tbl.AddRow("no buckets") } + + tbl.Print() + + return nil +} + +func ShowBucket(cCtx *cli.Context) error { + ListBuckets(cCtx) // display the filter view first + + client := cCtx.Context.Value(OstorClient).(*ostor.Ostor) + + s3, err := s3.NewS3(cCtx.String("s3-endpoint"), cCtx.String("email"), client) + if err != nil { + return err + } + + fmt.Println("") + + tbl := table.New("File", "Size") + tbl.WithHeaderFormatter(headerFmt()).WithFirstColumnFormatter(columnFmt()) + + for o := range s3.ListContents(context.Background(), cCtx.String("bucket")) { + tbl.AddRow(o.Key, utils.PrettyByteSize(o.Size), o.Owner.ID) + } + tbl.Print() return nil diff --git a/pkg/s3/s3.go b/pkg/s3/s3.go new file mode 100644 index 0000000..0074cc2 --- /dev/null +++ b/pkg/s3/s3.go @@ -0,0 +1,98 @@ +// Package s3 simulates an administrative interface for people who maintain ostor +// this package wraps around ostor and minio/minio-go to executes calls on behalf +// of accounts in the system. This is achieved by returning an account's credential +// pair and using it for calls. It requires that an account has one. You can call +// the key management features in the ostor CLI to achieve that or use the user methods +// in the ostor package to achieve the same. +package s3 + +import ( + "context" + "fmt" + log "log/slog" + "net/url" + "os" + + "github.com/Luzilla/acronis-s3-usage/pkg/ostor" + "github.com/minio/minio-go/v7" + "github.com/minio/minio-go/v7/pkg/credentials" +) + +// S3 wraps minio and ostor to provide a management API. +type S3 struct { + mc *minio.Client +} + +// NewS3 creates the S3 struct from endpoint, email and ostor. +func NewS3(endpointURL, email string, ostorClient *ostor.Ostor) (*S3, error) { + endpoint, err := url.Parse(endpointURL) + if err != nil { + return nil, fmt.Errorf("unable to parse %s: %s", endpoint, err) + } + + user, _, err := ostorClient.GetUser(email) + if err != nil { + return nil, err + } + + if len(user.AccessKeys) == 0 { + return nil, fmt.Errorf("account has no keys, please generate a key-pair first") + } + + keyPair := user.AccessKeys[0] + + mc, err := minio.New(endpoint.Host, &minio.Options{ + Creds: credentials.NewStaticV4( + keyPair.AccessKeyID, + keyPair.SecretAccessKey, + ""), + Secure: true, + }) + if err != nil { + return nil, fmt.Errorf("unable to initialize s3 client: %s", err) + } + + return &S3{mc: mc}, nil +} + +// CheckBucket determines if a bucket exists. +func (s *S3) CheckBucket(ctx context.Context, bucketName string) error { + status, err := s.mc.BucketExists(ctx, bucketName) + if err != nil { + return fmt.Errorf("unable to check if the bucket exists: %s", err) + } + + if !status { + fmt.Println("bucket does not exist") + return nil + } + + return nil +} + +// DeleteBucket does a recursive delete on all objects within a bucket and empties it. +func (s *S3) DeleteBucket(ctx context.Context, bucketName string) error { + delChan := make(chan minio.ObjectInfo) + + go func() { + defer close(delChan) + for object := range s.ListContents(ctx, bucketName) { + if object.Err != nil { + log.Error(object.Err.Error()) + os.Exit(1) + } + delChan <- object + } + }() + + for rErr := range s.mc.RemoveObjects(ctx, bucketName, delChan, minio.RemoveObjectsOptions{}) { + log.Error("Error detected during deletion: " + rErr.Err.Error()) + } + + return s.mc.RemoveBucket(ctx, bucketName) +} + +// ListContents lists the contents of a bucket and returns a channel to "range" on. +func (s *S3) ListContents(ctx context.Context, bucketName string) <-chan minio.ObjectInfo { + return s.mc.ListObjects(ctx, bucketName, minio.ListObjectsOptions{Recursive: true}) +}