Skip to content

Commit

Permalink
add download manager
Browse files Browse the repository at this point in the history
  • Loading branch information
MrLYC committed Feb 26, 2022
1 parent c7f1f4b commit f9bca57
Show file tree
Hide file tree
Showing 16 changed files with 684 additions and 29 deletions.
25 changes: 23 additions & 2 deletions cmd/command/install.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package command

import (
"os"

"github.com/hashicorp/go-getter"
"github.com/spf13/cobra"

"github.com/mrlyc/cmdr/core"
"github.com/mrlyc/cmdr/core/manager"
"github.com/mrlyc/cmdr/core/utils"
)

Expand All @@ -15,9 +19,26 @@ var installCmd = &cobra.Command{
cfg := core.GetConfiguration()
cfg.Set(core.CfgKeyCmdrLinkMode, "default")
},
Run: runCommand(func(cfg core.Configuration, manager core.CommandManager) error {
Run: runCommand(func(cfg core.Configuration, mgr core.CommandManager) error {
tracker := utils.NewProgressBarTracker("downloading", os.Stderr)

downloader := utils.NewDownloader(
tracker,
[]getter.Detector{
new(getter.GitHubDetector),
new(getter.GitLabDetector),
new(getter.GitDetector),
new(getter.BitBucketDetector),
new(getter.S3Detector),
new(getter.GCSDetector),
},
nil,
)

downloadManager := manager.NewDownloadManager(mgr, downloader)

return defineCommand(
manager,
downloadManager,
cfg.GetString(core.CfgKeyXCommandInstallName),
cfg.GetString(core.CfgKeyXCommandInstallVersion),
cfg.GetString(core.CfgKeyXCommandInstallLocation),
Expand Down
2 changes: 1 addition & 1 deletion core/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ import "fmt"
var (
ErrCommandAlreadyActivated = fmt.Errorf("command already activated")
ErrShellNotSupported = fmt.Errorf("shell not supported")
ErrBinariesNotFound = fmt.Errorf("binaries not found")
ErrBinaryNotFound = fmt.Errorf("binaries not found")
ErrReleaseAssetNotFound = fmt.Errorf("release asset not found")
)
8 changes: 8 additions & 0 deletions core/fetcher.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package core

//go:generate mockgen -source=$GOFILE -destination=mock/$GOFILE -package=mock Fetcher

type Fetcher interface {
IsSupport(uri string) bool
Fetch(uri, dir string) error
}
2 changes: 1 addition & 1 deletion core/manager/binary.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func (f *BinariesFilter) All() ([]core.Command, error) {

func (f *BinariesFilter) One() (core.Command, error) {
if len(f.binaries) == 0 {
return nil, errors.Wrapf(core.ErrBinariesNotFound, "binaries not found")
return nil, errors.Wrapf(core.ErrBinaryNotFound, "binaries not found")
}

return f.binaries[0], nil
Expand Down
2 changes: 1 addition & 1 deletion core/manager/binary_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ var _ = Describe("Binary", func() {

It("should return error", func() {
_, err := filter.One()
Expect(errors.Cause(err)).To(Equal(core.ErrBinariesNotFound))
Expect(errors.Cause(err)).To(Equal(core.ErrBinaryNotFound))
})

It("should return 0", func() {
Expand Down
110 changes: 110 additions & 0 deletions core/manager/download.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package manager

import (
"io/fs"
"os"
"path/filepath"
"sort"
"strings"

"github.com/pkg/errors"

"github.com/mrlyc/cmdr/core"
)

type DownloadManager struct {
core.CommandManager
fetcher core.Fetcher
}

func (m *DownloadManager) search(name, output string) (string, error) {
type searchedFile struct {
path string
score float64
}

files := make([]searchedFile, 0, 1)
nameLength := float64(len(name))

err := filepath.Walk(output, func(path string, info fs.FileInfo, err error) error {
if err != nil {
return err
}

if info.IsDir() {
return nil
}

score := 0.0
if info.Mode()&0111 != 0 {
score = 0.1 / nameLength // perfer to choose executable file
}

file := filepath.Base(path)
if strings.Contains(file, name) {
score += nameLength / float64(len(file))
}

if score > 0 {
files = append(files, searchedFile{
path: path,
score: score,
})
}

return nil
})

if err != nil {
return "", errors.Wrapf(err, "failed to walk %s", output)
}

if len(files) == 0 {
return "", errors.Wrapf(core.ErrBinaryNotFound, "binary %s not found", name)
}

sort.SliceStable(files, func(i, j int) bool {
if files[i].score != files[j].score {
return files[i].score > files[j].score
}

return len(files[i].path) < len(files[j].path)
})

return files[0].path, nil
}

func (m *DownloadManager) fetch(name, version, location, output string) (string, error) {
err := m.fetcher.Fetch(location, output)
if err != nil {
return "", errors.Wrapf(err, "failed to download %s", location)
}

return m.search(name, output)
}

func (m *DownloadManager) Define(name string, version string, uri string) error {
if !m.fetcher.IsSupport(uri) {
return m.CommandManager.Define(name, version, uri)
}

dst, err := os.MkdirTemp("", "")
if err != nil {
return errors.Wrapf(err, "failed to create temp dir")
}
defer os.RemoveAll(dst)

location, err := m.fetch(name, version, uri, dst)
if err != nil {
return errors.Wrapf(err, "failed to fetch %s", location)
}

return m.CommandManager.Define(name, version, location)
}

func NewDownloadManager(manager core.CommandManager, fetcher core.Fetcher) *DownloadManager {
return &DownloadManager{
CommandManager: manager,
fetcher: fetcher,
}
}
133 changes: 133 additions & 0 deletions core/manager/download_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package manager_test

import (
"io/ioutil"
"os"
"path/filepath"

"github.com/golang/mock/gomock"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"

"github.com/mrlyc/cmdr/core/manager"
"github.com/mrlyc/cmdr/core/mock"
)

var _ = Describe("Download", func() {
var (
ctrl *gomock.Controller
)

BeforeEach(func() {
ctrl = gomock.NewController(GinkgoT())
})

AfterEach(func() {
ctrl.Finish()
})

Context("DownloadManager", func() {
var (
fetcher *mock.MockFetcher
baseManager *mock.MockCommandManager
downloadManager *manager.DownloadManager
name = "cmdr"
version = "1.0.0"
uri = ""
)

BeforeEach(func() {
fetcher = mock.NewMockFetcher(ctrl)
baseManager = mock.NewMockCommandManager(ctrl)
downloadManager = manager.NewDownloadManager(baseManager, fetcher)
})

It("should call base manager", func() {
fetcher.EXPECT().IsSupport(uri).Return(false)
baseManager.EXPECT().Define(name, version, uri)

Expect(downloadManager.Define(name, version, uri)).To(Succeed())
})

It("should call with downloaded file", func() {
var targetPath string

fetcher.EXPECT().IsSupport(uri).Return(true)
fetcher.EXPECT().Fetch(gomock.Any(), gomock.Any()).DoAndReturn(func(uri, dir string) error {
targetPath = filepath.Join(dir, "cmdr")
Expect(ioutil.WriteFile(targetPath, []byte(""), 0755)).To(Succeed())

return nil
})
baseManager.EXPECT().Define(name, version, gomock.Any()).DoAndReturn(func(name, version, location string) error {
Expect(targetPath).To(Equal(location))
return nil
})

Expect(downloadManager.Define(name, version, uri)).To(Succeed())
})

DescribeTable("fetch multiple files", func(files map[string]os.FileMode, expected string) {
var outputDir string

fetcher.EXPECT().IsSupport(uri).Return(true)
fetcher.EXPECT().Fetch(gomock.Any(), gomock.Any()).DoAndReturn(func(uri, dir string) error {
outputDir = dir

for path, mode := range files {
target := filepath.Join(dir, path)
Expect(os.MkdirAll(filepath.Dir(target), 0755)).To(Succeed())
Expect(ioutil.WriteFile(target, []byte(""), mode)).To(Succeed())
}

return nil
})

baseManager.EXPECT().Define(name, version, gomock.Any()).DoAndReturn(func(name, version, location string) error {
Expect(filepath.Rel(outputDir, location)).To(Equal(expected))
return nil
})

Expect(downloadManager.Define(name, version, uri)).To(Succeed())
},
Entry("single executable even name not match", map[string]os.FileMode{
"x": 0755,
}, "x"),
Entry("perfer to choose by name", map[string]os.FileMode{
"x": 0755,
"cmdr": 0644,
}, "cmdr"),
Entry("perfer to choose by name when name not match", map[string]os.FileMode{
"x": 0755,
"xx": 0644,
}, "x"),
Entry("single executable", map[string]os.FileMode{
"cmdr": 0755,
}, "cmdr"),
Entry("single file", map[string]os.FileMode{
"cmdr": 0644,
}, "cmdr"),
Entry("perfer to choose executable", map[string]os.FileMode{
"cmdr1": 0644,
"cmdr2": 0755,
}, "cmdr2"),
Entry("perfer to choose shorter name", map[string]os.FileMode{
"cmdr-with-long-name": 0755,
"cmdr-shortter": 0755,
}, "cmdr-shortter"),
Entry("perfer to choose shorter name even if it is not executable", map[string]os.FileMode{
"cmdr-with-long-name": 0755,
"cmdr-shortter": 0644,
}, "cmdr-shortter"),
Entry("perfer to choose executable even if it is in a sub directory", map[string]os.FileMode{
"cmdr-with-long-name": 0755,
"this/a/long/dir/for/cmdr-shortter": 0644,
}, "this/a/long/dir/for/cmdr-shortter"),
Entry("perfer to choose the shorter path when multiple files have same name", map[string]os.FileMode{
"cmdr": 0755,
"sub/dir/cmdr": 0755,
}, "cmdr"),
)
})
})
62 changes: 62 additions & 0 deletions core/mock/fetcher.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f9bca57

Please sign in to comment.