Skip to content

Commit

Permalink
Add download s3bucket process
Browse files Browse the repository at this point in the history
  • Loading branch information
nao1215 committed Jan 30, 2024
1 parent c5cc98e commit 7daa434
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 29 deletions.
6 changes: 6 additions & 0 deletions config/s3hub/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package s3hub

const (
// DefaultDownloadDirPath is the default download directory.
DefaultDownloadDirPath = "s3hub-download"
)
61 changes: 61 additions & 0 deletions ui/s3hub/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import (
"crypto/rand"
"fmt"
"math/big"
"os"
"path/filepath"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/fatih/color"
"github.com/gogf/gf/os/gfile"
"github.com/nao1215/rainbow/app/di"
"github.com/nao1215/rainbow/app/domain/model"
"github.com/nao1215/rainbow/app/usecase"
"github.com/nao1215/rainbow/config/s3hub"
"github.com/nao1215/rainbow/ui"
"golang.org/x/sync/errgroup"
"golang.org/x/sync/semaphore"
Expand Down Expand Up @@ -59,6 +64,62 @@ func fetchS3KeysCmd(ctx context.Context, app *di.S3App, bucket model.Bucket) tea
})
}

// downloadS3BucketMsg is the message that is sent when the user wants to download the S3 bucket.
type downloadS3BucketMsg struct {
downloadedBuckets []model.Bucket
}

// downloadS3BucketCmd downloads the S3 bucket.
func downloadS3BucketCmd(ctx context.Context, app *di.S3App, bucket []model.Bucket) tea.Cmd {
d, err := rand.Int(rand.Reader, big.NewInt(500))
if err != nil {
return func() tea.Msg {
return ui.ErrMsg(fmt.Errorf("failed to start deleting s3 bucket: %w", err))
}
}
delay := time.Millisecond * time.Duration(d.Int64())

return tea.Tick(delay, func(t time.Time) tea.Msg {
for _, b := range bucket {
output, err := app.S3ObjectsLister.ListS3Objects(ctx, &usecase.S3ObjectsListerInput{
Bucket: b,
})
if err != nil {
return ui.ErrMsg(err)
}

if len(output.Objects) == 0 {
continue
}

for _, v := range output.Objects {
downloadOutput, err := app.S3ObjectDownloader.DownloadS3Object(ctx, &usecase.S3ObjectDownloaderInput{
Bucket: b,
Key: v.S3Key,
})
if err != nil {
return ui.ErrMsg(err)
}

destinationPath := filepath.Clean(filepath.Join(s3hub.DefaultDownloadDirPath, b.String(), v.S3Key.String()))
dir := filepath.Dir(destinationPath)
if !gfile.IsDir(dir) {
if err := os.MkdirAll(dir, 0750); err != nil {
return ui.ErrMsg(fmt.Errorf("can not create directory %s: %w", color.YellowString(dir), err))
}
}

if err := downloadOutput.S3Object.ToFile(destinationPath, 0644); err != nil {
return ui.ErrMsg(fmt.Errorf("can not write file to %s: %w", color.YellowString(destinationPath), err))
}
}
}
return downloadS3BucketMsg{
downloadedBuckets: bucket,
}
})
}

// deleteS3BucketMsg is the message that is sent when the user wants to delete the S3 bucket.
type deleteS3BucketMsg struct {
deletedBucket model.Bucket
Expand Down
96 changes: 68 additions & 28 deletions ui/s3hub/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ type s3hubListBucketModel struct {
ctx context.Context
// bucketSets is the list of the S3 buckets.
bucketSets model.BucketSets
// status is the status of the list bucket operation.
status s3hubListBucketStatus
// s3BucketListBucketStatus is the s3BucketListBucketStatus of the list bucket operation.
s3BucketListBucketStatus s3hubListBucketStatus
// s3hubDownloadStatus is the s3hubDownloadStatus of the download operation.
s3hubDownloadStatus s3hubDownloadStatus
// toggle is the currently selected menu item.
toggles ui.ToggleSets
}

// s3hubListBucketStatus is the status of the list bucket operation.
Expand All @@ -50,6 +54,18 @@ const (
s3hubListBucketStatusQuit
)

// s3hubDownloadStatus is the status of the download operation.
type s3hubDownloadStatus int

const (
// s3hubDownloadStatusNone is the status when the download operation is not executed.
s3hubDownloadStatusNone s3hubDownloadStatus = iota
// s3hubDownloadStatusDownloading is the status when the download operation is executed.
s3hubDownloadStatusDownloading
// s3hubDownloadStatusDownloaded is the status when the download operation is executed and the file is downloaded.
s3hubDownloadStatusDownloaded
)

const (
windowHeight = 10
)
Expand All @@ -69,14 +85,15 @@ func newS3HubListBucketModel() (*s3hubListBucketModel, error) {
}

return &s3hubListBucketModel{
awsConfig: cfg,
awsProfile: profile,
region: region,
app: app,
choice: ui.NewChoice(0, 0),
status: s3hubListBucketStatusNone,
ctx: ctx,
bucketSets: model.BucketSets{},
awsConfig: cfg,
awsProfile: profile,
region: region,
app: app,
choice: ui.NewChoice(0, 0),
s3BucketListBucketStatus: s3hubListBucketStatusNone,
ctx: ctx,
bucketSets: model.BucketSets{},
toggles: ui.NewToggleSets(0),
}, nil
}

Expand All @@ -97,16 +114,28 @@ func (m *s3hubListBucketModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "k", "up":
m.choice.Decrement()
case "ctrl+c":
m.status = s3hubListBucketStatusQuit
m.s3BucketListBucketStatus = s3hubListBucketStatusQuit
return m, tea.Quit
case "q", "esc":
m.status = s3hubListBucketStatusReturnToTop
m.s3BucketListBucketStatus = s3hubListBucketStatusReturnToTop
return newRootModel(), nil
case "d":
if m.s3BucketListBucketStatus == s3hubListBucketStatusBucketListed && m.s3hubDownloadStatus == s3hubDownloadStatusNone {
m.s3hubDownloadStatus = s3hubDownloadStatusDownloading

buckets := make([]model.Bucket, 0, len(m.bucketSets))
for i, b := range m.bucketSets {
if m.toggles[i].Enabled {
buckets = append(buckets, b.Bucket)
}
}
return m, downloadS3BucketCmd(m.ctx, m.app, buckets)
}
case "enter":
if m.status == s3hubListBucketStatusReturnToTop {
if m.s3BucketListBucketStatus == s3hubListBucketStatusReturnToTop || m.s3hubDownloadStatus == s3hubDownloadStatusDownloaded {
return newRootModel(), nil
}
if m.status == s3hubListBucketStatusBucketListed {
if m.s3BucketListBucketStatus == s3hubListBucketStatusBucketListed {
model, err := newS3HubListS3ObjectModel()
if err != nil {
m.err = err
Expand All @@ -116,17 +145,23 @@ func (m *s3hubListBucketModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
model.bucket = m.bucketSets[m.choice.Choice].Bucket
return model, fetchS3KeysCmd(m.ctx, m.app, model.bucket)
}
case "space":
// TODO: implement
case " ":
if m.s3BucketListBucketStatus == s3hubListBucketStatusBucketListed && m.s3hubDownloadStatus == s3hubDownloadStatusNone {
m.toggles[m.choice.Choice].Toggle()
}
}
case fetchS3BucketMsg:
m.status = s3hubListBucketStatusBucketFetched
m.s3BucketListBucketStatus = s3hubListBucketStatusBucketFetched
m.bucketSets = msg.buckets
m.choice = ui.NewChoice(0, m.bucketSets.Len()-1)
m.toggles = ui.NewToggleSets(m.bucketSets.Len())
return m, nil
case downloadS3BucketMsg:
m.s3hubDownloadStatus = s3hubDownloadStatusDownloaded
return m, nil
case ui.ErrMsg:
m.err = msg
m.status = s3hubListBucketStatusQuit
m.s3BucketListBucketStatus = s3hubListBucketStatusQuit
return m, tea.Quit
default:
return m, nil
Expand All @@ -136,21 +171,25 @@ func (m *s3hubListBucketModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

func (m *s3hubListBucketModel) View() string {
if m.err != nil {
m.status = s3hubListBucketStatusQuit
m.s3BucketListBucketStatus = s3hubListBucketStatusQuit
return ui.ErrorMessage(m.err)
}

if m.status == s3hubListBucketStatusQuit {
if m.s3BucketListBucketStatus == s3hubListBucketStatusQuit {
return ui.GoodByeMessage()
}

if m.status == s3hubListBucketStatusNone || m.status == s3hubListBucketStatusBucketFetching {
if m.s3hubDownloadStatus == s3hubDownloadStatusDownloaded {
return doneStyle.Render("All S3 buckets downloaded. Press <enter> to return to the top.")
}

if m.s3BucketListBucketStatus == s3hubListBucketStatusNone || m.s3BucketListBucketStatus == s3hubListBucketStatusBucketFetching {
return fmt.Sprintf(
"fetching the list of the S3 buckets (profile=%s)\n",
m.awsProfile.String())
}

if m.status == s3hubListBucketStatusBucketFetched {
if m.s3BucketListBucketStatus == s3hubListBucketStatusBucketFetched {
return m.bucketListString()
}
return m.bucketListString() // TODO: implement
Expand Down Expand Up @@ -184,27 +223,28 @@ func (m *s3hubListBucketModel) bucketListStrWithCheckbox() string {
}
}

m.status = s3hubListBucketStatusBucketListed
m.s3BucketListBucketStatus = s3hubListBucketStatusBucketListed
s := fmt.Sprintf("S3 buckets %d/%d (profile=%s)\n\n", m.choice.Choice+1, m.bucketSets.Len(), m.awsProfile.String())
for i := startIndex; i < endIndex; i++ {
b := m.bucketSets[i]
s += fmt.Sprintf("%s\n",
ui.Checkbox(
ui.ToggleWidget(
fmt.Sprintf(
"%s (region=%s, updated_at=%s)",
color.GreenString("%s", b.Bucket),
color.YellowString("%s", b.Region),
b.CreationDate.Format("2006-01-02 15:04:05 MST")),
m.choice.Choice == i))
m.choice.Choice == i, m.toggles[i].Enabled))
}
s += ui.Subtle("\n<esc>: return to the top | <Ctrl-C>: quit | up/down: select\n")
s += ui.Subtle("<enter>, <space>: choose bucket\n\n")
s += ui.Subtle("<space>: choose bucket to download | d: download buckets\n")
s += ui.Subtle("<enter>: list up s3 objects in bucket\n\n")
return s
}

// emptyBucketListString returns the string representation when there are no S3 buckets.
func (m *s3hubListBucketModel) emptyBucketListString() string {
m.status = s3hubListBucketStatusReturnToTop
m.s3BucketListBucketStatus = s3hubListBucketStatusReturnToTop
return fmt.Sprintf("No S3 buckets (profile=%s)\n\n%s\n",
m.awsProfile.String(),
ui.Subtle("<enter>: return to the top"))
Expand Down Expand Up @@ -302,7 +342,7 @@ func (m *s3hubListS3ObjectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.err = err
return m, tea.Quit
}
model.status = s3hubListBucketStatusBucketFetching
model.s3BucketListBucketStatus = s3hubListBucketStatusBucketFetching
return model, fetchS3BucketListCmd(model.ctx, model.app)
}

Expand Down
2 changes: 1 addition & 1 deletion ui/s3hub/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func (m *s3hubRootModel) updateChoices(msg tea.Msg) (tea.Model, tea.Cmd) {
m.err = err
return m, tea.Quit
}
model.status = s3hubListBucketStatusBucketFetching
model.s3BucketListBucketStatus = s3hubListBucketStatusBucketFetching
return model, fetchS3BucketListCmd(model.ctx, model.app)
case s3hubTopDeleteContentsChoice:
return &s3hubDeleteContentsModel{}, nil
Expand Down

0 comments on commit 7daa434

Please sign in to comment.