Skip to content

Commit

Permalink
feat: complete basic functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
beetcb committed Feb 9, 2022
1 parent 3da51fc commit 41daa61
Show file tree
Hide file tree
Showing 9 changed files with 1,302 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
vendor/

# Go workspace file
go.work
Empty file added README.md
Empty file.
56 changes: 56 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package main

import (
"os"
"strings"

"github.com/beetcb/ghdl"
"github.com/spf13/cobra"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "gh-dl <user/repo[#tagname]>",
Short: "gh-dl download binary from github release",
Long: `gh-dl download binary from github release
gh-dl handles archived or compressed file as well`,
Args: cobra.MinimumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
repo, tag := parseArg(args[0])
ghRelease := ghdl.GHRelease{RepoPath: repo, TagName: tag}
url, binaryName, err := ghRelease.GetGHReleases()
ghReleaseDl := ghdl.GHReleaseDl{Url: url, BinaryName: binaryName}
if err != nil {
panic(err)
}
binaryNameFlag, err := cmd.Flags().GetString("bin")
if err != nil {
panic(err)
}
if binaryNameFlag != "" {
binaryName = binaryNameFlag
}
ghReleaseDl.DlAndDecompression()
},
}

func main() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}

func init() {
rootCmd.PersistentFlags().StringP("bin", "b", "", "specify bin file name")
}

// parse user/repo[#tagname] arg
func parseArg(repoPath string) (repo string, tag string) {
seperateTag := strings.Split(repoPath, "#")
if len(seperateTag) == 2 {
tag = seperateTag[1]
}
repo = seperateTag[0]
return repo, tag
}
176 changes: 176 additions & 0 deletions dl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package ghdl

import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"

"github.com/beetcb/ghdl/helper/pg"
)

type GHReleaseDl struct {
BinaryName string
Url string
}

func (dl *GHReleaseDl) DlAndDecompression() {
b := dl.BinaryName + func() string {
if runtime.GOOS == "windows" {
return ".exe"
} else {
return ""
}
}()

req, err := http.NewRequest("GET", dl.Url, nil)
if err != nil {
panic(err)
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
fileSize, err := strconv.Atoi(resp.Header.Get("Content-Length"))
if err != nil {
panic(err)
}

file, err := ioutil.TempFile("", "*")
if err != nil {
panic(err)
}
defer os.Remove(file.Name())

// create progress tui
starter := func(updater func(float64)) {
if _, err := io.Copy(file, &pg.ProgressBytesReader{Reader: resp.Body, Handler: func(p int) {
updater(float64(p) / float64(fileSize))
}}); err != nil {
panic(err)
}
}
pg.Progress(starter)

// `file` has no content, we must open it for reading
openfile, err := os.Open(file.Name())
if err != nil {
panic(err)
}
filebody, err := ioutil.ReadAll(openfile)
if err != nil {
panic(err)
}
bytesReader := bytes.NewReader(filebody)

fileExt := filepath.Ext(dl.Url)
var decompressedBinary *[]byte
switch fileExt {
case ".zip":
decompressedBinary, err = dl.zipBinary(bytesReader, b)
if err != nil {
panic(err)
}
case ".gz":
if strings.Contains(dl.Url, ".tar.gz") {
decompressedBinary, err = dl.targzBinary(bytesReader, b)
if err != nil {
panic(err)
}
} else {
decompressedBinary, err = dl.gzBinary(bytesReader, b)
if err != nil {
panic(err)
}
}
case ".deb":
case ".rpm":
case ".apk":
fileName := b + fileExt
fmt.Printf("Detected deb/rpm/apk package, download directly to ./%s\nYou can install it with the appropriate commands\n", fileName)
if err := os.WriteFile(fileName, filebody, 0777); err != nil {
panic(err)
}
case "":
decompressedBinary = &filebody
default:
panic("unsupported file format")
}
if err := os.WriteFile(b, *decompressedBinary, 0777); err != nil {
panic(err)
}
}

func (*GHReleaseDl) zipBinary(r *bytes.Reader, b string) (*[]byte, error) {
zipR, err := zip.NewReader(r, int64(r.Len()))
if err != nil {
return nil, err
}

for _, f := range zipR.File {
if filepath.Base(f.Name) == b || len(zipR.File) == 1 {
open, err := f.Open()
if err != nil {
return nil, err
}
ret, err := ioutil.ReadAll(open)
if err != nil {
return nil, err
}
return &ret, err
}
}
return nil, fmt.Errorf("Binary file %v not found", b)
}

func (*GHReleaseDl) gzBinary(r *bytes.Reader, b string) (*[]byte, error) {
gzR, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzR.Close()
ret, err := ioutil.ReadAll(gzR)
if err != nil {
return nil, err
}
return &ret, nil
}

func (*GHReleaseDl) targzBinary(r *bytes.Reader, b string) (*[]byte, error) {
gzR, err := gzip.NewReader(r)
if err != nil {
return nil, err
}
defer gzR.Close()
tarR := tar.NewReader(gzR)

var file []byte
for {
header, err := tarR.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if (header.Typeflag != tar.TypeDir) && filepath.Base(header.Name) == b {
file, err = ioutil.ReadAll(tarR)
if err != nil {
return nil, err
}
break
}
}
return &file, nil
}
94 changes: 94 additions & 0 deletions gh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package ghdl

import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"runtime"
"strings"

"github.com/beetcb/ghdl/helper/sl"
)

const (
OS = runtime.GOOS
ARCH = runtime.GOARCH
)

type GHRelease struct {
RepoPath string
TagName string
}

type APIReleaseResp struct {
Assets []APIReleaseAsset `json:"assets"`
}

type APIReleaseAsset struct {
Name string `json:"name"`
DownloadUrl string `json:"browser_download_url"`
}

func (gr *GHRelease) GetGHReleases() (string, string, error) {
var tag string
if gr.TagName == "" {
tag = "latest"
} else {
tag = "tags/" + gr.TagName
}
binaryName := filepath.Base(gr.RepoPath)
apiUrl := fmt.Sprint("https://api.github.com/repos/", gr.RepoPath, "/releases/", tag)

// Get releases info
req, err := http.NewRequest("GET", apiUrl, nil)
if err != nil {
return "", "", err
}
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", "", err
} else if resp.StatusCode != http.StatusOK {
return "", "", fmt.Errorf("requst to %v failed: %v", apiUrl, resp.Status)
}
defer resp.Body.Close()
byte, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", "", err
}
var respJSON APIReleaseResp
if err := json.Unmarshal(byte, &respJSON); err != nil {
return "", "", err
}
releaseAssets := respJSON.Assets
if len(releaseAssets) == 0 {
return "", "", fmt.Errorf("no binary release found")
}

// Pick release assets
matchedAssets := filterAssets(filterAssets(releaseAssets, OS), ARCH)
if len(matchedAssets) != 1 {
var choices []string
for _, asset := range matchedAssets {
choices = append(choices, asset.Name)
}
idx := sl.Select(&choices)
return matchedAssets[idx].DownloadUrl, binaryName, nil
}
return matchedAssets[0].DownloadUrl, binaryName, nil
}

// Filter assets by match pattern, falling back to the default assets if no match is found
func filterAssets(assets []APIReleaseAsset, match string) (ret []APIReleaseAsset) {
for _, asset := range assets {
if strings.Contains(strings.ToLower(asset.Name), match) {
ret = append(ret, asset)
}
}
if len(ret) == 0 {
return assets
}
return ret
}
10 changes: 10 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module github.com/beetcb/ghdl

go 1.16

require (
github.com/charmbracelet/bubbles v0.10.2
github.com/charmbracelet/bubbletea v0.19.3
github.com/charmbracelet/lipgloss v0.4.0
github.com/spf13/cobra v1.3.0
)
Loading

0 comments on commit 41daa61

Please sign in to comment.