mirror of https://github.com/synctv-org/synctv
Feat: self-update command
parent
b7af32adfd
commit
2285bc86b9
@ -0,0 +1,34 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/synctv-org/synctv/internal/conf"
|
||||
"github.com/synctv-org/synctv/internal/version"
|
||||
)
|
||||
|
||||
const SelfUpdateLong = `self-update command will update synctv-server binary to latest version.
|
||||
Version check url: https://github.com/synctv-org/synctv/releases/latest
|
||||
|
||||
If use '--dev' flag, will update to latest dev version always.`
|
||||
|
||||
var SelfUpdateCmd = &cobra.Command{
|
||||
Use: "self-update",
|
||||
Short: "self-update",
|
||||
Long: SelfUpdateLong,
|
||||
PersistentPreRunE: Init,
|
||||
RunE: SelfUpdate,
|
||||
}
|
||||
|
||||
func SelfUpdate(cmd *cobra.Command, args []string) error {
|
||||
v, err := version.NewVersionInfo(version.WithBaseURL(conf.Conf.Global.GitHubBaseURL))
|
||||
if err != nil {
|
||||
log.Errorf("get version info error: %v", err)
|
||||
return err
|
||||
}
|
||||
return v.SelfUpdate(cmd.Context())
|
||||
}
|
||||
|
||||
func init() {
|
||||
RootCmd.AddCommand(SelfUpdateCmd)
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
package conf
|
||||
|
||||
type GlobalConfig struct {
|
||||
GitHubBaseURL string `yaml:"github_base_url" lc:"default: https://api.github.com/" env:"GITHUB_BASE_URL"`
|
||||
}
|
||||
|
||||
func DefaultGlobalConfig() GlobalConfig {
|
||||
return GlobalConfig{}
|
||||
return GlobalConfig{
|
||||
GitHubBaseURL: "https://api.github.com/",
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,110 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/cavaliergopher/grab/v3"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func SelfUpdate(ctx context.Context, url string) error {
|
||||
now := time.Now().UnixNano()
|
||||
currentExecFile, err := ExecutableFile()
|
||||
if err != nil {
|
||||
log.Errorf("self update: get current executable file error: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Infof("self update: current executable file: %s", currentExecFile)
|
||||
|
||||
tmp := filepath.Join(os.TempDir(), "synctv-server", fmt.Sprintf("self-update-%d", now))
|
||||
if err := os.MkdirAll(tmp, 0755); err != nil {
|
||||
log.Errorf("self update: mkdir %s error: %v", tmp, err)
|
||||
return err
|
||||
}
|
||||
log.Infof("self update: temp path: %s", tmp)
|
||||
defer func() {
|
||||
log.Infof("self update: remove temp path: %s", tmp)
|
||||
if err := os.RemoveAll(tmp); err != nil {
|
||||
log.Warnf("self update: remove temp path error: %v", err)
|
||||
}
|
||||
}()
|
||||
file, err := DownloadWithProgress(ctx, url, tmp)
|
||||
if err != nil {
|
||||
log.Errorf("self update: download %s error: %v", url, err)
|
||||
return err
|
||||
}
|
||||
log.Infof("self update: download success: %s", file)
|
||||
|
||||
if err := os.Chmod(file, 0755); err != nil {
|
||||
log.Errorf("self update: chmod %s error: %v", file, err)
|
||||
return err
|
||||
}
|
||||
log.Infof("self update: chmod success: %s", file)
|
||||
|
||||
oldName := fmt.Sprintf("%s-%d.old", currentExecFile, now)
|
||||
if err := os.Rename(currentExecFile, oldName); err != nil {
|
||||
log.Errorf("self update: rename %s -> %s error: %v", currentExecFile, oldName, err)
|
||||
return err
|
||||
}
|
||||
log.Infof("self update: rename success: %s -> %s", currentExecFile, oldName)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Infof("self update: rollback: %s -> %s", oldName, currentExecFile)
|
||||
if err := os.Rename(oldName, currentExecFile); err != nil {
|
||||
log.Errorf("self update: rollback: rename %s -> %s error: %v", oldName, currentExecFile, err)
|
||||
}
|
||||
} else {
|
||||
log.Infof("self update: remove old executable file: %s", oldName)
|
||||
if err := os.Remove(oldName); err != nil {
|
||||
log.Warnf("self update: remove old executable file error: %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := os.Rename(file, currentExecFile); err != nil {
|
||||
log.Errorf("self update: rename %s -> %s error: %v", file, currentExecFile, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("self update: update success: %s", currentExecFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DownloadWithProgress(ctx context.Context, url, path string) (string, error) {
|
||||
req, err := grab.NewRequest(path, url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req = req.WithContext(ctx)
|
||||
resp := grab.NewClient().Do(req)
|
||||
t := time.NewTicker(250 * time.Millisecond)
|
||||
defer t.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-t.C:
|
||||
log.Infof("self update: transferred %d / %d bytes (%.2f%%)",
|
||||
resp.BytesComplete(),
|
||||
resp.Size(),
|
||||
100*resp.Progress())
|
||||
|
||||
case <-resp.Done:
|
||||
return resp.Filename, resp.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get current executable file
|
||||
func ExecutableFile() (string, error) {
|
||||
p, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.EvalSymlinks(p)
|
||||
}
|
@ -0,0 +1,188 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-github/v56/github"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/synctv-org/synctv/cmd/flags"
|
||||
"github.com/synctv-org/synctv/utils"
|
||||
)
|
||||
|
||||
const (
|
||||
owner = "synctv-org"
|
||||
repo = "synctv"
|
||||
)
|
||||
|
||||
var (
|
||||
Version string = "dev"
|
||||
WebVersion string = "dev"
|
||||
GitCommit string
|
||||
)
|
||||
|
||||
type VersionInfo struct {
|
||||
current string
|
||||
latest *github.RepositoryRelease
|
||||
dev *github.RepositoryRelease
|
||||
c *github.Client
|
||||
|
||||
baseURL string
|
||||
}
|
||||
|
||||
func WithBaseURL(baseURL string) VersionInfoConf {
|
||||
return func(v *VersionInfo) {
|
||||
v.baseURL = baseURL
|
||||
}
|
||||
}
|
||||
|
||||
type VersionInfoConf func(*VersionInfo)
|
||||
|
||||
func NewVersionInfo(conf ...VersionInfoConf) (*VersionInfo, error) {
|
||||
v := &VersionInfo{
|
||||
current: Version,
|
||||
}
|
||||
for _, c := range conf {
|
||||
c(v)
|
||||
}
|
||||
return v, v.fix()
|
||||
}
|
||||
|
||||
func (v *VersionInfo) fix() (err error) {
|
||||
if v.baseURL == "" {
|
||||
v.baseURL = "https://api.github.com/"
|
||||
}
|
||||
v.c, err = github.NewClient(nil).WithEnterpriseURLs(v.baseURL, "")
|
||||
return err
|
||||
}
|
||||
|
||||
func (v *VersionInfo) initLatest(ctx context.Context) (err error) {
|
||||
if v.latest != nil {
|
||||
return nil
|
||||
}
|
||||
v.latest, _, err = v.c.Repositories.GetLatestRelease(ctx, owner, repo)
|
||||
return
|
||||
}
|
||||
|
||||
func (v *VersionInfo) initDev(ctx context.Context) (err error) {
|
||||
if v.dev != nil {
|
||||
return nil
|
||||
}
|
||||
v.dev, _, err = v.c.Repositories.GetReleaseByTag(ctx, owner, repo, "dev")
|
||||
return
|
||||
}
|
||||
|
||||
func (v *VersionInfo) Current() string {
|
||||
return v.current
|
||||
}
|
||||
|
||||
func (v *VersionInfo) Latest(ctx context.Context) (string, error) {
|
||||
if err := v.initLatest(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return v.latest.GetTagName(), nil
|
||||
}
|
||||
|
||||
func (v *VersionInfo) CheckLatest(ctx context.Context) (string, error) {
|
||||
release, _, err := v.c.Repositories.GetLatestRelease(ctx, owner, repo)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
v.latest = release
|
||||
return release.GetTagName(), nil
|
||||
}
|
||||
|
||||
func (v *VersionInfo) LatestBinaryURL(ctx context.Context) (string, error) {
|
||||
if err := v.initLatest(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return getBinaryURL(v.latest)
|
||||
}
|
||||
|
||||
func (v *VersionInfo) DevBinaryURL(ctx context.Context) (string, error) {
|
||||
if err := v.initDev(ctx); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return getBinaryURL(v.dev)
|
||||
}
|
||||
|
||||
func getBinaryURL(repo *github.RepositoryRelease) (string, error) {
|
||||
prefix := fmt.Sprintf("synctv-%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||
for _, a := range repo.Assets {
|
||||
if strings.HasPrefix(a.GetName(), prefix) {
|
||||
return a.GetBrowserDownloadURL(), nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("no binary found")
|
||||
}
|
||||
|
||||
// NeedUpdate return true if current version is less than latest version
|
||||
// if current version is dev, always return false
|
||||
func (v *VersionInfo) NeedUpdate(ctx context.Context) (bool, error) {
|
||||
if v.Current() == "dev" {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
latest, err := v.Latest(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
comp, err := utils.CompVersion(v.Current(), latest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch comp {
|
||||
case utils.VersionEqual:
|
||||
return false, nil
|
||||
case utils.VersionLess:
|
||||
return true, nil
|
||||
case utils.VersionGreater:
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (v *VersionInfo) SelfUpdate(ctx context.Context) (err error) {
|
||||
if flags.Dev {
|
||||
log.Info("self update: dev mode, update to latest dev version")
|
||||
} else if v.Current() != "dev" {
|
||||
latest, err := v.Latest(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comp, err := utils.CompVersion(v.Current(), latest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch comp {
|
||||
case utils.VersionEqual:
|
||||
log.Infof("self update: current version is latest: %s", v.Current())
|
||||
return nil
|
||||
case utils.VersionLess:
|
||||
log.Infof("self update: current version is less than latest: %s -> %s", v.Current(), latest)
|
||||
case utils.VersionGreater:
|
||||
log.Infof("self update: current version is greater than latest: %s ? %s", v.Current(), latest)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
log.Info("self update: current version is dev, force update")
|
||||
}
|
||||
|
||||
var url string
|
||||
if flags.Dev {
|
||||
url, err = v.DevBinaryURL(ctx)
|
||||
} else {
|
||||
url, err = v.LatestBinaryURL(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return SelfUpdate(ctx, url)
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package version_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/synctv-org/synctv/internal/version"
|
||||
)
|
||||
|
||||
func TestCheckLatest(t *testing.T) {
|
||||
v, err := version.NewVersionInfo()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := v.CheckLatest(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(s)
|
||||
}
|
||||
|
||||
func TestLatestBinaryURL(t *testing.T) {
|
||||
v, err := version.NewVersionInfo()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, err := v.LatestBinaryURL(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Log(s)
|
||||
}
|
Loading…
Reference in New Issue