Skip to content

Commit

Permalink
feat(upgrade): support upgrade ssx online
Browse files Browse the repository at this point in the history
  • Loading branch information
vimiix committed Jun 12, 2024
1 parent fbe02a0 commit b33cb84
Show file tree
Hide file tree
Showing 9 changed files with 376 additions and 4 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ test: ## run all unit tests
$(GO) test -gcflags=all=-l $(TEST_FILES) -coverprofile dist/cov.out -covermode count

.PHONY: ssx
ssx: lint ## build ssx binary
ssx: ## build ssx binary
$(GO) build -ldflags '$(LDFLAGS)' -gcflags '-N -l' -o dist/ssx ./cmd/ssx/main.go

.PHONY: tag
Expand Down
1 change: 1 addition & 0 deletions cmd/ssx/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ ssx 100 pwd`,
root.AddCommand(newDeleteCmd())
root.AddCommand(newTagCmd())
root.AddCommand(newInfoCmd())
root.AddCommand(newUpgradeCmd())

root.CompletionOptions.HiddenDefaultCmd = true
root.SetHelpCommand(&cobra.Command{Hidden: true})
Expand Down
188 changes: 188 additions & 0 deletions cmd/ssx/cmd/upgrade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package cmd

import (
"context"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"github.com/tidwall/gjson"
"github.com/vimiix/ssx/internal/file"
"github.com/vimiix/ssx/internal/lg"
"github.com/vimiix/ssx/internal/utils"
)

const (
GITHUB_LATEST_API = "https://api.github.com/repos/vimiix/ssx/releases/latest"
GITHUB_PKG_FMT = "https://github.com/vimiix/ssx/releases/download/v{VERSION}/ssx_v{VERSION}_{OS}_{ARCH}.tar.gz"
)

type upgradeOpt struct {
PkgPath string
Version string
}

func newUpgradeCmd() *cobra.Command {
opt := &upgradeOpt{}
cmd := &cobra.Command{
Use: "upgrade",
Short: "upgrade ssx version",
Example: `# Upgrade online
ssx upgrade [<version>]
# Upgrade with local filepath or specify new package URL path
ssx upgrade -p <PATH>
# If both version and package path are specified,
# ssx prefer to use package path.`,
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
opt.Version = args[0]
}
return upgrade(cmd.Context(), opt)
}}
cmd.Flags().StringVarP(&opt.PkgPath, "package", "p", "", "new package file or URL path")
return cmd
}

func unifyArch() (string, error) {
switch runtime.GOARCH {
case "amd64", "x86_64":
return "x86_64", nil
case "arm64", "aarch64":
return "arm64", nil
default:
return "", errors.Errorf("not supported architecture: %s", runtime.GOARCH)
}
}

func upgrade(ctx context.Context, opt *upgradeOpt) error {
tempDir, err := os.MkdirTemp("", "*")
if err != nil {
return err
}
lg.Debug("make temp dir: %s", tempDir)
defer os.RemoveAll(tempDir)
var localPkg string
if opt.PkgPath != "" {
if strings.Contains(opt.PkgPath, "://") {
localPkg = filepath.Join(tempDir, "ssx.tar.gz")
lg.Info("downloading package from %s", opt.PkgPath)
if err := utils.DownloadFile(ctx, opt.PkgPath, localPkg); err != nil {
return err
}
} else {
if !file.IsExist(opt.PkgPath) {
return errors.Errorf("file not found: %s", opt.PkgPath)
}
localPkg = opt.PkgPath
}
} else if opt.Version != "" {
semVer := strings.TrimPrefix(opt.Version, "v")
if len(strings.Split(semVer, ".")) != 3 {
return errors.Errorf("bad version: %s", opt.Version)
}
arch, err := unifyArch()
if err != nil {
return err
}
replacer := strings.NewReplacer("{VERSION}", semVer, "{OS}", runtime.GOOS, "{ARCH}", arch)
urlStr := replacer.Replace(GITHUB_PKG_FMT)
localPkg = filepath.Join(tempDir, "ssx.tar.gz")
lg.Info("downloading package from %s", urlStr)
if err := utils.DownloadFile(ctx, urlStr, localPkg); err != nil {
return err
}
} else {
lg.Info("detecting latest package url")
urlStr, err := getLatestPkgURL()
if err != nil {
return err
}
if urlStr == "" {
return errors.New("failed to get latest package url")
}
localPkg = filepath.Join(tempDir, "ssx.tar.gz")
lg.Info("downloading latest package from %s", urlStr)
if err := utils.DownloadFile(ctx, urlStr, localPkg); err != nil {
return err
}
}
lg.Info("extracting package")
if err := utils.Untar(localPkg, tempDir); err != nil {
return err
}
newBin := filepath.Join(tempDir, "ssx")
if !file.IsExist(newBin) {
return errors.New("not found ssx binary after extracting package")
}
execPath, err := os.Executable()
if err != nil {
return err
}
execAbsPath, err := filepath.Abs(execPath)
if err != nil {
return err
}
lg.Info("replacing old binary with new binary")
if err := replaceBinary(newBin, execAbsPath); err != nil {
return err
}
lg.Info("upgrade success")
return nil
}

func getLatestPkgURL() (string, error) {
arch, err := unifyArch()
if err != nil {
return "", err
}
r, err := http.Get(GITHUB_LATEST_API)
if err != nil {
return "", err
}
defer r.Body.Close()
jsonBody, err := io.ReadAll(r.Body)
if err != nil {
return "", err
}

res := gjson.Get(string(jsonBody),
fmt.Sprintf(`assets.#(name%%"*%s_%s.tar.gz").browser_download_url`, runtime.GOOS, arch))
return res.String(), nil
}

func replaceBinary(newBin string, oldBin string) error {
bakBin := oldBin + ".bak"
lg.Debug("backup old binary from %s to %s", oldBin, bakBin)
if err := os.Link(oldBin, bakBin); err != nil {
return err
}
// if err := file.CopyFile(oldBin, bakName, 0700); err != nil {
// return err
// }

lg.Debug("remove old binary")
if err := os.RemoveAll(oldBin); err != nil {
return err
}

lg.Debug("make the new binary effective")
if err := file.CopyFile(newBin, oldBin, 0700); err != nil {
_ = os.RemoveAll(oldBin)
renameErr := os.Rename(bakBin, oldBin)
if renameErr != nil {
lg.Warn("restore old binary failed, please rename it manually\n"+
" mv %s %s", bakBin, oldBin)
}
return err
}
_ = os.RemoveAll(bakBin)
return nil
}
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,8 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/tidwall/gjson v1.17.1 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/vimiix/tablewriter v0.0.0-20231207073205-aad9e2006284 h1:7o3B9eLdW6tgtWcgP0TVnq9QAUFu5Ii/RoaFOkEMYbc=
github.com/vimiix/tablewriter v0.0.0-20231207073205-aad9e2006284/go.mod h1:uQpPcEuo28DE69kbtdWpMfeB+el/Kaeh2hCEdrz1iKI=
go.etcd.io/bbolt v1.3.10 h1:+BqfJTcCzTItrop8mq/lbzL8wSGtj94UO/3U31shqG0=
Expand Down
2 changes: 0 additions & 2 deletions internal/encrypt/encrypt.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
// Copyright 2022 Enmotech Inc. All rights reserved.

package encrypt

import (
Expand Down
34 changes: 34 additions & 0 deletions internal/file/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package file

import (
"io"
"os"
)

// CopyFile copies the contents of src to dst
func CopyFile(src, dst string, perm os.FileMode) error {
sf, err := os.Open(src)
if err != nil {
return err
}
defer sf.Close()
tf, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
defer tf.Close()
_, err = io.Copy(tf, sf)
return err
}

// IsExist check given path if exists
func IsExist(path string) bool {
if path == "" {
return false
}
_, err := os.Stat(path)
if err != nil {
return os.IsExist(err)
}
return true
}
Loading

0 comments on commit b33cb84

Please sign in to comment.