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
|
package conf
|
||||||
|
|
||||||
type GlobalConfig struct {
|
type GlobalConfig struct {
|
||||||
|
GitHubBaseURL string `yaml:"github_base_url" lc:"default: https://api.github.com/" env:"GITHUB_BASE_URL"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultGlobalConfig() GlobalConfig {
|
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