Skip to content

Commit

Permalink
Merge pull request #33 from Luzilla/bucket-management
Browse files Browse the repository at this point in the history
Update(ostor): bucket management
  • Loading branch information
till authored Oct 29, 2024
2 parents 132a73c + 4d66a20 commit e440202
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 4 deletions.
28 changes: 28 additions & 0 deletions cmd/ostor/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
15 changes: 13 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
28 changes: 28 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,46 @@ 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=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
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=
Expand All @@ -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=
Expand Down
62 changes: 60 additions & 2 deletions internal/cmd/buckets.go
Original file line number Diff line number Diff line change
@@ -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.IsDeletable(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)

Expand All @@ -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
Expand Down
110 changes: 110 additions & 0 deletions pkg/s3/s3.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 the s3 client to provide shorthands for common ops.
type S3 struct {
mc *minio.Client
}

// NewS3 creates an S3 handler which is specific to the provided email.
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
}

// IsDeletable determines if a bucket exists and if is empty
func (s *S3) IsDeletable(ctx context.Context, bucketName string) (status bool, err error) {
status, err = s.mc.BucketExists(ctx, bucketName)
if err != nil {
err = fmt.Errorf("unable to check if the bucket %q exists: %s", bucketName, err)
return
}

if !status {
err = fmt.Errorf("bucket %q does not exist", bucketName)
return
}

listChan := s.mc.ListObjects(ctx, bucketName, minio.ListObjectsOptions{
Recursive: true,
MaxKeys: 1,
})
_, ok := <-listChan
if !ok {
status = false
err = fmt.Errorf("bucket %q is not empty", bucketName)
return
}

return
}

// DeleteBucket does a recursive delete on all objects within a bucket to empty it, before deleting 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 (recursively) 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})
}

0 comments on commit e440202

Please sign in to comment.