From 217c55052014705174fcca358757f5064ac6dfbe Mon Sep 17 00:00:00 2001 From: b4b4r07 Date: Thu, 24 Feb 2022 22:04:40 +0900 Subject: [PATCH] Refactor config and github config --- cmd/self-update.go | 13 +- pkg/config/gist.go | 24 ---- pkg/config/github.go | 309 ++----------------------------------------- pkg/config/http.go | 10 +- pkg/github/github.go | 241 +++++++++++++++++++++++++++++++++ 5 files changed, 264 insertions(+), 333 deletions(-) create mode 100644 pkg/github/github.go diff --git a/cmd/self-update.go b/cmd/self-update.go index 78b3b3e..6446082 100644 --- a/cmd/self-update.go +++ b/cmd/self-update.go @@ -14,8 +14,8 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/Masterminds/semver/v3" - "github.com/b4b4r07/afx/pkg/config" "github.com/b4b4r07/afx/pkg/errors" + "github.com/b4b4r07/afx/pkg/github" "github.com/b4b4r07/afx/pkg/helpers/templates" "github.com/creativeprojects/go-selfupdate" "github.com/fatih/color" @@ -110,18 +110,17 @@ func (c *selfUpdateCmd) selectTag(args []string) error { return errors.Wrap(err, "failed to get input from console") } - release := config.GitHubRelease{ - Name: "afx", - Client: http.DefaultClient, - Assets: config.Assets{}, - Filename: "", + release := github.Release{ + Name: "afx", + Client: http.DefaultClient, + Assets: github.Assets{}, } rel := gjson.Get(string(body), fmt.Sprintf("#(tag_name==\"%s\")", tag)) assets := rel.Get("assets") assets.ForEach(func(key, value gjson.Result) bool { name := value.Get("name").String() - release.Assets = append(release.Assets, config.Asset{ + release.Assets = append(release.Assets, github.Asset{ Name: name, Home: filepath.Join(os.Getenv("HOME"), ".afx"), Path: filepath.Join(os.Getenv("HOME"), ".afx", name), diff --git a/pkg/config/gist.go b/pkg/config/gist.go index 962a9fe..33fa7aa 100644 --- a/pkg/config/gist.go +++ b/pkg/config/gist.go @@ -2,10 +2,8 @@ package config import ( "context" - "encoding/json" "fmt" "log" - "net/http" "os" "path/filepath" @@ -28,28 +26,6 @@ type Gist struct { DependsOn []string `yaml:"depends-on"` } -func NewGist(owner, id string) (Gist, error) { - type data struct { - Description string `json:"description"` - } - resp, err := http.Get(fmt.Sprintf("https://api.github.com/gists/%s", id)) - if err != nil { - return Gist{}, err - } - defer resp.Body.Close() - var d data - err = json.NewDecoder(resp.Body).Decode(&d) - if err != nil { - return Gist{}, err - } - return Gist{ - Name: id, - Owner: owner, - ID: id, - Description: d.Description, - }, nil -} - // Init is func (c Gist) Init() error { var errs errors.Errors diff --git a/pkg/config/github.go b/pkg/config/github.go index adfe954..92e2352 100644 --- a/pkg/config/github.go +++ b/pkg/config/github.go @@ -3,27 +3,22 @@ package config import ( "context" "fmt" - "io" "io/ioutil" "log" "net/http" "os" "path/filepath" - "regexp" - "runtime" - "golang.org/x/oauth2" git "gopkg.in/src-d/go-git.v4" "gopkg.in/src-d/go-git.v4/config" "gopkg.in/src-d/go-git.v4/plumbing" "github.com/b4b4r07/afx/pkg/data" "github.com/b4b4r07/afx/pkg/errors" + "github.com/b4b4r07/afx/pkg/github" "github.com/b4b4r07/afx/pkg/logging" "github.com/b4b4r07/afx/pkg/state" "github.com/b4b4r07/afx/pkg/templates" - "github.com/google/go-github/github" - "github.com/mholt/archiver" "github.com/tidwall/gjson" ) @@ -38,7 +33,7 @@ type GitHub struct { Branch string `yaml:"branch"` Option *GitHubOption `yaml:"with"` - Release *Release `yaml:"release"` + Release *GitHubRelease `yaml:"release"` Plugin *Plugin `yaml:"plugin"` Command *Command `yaml:"command" validate:"required_with=Release"` // TODO: (not required Release) @@ -50,77 +45,19 @@ type GitHubOption struct { Depth int `yaml:"depth"` } -// Release represents a GitHub release structure -type Release struct { +// GitHubRelease represents a GitHub release structure +type GitHubRelease struct { Name string `yaml:"name" validate:"required"` Tag string `yaml:"tag"` - // TODO: (internal change): rename Artifact to Asset - Artifact Artifact `yaml:"asset"` + Asset GitHubReleaseAsset `yaml:"asset"` } -type Artifact struct { +type GitHubReleaseAsset struct { Filename string `yaml:"filename"` Replacements map[string]string `yaml:"replacements"` } -func NewGitHub(ctx context.Context, owner, repo string) (GitHub, error) { - r, err := getRepo(ctx, owner, repo) - if err != nil { - return GitHub{}, err - } - release, command := getRelease(ctx, owner, repo) - return GitHub{ - Name: repo, - Owner: owner, - Repo: repo, - Branch: "master", - Description: r.GetDescription(), - Plugin: nil, - Command: command, - Release: release, - }, nil -} - -func githubClient(ctx context.Context) *github.Client { - token := os.Getenv("GITHUB_TOKEN") - - ts := oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: token}, - ) - tc := oauth2.NewClient(ctx, ts) - - return github.NewClient(tc) -} - -func getRepo(ctx context.Context, owner, repo string) (*github.Repository, error) { - c := githubClient(ctx) - r, _, err := c.Repositories.Get(ctx, owner, repo) - return r, err -} - -func getRelease(ctx context.Context, owner, repo string) (*Release, *Command) { - var release *Release - var command *Command - c := githubClient(ctx) - latest, _, err := c.Repositories.GetLatestRelease( - ctx, owner, repo, - ) - if err == nil { - release = &Release{ - Name: repo, - Tag: latest.GetTagName(), - } - command = &Command{ - Link: []*Link{{ - From: repo, - To: repo, - }}, - } - } - return release, command -} - // Init runs initialization step related to GitHub packages func (c GitHub) Init() error { var errs errors.Errors @@ -283,7 +220,7 @@ func (c GitHub) InstallFromRelease(ctx context.Context) error { req, err := http.NewRequest(http.MethodGet, c.ReleaseURL(), nil) if err != nil { return errors.Wrapf(err, - "failed to complete the request to %v to fetch artifact list", + "failed to complete the request to %v to fetch asset list", c.ReleaseURL()) } @@ -321,16 +258,16 @@ func (c GitHub) InstallFromRelease(ctx context.Context) error { return errors.Wrapf(err, "failed to template filename") } - release := GitHubRelease{ + release := github.Release{ Name: c.Release.Name, Client: httpClient, - Assets: Assets{}, + Assets: github.Assets{}, Filename: filename, } assets.ForEach(func(key, value gjson.Result) bool { name := value.Get("name").String() - release.Assets = append(release.Assets, Asset{ + release.Assets = append(release.Assets, github.Asset{ Name: name, Home: c.GetHome(), Path: filepath.Join(c.GetHome(), name), @@ -356,8 +293,8 @@ func (c GitHub) InstallFromRelease(ctx context.Context) error { } func (c GitHub) templateFilename() (string, error) { - filename := c.Release.Artifact.Filename - replacements := c.Release.Artifact.Replacements + filename := c.Release.Asset.Filename + replacements := c.Release.Asset.Replacements if filename == "" { // no filename specified @@ -386,228 +323,6 @@ func (c GitHub) templateFilename() (string, error) { return filename, nil } -// GitHubRelease represents a GitHub release and its client -// A difference from Release is whether a client or not -type GitHubRelease struct { - Client *http.Client - - Name string - Assets Assets - - Filename string -} - -// Asset represents GitHub release's asset. -// Basically this means one archive file attached in a release -type Asset struct { - Name string - Home string - Path string - URL string -} - -type Assets []Asset - -func (as *Assets) filter(fn func(Asset) bool) *Assets { - var assets Assets - if len(*as) < 2 { - // no more need to filter - log.Printf("[DEBUG] assets.filter: finished filtering because length of assets is less than two") - return as - } - - for _, asset := range *as { - if fn(asset) { - assets = append(assets, asset) - } - } - - // logging if assets are changed by filter - if len(*as) != len(assets) { - log.Printf("[DEBUG] assets.filter: filtered: %#v", getAssetKeys(assets)) - } - - *as = assets - return as -} - -// getAssetKeys just returns list of asset.Name -func getAssetKeys(assets []Asset) []string { - var names []string - for _, asset := range assets { - names = append(names, asset.Name) - } - return names -} - -func (r *GitHubRelease) GetAsset() (Asset, error) { - log.Printf("[DEBUG] assets: %#v\n", getAssetKeys(r.Assets)) - - if len(r.Assets) == 0 { - return Asset{}, fmt.Errorf("%s: no assets found", r.Name) - } - - if r.Filename != "" { - log.Printf("[DEBUG] asset: found filename %q is specified in config", r.Filename) - for _, asset := range r.Assets { - if asset.Name == r.Filename { - log.Printf("[DEBUG] asset: filename %q is matched with assets", r.Filename) - return asset, nil - } - } - return Asset{}, fmt.Errorf("%s: no matched in assets", r.Filename) - } - - assets := *r.Assets. - filter(func(asset Asset) bool { - expr := `.*\.sbom` - // filter out - return !regexp.MustCompile(expr).MatchString(asset.Name) - }). - filter(func(asset Asset) bool { - expr := ".*(sha256sum|checksum).*" - // filter out - return !regexp.MustCompile(expr).MatchString(asset.Name) - }). - filter(func(asset Asset) bool { - expr := "" - switch runtime.GOOS { - case "darwin": - expr += ".*(apple|darwin|Darwin|osx|mac|macos|macOS).*" - case "linux": - expr += ".*(linux|hoe).*" - } - return regexp.MustCompile(expr).MatchString(asset.Name) - }). - filter(func(asset Asset) bool { - expr := "" - switch runtime.GOARCH { - case "amd64": - expr += ".*(amd64|64).*" - case "386": - expr += ".*(386|86).*" - } - return regexp.MustCompile(expr).MatchString(asset.Name) - }) - - switch len(assets) { - case 0: - return Asset{}, errors.New("asset not found after filtered") - case 1: - return assets[0], nil - default: - log.Printf("[WARN] %d assets found: %#v", len(assets), getAssetKeys(assets)) - log.Printf("[WARN] first one %q will be used", assets[0].Name) - return assets[0], nil - } -} - -// Download is -func (r *GitHubRelease) Download(ctx context.Context) (Asset, error) { - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - asset, err := r.GetAsset() - if err != nil { - return asset, err - } - - log.Printf("[DEBUG] asset: %#v", asset) - - req, err := http.NewRequest(http.MethodGet, asset.URL, nil) - if err != nil { - return asset, err - } - - client := new(http.Client) - resp, err := client.Do(req.WithContext(ctx)) - if err != nil { - return asset, err - } - defer resp.Body.Close() - - os.MkdirAll(asset.Home, os.ModePerm) - file, err := os.Create(asset.Path) - if err != nil { - return asset, errors.Wrapf(err, "%s: failed to create file", asset.Path) - } - defer file.Close() - _, err = io.Copy(file, resp.Body) - - return asset, err -} - -// Unarchive is -func (r *GitHubRelease) Unarchive(asset Asset) error { - uaIface, err := archiver.ByExtension(asset.Path) - if err != nil { - // err: this will be an error of format unrecognized by filename - // but in this case, maybe not archived file: e.g. tigrawap/slit - // - log.Printf("[WARN] archiver.ByExtension(): %v", err) - - // TODO: remove this logic? - // thanks to this logic, we don't need to specify this statement to link.from - // - // command: - // link: - // - from: '*jq*' - // - // because this logic renames a binary of 'jq-1.6' to 'jq' - // - target := filepath.Join(asset.Home, r.Name) - if _, err := os.Stat(target); err != nil { - log.Printf("[DEBUG] renamed from %s to %s", asset.Path, target) - os.Rename(asset.Path, target) - os.Chmod(target, 0755) - } - return nil - } - - tar := &archiver.Tar{ - OverwriteExisting: true, - MkdirAll: false, - ImplicitTopLevelFolder: false, - ContinueOnError: false, - } - switch v := uaIface.(type) { - case *archiver.Rar: - v.OverwriteExisting = true - case *archiver.Zip: - v.OverwriteExisting = true - case *archiver.TarBz2: - v.Tar = tar - case *archiver.TarGz: - v.Tar = tar - case *archiver.TarLz4: - v.Tar = tar - case *archiver.TarSz: - v.Tar = tar - case *archiver.TarXz: - v.Tar = tar - case *archiver.Gz, - *archiver.Bz2, - *archiver.Lz4, - *archiver.Snappy, - *archiver.Xz: - // nothing to customise - } - - u, ok := uaIface.(archiver.Unarchiver) - if !ok { - return errors.New("cannot type assertion with archiver.Unarchiver") - } - - if err := u.Unarchive(asset.Path, asset.Home); err != nil { - return errors.Wrapf(err, "%s: failed to unarchive", r.Name) - } - - log.Printf("[DEBUG] removed archive file: %s", asset.Path) - os.Remove(asset.Path) - - return nil -} - // HasPluginBlock is func (c GitHub) HasPluginBlock() bool { return c.Plugin != nil diff --git a/pkg/config/http.go b/pkg/config/http.go index c5fe804..a8e70a3 100644 --- a/pkg/config/http.go +++ b/pkg/config/http.go @@ -114,13 +114,13 @@ func unarchive(f string) error { switch { case filetype.IsArchive(buf): if err := archiver.Unarchive(f, filepath.Dir(f)); err != nil { - return err + return errors.Wrapf(err, "%s: failed to unarhive", f) } - return nil default: - log.Printf("[INFO] %s no need to unarchive\n", f) - return nil + log.Printf("[DEBUG] %s: no need to unarchive", f) } + + return nil } // Installed is @@ -182,7 +182,7 @@ func (c HTTP) Uninstall(ctx context.Context) error { errs.Append(err) return } - log.Printf("[INFO] Delete %s\n", f) + log.Printf("[INFO] Delete %s", f) } if c.HasCommandBlock() { diff --git a/pkg/github/github.go b/pkg/github/github.go new file mode 100644 index 0000000..343198b --- /dev/null +++ b/pkg/github/github.go @@ -0,0 +1,241 @@ +package github + +import ( + "context" + "fmt" + "io" + "log" + "net/http" + "os" + "path/filepath" + "regexp" + "runtime" + + "github.com/b4b4r07/afx/pkg/errors" + "github.com/mholt/archiver" +) + +// Release represents a GitHub release and its client +// A difference from Release is whether a client or not +type Release struct { + Client *http.Client + + Name string + Assets Assets + + // Filename is used for specifying a filename directly in release assets. + // Normally it requires to filter release assets based on OS/Arch information. + // But by doing this field, don't need to filter assets. + Filename string +} + +// Asset represents GitHub release's asset. +// Basically this means one archive file attached in a release +type Asset struct { + Name string + Home string + Path string + URL string +} + +type Assets []Asset + +func (as *Assets) filter(fn func(Asset) bool) *Assets { + var assets Assets + if len(*as) < 2 { + // no more need to filter + log.Printf("[DEBUG] assets.filter: finished filtering because length of assets is less than two") + return as + } + + for _, asset := range *as { + if fn(asset) { + assets = append(assets, asset) + } + } + + // logging if assets are changed by filter + if len(*as) != len(assets) { + log.Printf("[DEBUG] assets.filter: filtered: %#v", getAssetKeys(assets)) + } + + *as = assets + return as +} + +// getAssetKeys just returns list of asset.Name +func getAssetKeys(assets []Asset) []string { + var names []string + for _, asset := range assets { + names = append(names, asset.Name) + } + return names +} + +func (r *Release) GetAsset() (Asset, error) { + log.Printf("[DEBUG] assets: %#v\n", getAssetKeys(r.Assets)) + + if len(r.Assets) == 0 { + return Asset{}, fmt.Errorf("%s: no assets found", r.Name) + } + + if r.Filename != "" { + log.Printf("[DEBUG] asset: found filename %q is specified in config", r.Filename) + for _, asset := range r.Assets { + if asset.Name == r.Filename { + log.Printf("[DEBUG] asset: filename %q is matched with assets", r.Filename) + return asset, nil + } + } + return Asset{}, fmt.Errorf("%s: no matched in assets", r.Filename) + } + + assets := *r.Assets. + filter(func(asset Asset) bool { + expr := `.*\.sbom` + // filter out + return !regexp.MustCompile(expr).MatchString(asset.Name) + }). + filter(func(asset Asset) bool { + expr := ".*(sha256sum|checksum).*" + // filter out + return !regexp.MustCompile(expr).MatchString(asset.Name) + }). + filter(func(asset Asset) bool { + expr := "" + switch runtime.GOOS { + case "darwin": + expr += ".*(apple|darwin|Darwin|osx|mac|macos|macOS).*" + case "linux": + expr += ".*(linux|hoe).*" + } + return regexp.MustCompile(expr).MatchString(asset.Name) + }). + filter(func(asset Asset) bool { + expr := "" + switch runtime.GOARCH { + case "amd64": + expr += ".*(amd64|64).*" + case "386": + expr += ".*(386|86).*" + } + return regexp.MustCompile(expr).MatchString(asset.Name) + }) + + switch len(assets) { + case 0: + return Asset{}, errors.New("asset not found after filtered") + case 1: + return assets[0], nil + default: + log.Printf("[WARN] %d assets found: %#v", len(assets), getAssetKeys(assets)) + log.Printf("[WARN] first one %q will be used", assets[0].Name) + return assets[0], nil + } +} + +// Download is +func (r *Release) Download(ctx context.Context) (Asset, error) { + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + asset, err := r.GetAsset() + if err != nil { + return asset, err + } + + log.Printf("[DEBUG] asset: %#v", asset) + + req, err := http.NewRequest(http.MethodGet, asset.URL, nil) + if err != nil { + return asset, err + } + + client := new(http.Client) + resp, err := client.Do(req.WithContext(ctx)) + if err != nil { + return asset, err + } + defer resp.Body.Close() + + os.MkdirAll(asset.Home, os.ModePerm) + file, err := os.Create(asset.Path) + if err != nil { + return asset, errors.Wrapf(err, "%s: failed to create file", asset.Path) + } + defer file.Close() + _, err = io.Copy(file, resp.Body) + + return asset, err +} + +// Unarchive is +func (r *Release) Unarchive(asset Asset) error { + uaIface, err := archiver.ByExtension(asset.Path) + if err != nil { + // err: this will be an error of format unrecognized by filename + // but in this case, maybe not archived file: e.g. tigrawap/slit + // + log.Printf("[WARN] archiver.ByExtension(): %v", err) + + // TODO: remove this logic? + // thanks to this logic, we don't need to specify this statement to link.from + // + // command: + // link: + // - from: '*jq*' + // + // because this logic renames a binary of 'jq-1.6' to 'jq' + // + target := filepath.Join(asset.Home, r.Name) + if _, err := os.Stat(target); err != nil { + log.Printf("[DEBUG] renamed from %s to %s", asset.Path, target) + os.Rename(asset.Path, target) + os.Chmod(target, 0755) + } + return nil + } + + tar := &archiver.Tar{ + OverwriteExisting: true, + MkdirAll: false, + ImplicitTopLevelFolder: false, + ContinueOnError: false, + } + switch v := uaIface.(type) { + case *archiver.Rar: + v.OverwriteExisting = true + case *archiver.Zip: + v.OverwriteExisting = true + case *archiver.TarBz2: + v.Tar = tar + case *archiver.TarGz: + v.Tar = tar + case *archiver.TarLz4: + v.Tar = tar + case *archiver.TarSz: + v.Tar = tar + case *archiver.TarXz: + v.Tar = tar + case *archiver.Gz, + *archiver.Bz2, + *archiver.Lz4, + *archiver.Snappy, + *archiver.Xz: + // nothing to customise + } + + u, ok := uaIface.(archiver.Unarchiver) + if !ok { + return errors.New("cannot type assertion with archiver.Unarchiver") + } + + if err := u.Unarchive(asset.Path, asset.Home); err != nil { + return errors.Wrapf(err, "%s: failed to unarchive", r.Name) + } + + log.Printf("[DEBUG] removed archive file: %s", asset.Path) + os.Remove(asset.Path) + + return nil +}