Feat: self-update command

pull/21/head
zijiren233 2 years ago
parent b7af32adfd
commit 2285bc86b9

@ -118,9 +118,9 @@ function FixArgs() {
fi
fi
LDFLAGS="$LDFLAGS \
-X 'github.com/synctv-org/synctv/internal/conf.Version=$VERSION' \
-X 'github.com/synctv-org/synctv/internal/conf.WebVersion=$WEB_VERSION' \
-X 'github.com/synctv-org/synctv/internal/conf.GitCommit=$GIT_COMMIT'"
-X 'github.com/synctv-org/synctv/internal/version.Version=$VERSION' \
-X 'github.com/synctv-org/synctv/internal/version.WebVersion=$WEB_VERSION' \
-X 'github.com/synctv-org/synctv/internal/version.GitCommit=$GIT_COMMIT'"
BUILD_DIR="$(echo "$BUILD_DIR" | sed 's#/$##')"
}

@ -6,7 +6,7 @@ import (
"github.com/spf13/cobra"
"github.com/synctv-org/synctv/cmd/flags"
"github.com/synctv-org/synctv/internal/conf"
"github.com/synctv-org/synctv/internal/version"
)
var RootCmd = &cobra.Command{
@ -23,7 +23,7 @@ func Execute() {
}
func init() {
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", conf.Version == "dev", "start with dev mode")
RootCmd.PersistentFlags().BoolVar(&flags.Dev, "dev", version.Version == "dev", "start with dev mode")
RootCmd.PersistentFlags().BoolVar(&flags.LogStd, "log-std", true, "log to std")
RootCmd.PersistentFlags().BoolVar(&flags.EnvNoPrefix, "env-no-prefix", false, "env no SYNCTV_ prefix")
RootCmd.PersistentFlags().BoolVar(&flags.SkipConfig, "skip-config", false, "skip config")

@ -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)
}

@ -5,7 +5,7 @@ import (
"runtime"
"github.com/spf13/cobra"
"github.com/synctv-org/synctv/internal/conf"
"github.com/synctv-org/synctv/internal/version"
)
var VersionCmd = &cobra.Command{
@ -13,9 +13,9 @@ var VersionCmd = &cobra.Command{
Short: "Print the version number of Sync TV Server",
Long: `All software has versions. This is Sync TV Server's`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("synctv %s\n", conf.Version)
fmt.Printf("- web/version: %s\n", conf.WebVersion)
fmt.Printf("- git/commit: %s\n", conf.GitCommit)
fmt.Printf("synctv %s\n", version.Version)
fmt.Printf("- web/version: %s\n", version.WebVersion)
fmt.Printf("- git/commit: %s\n", version.GitCommit)
fmt.Printf("- os/platform: %s\n", runtime.GOOS)
fmt.Printf("- os/arch: %s\n", runtime.GOARCH)
fmt.Printf("- go/version: %s\n", runtime.Version())

@ -4,10 +4,12 @@ go 1.20
require (
github.com/caarlos0/env/v9 v9.0.0
github.com/cavaliergopher/grab/v3 v3.0.1
github.com/gin-contrib/cors v1.4.0
github.com/gin-gonic/gin v1.9.1
github.com/go-resty/resty/v2 v2.9.1
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/google/go-github/v56 v56.0.0
github.com/google/uuid v1.3.1
github.com/gorilla/websocket v1.5.0
github.com/json-iterator/go v1.1.12
@ -40,6 +42,7 @@ require (
github.com/go-playground/validator/v10 v10.15.5 // indirect
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.5 // indirect

@ -6,6 +6,8 @@ github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZF
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/caarlos0/env/v9 v9.0.0 h1:SI6JNsOA+y5gj9njpgybykATIylrRMklbs5ch6wO6pc=
github.com/caarlos0/env/v9 v9.0.0/go.mod h1:ye5mlCVMYh6tZ+vCgrs/B95sj88cg5Tlnc0XIzgZ020=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
@ -17,8 +19,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g=
@ -51,8 +51,13 @@ github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJ
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4=
github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98 h1:pUa4ghanp6q4IJHwE9RwLgmVFfReJN+KbQ8ExNEUUoQ=
github.com/google/pprof v0.0.0-20230926050212-f7f687d19a98/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=

@ -24,14 +24,13 @@ func setLog(l *logrus.Logger) {
func InitLog() {
setLog(logrus.StandardLogger())
logConfig := conf.Conf.Log
if logConfig.Enable {
if conf.Conf.Log.Enable {
var l = &lumberjack.Logger{
Filename: logConfig.FilePath,
MaxSize: logConfig.MaxSize,
MaxBackups: logConfig.MaxBackups,
MaxAge: logConfig.MaxAge,
Compress: logConfig.Compress,
Filename: conf.Conf.Log.FilePath,
MaxSize: conf.Conf.Log.MaxSize,
MaxBackups: conf.Conf.Log.MaxBackups,
MaxAge: conf.Conf.Log.MaxAge,
Compress: conf.Conf.Log.Compress,
}
if err := l.Rotate(); err != nil {
logrus.Fatalf("log: rotate log file error: %v", err)
@ -39,8 +38,9 @@ func InitLog() {
var w io.Writer = colorable.NewNonColorableWriter(l)
if flags.Dev || flags.LogStd {
w = io.MultiWriter(os.Stdout, w)
logrus.Infof("log: enable log to stdout and file: %s", conf.Conf.Log.FilePath)
} else {
logrus.Infof("log: disable log to stdout, only log to file: %s", logConfig.FilePath)
logrus.Infof("log: disable log to stdout, only log to file: %s", conf.Conf.Log.FilePath)
}
logrus.SetOutput(w)
}

@ -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/",
}
}

@ -3,9 +3,3 @@ package conf
var (
Conf *Config
)
var (
Version string = "dev"
WebVersion string = "dev"
GitCommit string
)

@ -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)
}

@ -4,6 +4,8 @@ import (
"math/rand"
"os"
"path/filepath"
"strconv"
"strings"
yamlcomment "github.com/zijiren233/yaml-comment"
"gopkg.in/yaml.v3"
@ -86,3 +88,48 @@ func ReadYaml(file string, module any) error {
defer f.Close()
return yaml.NewDecoder(f).Decode(module)
}
const (
VersionEqual = iota
VersionGreater
VersionLess
)
func CompVersion(v1, v2 string) (int, error) {
if v1 == v2 {
return VersionEqual, nil
}
v1s, err := SplitVersion(strings.TrimLeft(v1, "v"))
if err != nil {
return VersionEqual, err
}
v2s, err := SplitVersion(strings.TrimLeft(v2, "v"))
if err != nil {
return VersionEqual, err
}
for i := 0; i < len(v1s) && i < len(v2s); i++ {
if v1s[i] > v2s[i] {
return VersionGreater, nil
} else if v1s[i] < v2s[i] {
return VersionLess, nil
}
}
if len(v1s) > len(v2s) {
return VersionGreater, nil
} else if len(v1s) < len(v2s) {
return VersionLess, nil
}
return VersionGreater, nil
}
func SplitVersion(v string) ([]int, error) {
var vs []int
for _, s := range strings.Split(v, ".") {
i, err := strconv.Atoi(s)
if err != nil {
return nil, err
}
vs = append(vs, i)
}
return vs, nil
}

@ -54,3 +54,15 @@ func TestGetPageItems(t *testing.T) {
})
}
}
func FuzzCompVersion(f *testing.F) {
f.Add("v1.0.0", "v1.0.1")
f.Add("v0.2.9", "v1.5.2")
f.Fuzz(func(t *testing.T, a, b string) {
t.Logf("a: %s, b: %s", a, b)
_, err := utils.CompVersion(a, b)
if err != nil {
t.Errorf("CompVersion error = %v", err)
}
})
}

Loading…
Cancel
Save