Skip to content

Commit

Permalink
feat: self update (#806)
Browse files Browse the repository at this point in the history
Added package `update` and `semver` that can be used for selfupdate.

Most of the code was copied from [go-selfupdate](https://github.com/creativeprojects/go-selfupdate), [go-update](https://github.com/inconshreveable/go-update) and [semver](https://github.com/Masterminds/semver). Most of the unneeded code has been removed, so there is no need to add new dependencies.

In addition, users with the `mips` arch will not be able to selfupdate, as there is no way to get the float of the MIPS arch.

Tested OS and arch:
- [x] linux/amd64
- [x] linux/arm64
- [x] windows/amd64
  • Loading branch information
WaterLemons2k authored Aug 6, 2023
1 parent 57b3aa6 commit 352e6b8
Show file tree
Hide file tree
Showing 15 changed files with 1,000 additions and 0 deletions.
8 changes: 8 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/jeessy2/ddns-go/v5/config"
"github.com/jeessy2/ddns-go/v5/dns"
"github.com/jeessy2/ddns-go/v5/util"
"github.com/jeessy2/ddns-go/v5/util/update"
"github.com/jeessy2/ddns-go/v5/web"
"github.com/kardianos/service"
)
Expand All @@ -24,6 +25,9 @@ import (
// ddns-go version
var versionFlag = flag.Bool("v", false, "ddns-go 版本")

// 更新 ddns-go
var updateFlag = flag.Bool("u", false, "更新 ddns-go")

// 监听地址
var listen = flag.String("l", ":9876", "监听地址")

Expand Down Expand Up @@ -63,6 +67,10 @@ func main() {
fmt.Println(version)
return
}
if *updateFlag {
update.Self(version)
return
}
if _, err := net.ResolveTCPAddr("tcp", *listen); err != nil {
log.Fatalf("解析监听地址异常,%s", err)
}
Expand Down
118 changes: 118 additions & 0 deletions util/semver/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Based on https://github.com/Masterminds/semver/blob/v3.2.1/version.go

package semver

import (
"bytes"
"fmt"
"regexp"
"strconv"
"strings"
)

// 在 init() 中创建的正则表达式的编译版本被缓存在这里,这样
// 它只需要被创建一次。
var versionRegex *regexp.Regexp

// semVerRegex 是用于解析语义化版本的正则表达式。
const semVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`

// Version 表示单独的语义化版本。
type Version struct {
major, minor, patch uint64
}

func init() {
versionRegex = regexp.MustCompile("^" + semVerRegex + "$")
}

// NewVersion 解析给定的版本并返回 Version 实例,如果
// 无法解析该版本则返回错误。如果版本是类似于 SemVer 的版本,则会
// 尝试将其转换为 SemVer。
func NewVersion(v string) (*Version, error) {
m := versionRegex.FindStringSubmatch(v)
if m == nil {
return nil, fmt.Errorf("%s 不是有效的语义化版本", v)
}

sv := &Version{}

var err error
sv.major, err = strconv.ParseUint(m[1], 10, 64)
if err != nil {
return nil, fmt.Errorf("解析版本号时出错:%s", err)
}

if m[2] != "" {
sv.minor, err = strconv.ParseUint(strings.TrimPrefix(m[2], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("解析版本号时出错:%s", err)
}
} else {
sv.minor = 0
}

if m[3] != "" {
sv.patch, err = strconv.ParseUint(strings.TrimPrefix(m[3], "."), 10, 64)
if err != nil {
return nil, fmt.Errorf("解析版本号时出错:%s", err)
}
} else {
sv.patch = 0
}

return sv, nil
}

// String 将 Version 对象转换为字符串。
// 注意,如果原始版本包含前缀 v,则转换后的版本将不包含 v。
// 根据规范,语义版本不包含前缀 v,而在实现上则是可选的。
func (v Version) String() string {
var buf bytes.Buffer

fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch)

return buf.String()
}

// GreaterThan 测试一个版本是否大于另一个版本。
func (v *Version) GreaterThan(o *Version) bool {
return v.compare(o) > 0
}

// GreaterThanOrEqual 测试一个版本是否大于或等于另一个版本。
func (v *Version) GreaterThanOrEqual(o *Version) bool {
return v.compare(o) >= 0
}

// compare 比较当前版本与另一个版本。如果当前版本小于另一个版本则返回 -1;如果两个版本相等则返回 0;如果当前版本大于另一个版本,则返回 1。
//
// 版本比较是基于 X.Y.Z 格式进行的。
func (v *Version) compare(o *Version) int {
// 比较主版本号、次版本号和修订号。如果
// 发现差异则返回比较结果。
if d := compareSegment(v.major, o.major); d != 0 {
return d
}
if d := compareSegment(v.minor, o.minor); d != 0 {
return d
}
if d := compareSegment(v.patch, o.patch); d != 0 {
return d
}

return 0
}

func compareSegment(v, o uint64) int {
if v < o {
return -1
}
if v > o {
return 1
}

return 0
}
207 changes: 207 additions & 0 deletions util/semver/version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// Based on https://github.com/Masterminds/semver/blob/v3.2.1/version_test.go

package semver

import "testing"

func TestNewVersion(t *testing.T) {
tests := []struct {
version string
err bool
}{
{"1.2.3", false},
{"1.2.3+test.01", false},
{"1.2.3-alpha.-1", false},
{"v1.2.3", false},
{"1.0", false},
{"v1.0", false},
{"1", false},
{"v1", false},
{"1.2.beta", true},
{"v1.2.beta", true},
{"foo", true},
{"1.2-5", false},
{"v1.2-5", false},
{"1.2-beta.5", false},
{"v1.2-beta.5", false},
{"\n1.2", true},
{"\nv1.2", true},
{"1.2.0-x.Y.0+metadata", false},
{"v1.2.0-x.Y.0+metadata", false},
{"1.2.0-x.Y.0+metadata-width-hyphen", false},
{"v1.2.0-x.Y.0+metadata-width-hyphen", false},
{"1.2.3-rc1-with-hyphen", false},
{"v1.2.3-rc1-with-hyphen", false},
{"1.2.3.4", true},
{"v1.2.3.4", true},
{"1.2.2147483648", false},
{"1.2147483648.3", false},
{"2147483648.3.0", false},

// Due to having 4 parts these should produce an error. See
// https://github.com/Masterminds/semver/issues/185 for the reason for
// these tests.
{"12.3.4.1234", true},
{"12.23.4.1234", true},
{"12.3.34.1234", true},

// The SemVer spec in a pre-release expects to allow [0-9A-Za-z-].
{"20221209-update-renovatejson-v4", false},
}

for _, tc := range tests {
_, err := NewVersion(tc.version)
if tc.err && err == nil {
t.Fatalf("expected error for version: %s", tc.version)
} else if !tc.err && err != nil {
t.Fatalf("error for version %s: %s", tc.version, err)
}
}
}

func TestParts(t *testing.T) {
v, err := NewVersion("1.2.3")
if err != nil {
t.Error("Error parsing version 1.2.3")
}

if v.major != 1 {
t.Error("major returning wrong value")
}
if v.minor != 2 {
t.Error("minor returning wrong value")
}
if v.patch != 3 {
t.Error("patch returning wrong value")
}
}

func TestCoerceString(t *testing.T) {
tests := []struct {
version string
expected string
}{
{"1.2.3", "1.2.3"},
{"v1.2.3", "1.2.3"},
{"1.0", "1.0.0"},
{"v1.0", "1.0.0"},
{"1", "1.0.0"},
{"v1", "1.0.0"},
}

for _, tc := range tests {
v, err := NewVersion(tc.version)
if err != nil {
t.Errorf("Error parsing version %s", tc)
}

s := v.String()
if s != tc.expected {
t.Errorf("Error generating string. Expected '%s' but got '%s'", tc.expected, s)
}
}
}

func TestCompare(t *testing.T) {
tests := []struct {
v1 string
v2 string
expected int
}{
{"1.2.3", "1.5.1", -1},
{"2.2.3", "1.5.1", 1},
{"2.2.3", "2.2.2", 1},
}

for _, tc := range tests {
v1, err := NewVersion(tc.v1)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}

v2, err := NewVersion(tc.v2)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}

a := v1.compare(v2)
e := tc.expected
if a != e {
t.Errorf(
"Comparison of '%s' and '%s' failed. Expected '%d', got '%d'",
tc.v1, tc.v2, e, a,
)
}
}
}

func TestGreaterThan(t *testing.T) {
tests := []struct {
v1 string
v2 string
expected bool
}{
{"1.2.3", "1.5.1", false},
{"2.2.3", "1.5.1", true},
{"3.2-beta", "3.2-beta", false},
{"3.2.0-beta.1", "3.2.0-beta.5", false},
{"7.43.0-SNAPSHOT.99", "7.43.0-SNAPSHOT.103", false},
{"7.43.0-SNAPSHOT.99", "7.43.0-SNAPSHOT.BAR", false},
}

for _, tc := range tests {
v1, err := NewVersion(tc.v1)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}

v2, err := NewVersion(tc.v2)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}

a := v1.GreaterThan(v2)
e := tc.expected
if a != e {
t.Errorf(
"Comparison of '%s' and '%s' failed. Expected '%t', got '%t'",
tc.v1, tc.v2, e, a,
)
}
}
}

func TestGreaterThanOrEqual(t *testing.T) {
tests := []struct {
v1 string
v2 string
expected bool
}{
{"1.2.3", "1.5.1", false},
{"2.2.3", "1.5.1", true},
{"3.2-beta", "3.2-beta", true},
{"3.2-beta.4", "3.2-beta.2", true},
{"7.43.0-SNAPSHOT.FOO", "7.43.0-SNAPSHOT.103", true},
}

for _, tc := range tests {
v1, err := NewVersion(tc.v1)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}

v2, err := NewVersion(tc.v2)
if err != nil {
t.Errorf("Error parsing version: %s", err)
}

a := v1.GreaterThanOrEqual(v2)
e := tc.expected
if a != e {
t.Errorf(
"Comparison of '%s' and '%s' failed. Expected '%t', got '%t'",
tc.v1, tc.v2, e, a,
)
}
}
}
Loading

0 comments on commit 352e6b8

Please sign in to comment.